diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..6e87a003 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +# Editor configuration, see http://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000..4a938faa --- /dev/null +++ b/.eslintignore @@ -0,0 +1,5 @@ +coverage +dist +node_modules +tmp +tools \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 00000000..6615d5ed --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,67 @@ +{ + "root": true, + "plugins": ["@nrwl/nx", "@typescript-eslint"], + "parser": "@typescript-eslint/parser", + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:@typescript-eslint/recommended-requiring-type-checking" + ], + "parserOptions": { + "project": ["tsconfig.base.json"] + }, + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": { + "@nrwl/nx/enforce-module-boundaries": [ + "error", + { + "enforceBuildableLibDependency": true, + "allow": ["api-interfaces"], + "depConstraints": [ + { + "sourceTag": "*", + "onlyDependOnLibsWithTags": ["*"] + } + ] + } + ] + } + }, + { + "files": ["*.ts", "*.tsx"], + "extends": ["plugin:@nrwl/nx/typescript"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "extends": ["plugin:@nrwl/nx/javascript"], + "rules": {} + }, + { + "files": [ + "*.spec.ts", + "*.spec.tsx", + "*.spec.js", + "*.spec.jsx", + "*.test.ts", + "*.test.tsx", + "*.test.js", + "*.test.jsx" + ], + "env": { + "jest": true + }, + "rules": { + "@typescript-eslint/unbound-method": "off" + } + } + ], + "rules": { + "no-console": "warn", + "@typescript-eslint/no-unsafe-call": "off", + "@typescript-eslint/no-unsafe-member-access": "off", + "@typescript-eslint/no-unsafe-assignment": "off" + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..707c7d8a --- /dev/null +++ b/.gitignore @@ -0,0 +1,45 @@ +# See http://help.github.com/ignore-files/ for more about ignoring files. + +# compiled output +dist +tmp +/out-tsc +artifacts + +# dependencies +node_modules + +# IDEs and editors +/.idea +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# IDE - VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +# misc +/.sass-cache +/connect.lock +/coverage +junit.xml +/libpeerconnection.log +npm-debug.log +yarn-error.log +testem.log +/typings +.env + +# System Files +.DS_Store +Thumbs.db + +# Yarn +.yarn/* diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 00000000..d933288e --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +npx nx affected run-many --target=lint diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100755 index 00000000..e6f32b45 --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1,4 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +npx nx affected run-many --target=test diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 00000000..4129edb0 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +v16.18.1 \ No newline at end of file diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..d0b804da --- /dev/null +++ b/.prettierignore @@ -0,0 +1,4 @@ +# Add files here to ignore them from prettier formatting + +/dist +/coverage diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..544138be --- /dev/null +++ b/.prettierrc @@ -0,0 +1,3 @@ +{ + "singleQuote": true +} diff --git a/.storybook/main.js b/.storybook/main.js new file mode 100644 index 00000000..1492ac61 --- /dev/null +++ b/.storybook/main.js @@ -0,0 +1,11 @@ +module.exports = { + stories: [], + addons: ['@storybook/addon-essentials'], + // uncomment the property below if you want to apply some webpack config globally + // webpackFinal: async (config, { configType }) => { + // // Make whatever fine-grained changes you need that should apply to all storybook configs + + // // Return the altered config + // return config; + // }, +}; diff --git a/.storybook/tsconfig.json b/.storybook/tsconfig.json new file mode 100644 index 00000000..7dd91521 --- /dev/null +++ b/.storybook/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../tsconfig.base.json", + "exclude": [ + "../**/*.spec.js", + "../**/*.test.js", + "../**/*.spec.ts", + "../**/*.test.ts", + "../**/*.spec.tsx", + "../**/*.test.tsx", + "../**/*.spec.jsx", + "../**/*.test.jsx" + ], + "include": ["../**/*"] +} diff --git a/babel.config.json b/babel.config.json new file mode 100644 index 00000000..065aee77 --- /dev/null +++ b/babel.config.json @@ -0,0 +1,3 @@ +{ + "babelrcRoots": ["*"] +} diff --git a/jest.config.ts b/jest.config.ts new file mode 100644 index 00000000..2a738f77 --- /dev/null +++ b/jest.config.ts @@ -0,0 +1,5 @@ +import { getJestProjects } from '@nrwl/jest'; + +export default { + projects: getJestProjects(), +}; diff --git a/jest.preset.js b/jest.preset.js new file mode 100644 index 00000000..e6c8ebea --- /dev/null +++ b/jest.preset.js @@ -0,0 +1,3 @@ +const nxPreset = require('@nrwl/jest/preset').default; + +module.exports = { ...nxPreset }; diff --git a/jest.proxy.setup.ts b/jest.proxy.setup.ts new file mode 100644 index 00000000..6032e59d --- /dev/null +++ b/jest.proxy.setup.ts @@ -0,0 +1,9 @@ +import nodeProxy from 'node-global-proxy'; + +if ( + process.env['HTTP_PROXY'] && + process.env['NODE_TLS_REJECT_UNAUTHORIZED'] === '0' +) { + nodeProxy.setConfig(process.env['HTTP_PROXY']); + nodeProxy.start(); +} diff --git a/libs/.gitkeep b/libs/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/libs/api-client/.babelrc b/libs/api-client/.babelrc new file mode 100644 index 00000000..e24a5465 --- /dev/null +++ b/libs/api-client/.babelrc @@ -0,0 +1,10 @@ +{ + "presets": [ + [ + "@nrwl/web/babel", + { + "useBuiltIns": "usage" + } + ] + ] +} diff --git a/libs/api-client/README.md b/libs/api-client/README.md new file mode 100644 index 00000000..34c3e145 --- /dev/null +++ b/libs/api-client/README.md @@ -0,0 +1,11 @@ +# api-client + +This library was generated with [Nx](https://nx.dev). + +## Building + +Run `nx build api-client` to build the library. + +## Running unit tests + +Run `nx test api-client` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/api-client/jest.config.ts b/libs/api-client/jest.config.ts new file mode 100644 index 00000000..ce6d62b1 --- /dev/null +++ b/libs/api-client/jest.config.ts @@ -0,0 +1,15 @@ +/* eslint-disable */ +export default { + displayName: 'api-client', + preset: '../../jest.preset.js', + globals: { + 'ts-jest': { + tsconfig: '/tsconfig.spec.json', + }, + }, + transform: { + '^.+\\.[tj]s$': 'ts-jest', + }, + moduleFileExtensions: ['ts', 'js', 'html'], + coverageDirectory: '../../coverage/libs/api-client', +}; diff --git a/libs/api-client/package.json b/libs/api-client/package.json new file mode 100644 index 00000000..293a1227 --- /dev/null +++ b/libs/api-client/package.json @@ -0,0 +1,5 @@ +{ + "name": "@dvp/api-client", + "version": "0.0.1", + "type": "commonjs" +} diff --git a/libs/api-client/project.json b/libs/api-client/project.json new file mode 100644 index 00000000..b86a68f4 --- /dev/null +++ b/libs/api-client/project.json @@ -0,0 +1,34 @@ +{ + "name": "api-client", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/api-client/src", + "projectType": "library", + "targets": { + "build": { + "executor": "@nrwl/js:tsc", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/libs/api-client", + "main": "libs/api-client/src/index.ts", + "tsConfig": "libs/api-client/tsconfig.lib.json", + "assets": ["libs/api-client/*.md"] + } + }, + "lint": { + "executor": "@nrwl/linter:eslint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["libs/api-client/**/*.ts"] + } + }, + "test": { + "executor": "@nrwl/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/api-client/jest.config.ts", + "passWithNoTests": true + } + } + }, + "tags": [] +} diff --git a/libs/api-client/src/.gitignore b/libs/api-client/src/.gitignore new file mode 100644 index 00000000..149b5765 --- /dev/null +++ b/libs/api-client/src/.gitignore @@ -0,0 +1,4 @@ +wwwroot/*.js +node_modules +typings +dist diff --git a/libs/api-client/src/.npmignore b/libs/api-client/src/.npmignore new file mode 100644 index 00000000..999d88df --- /dev/null +++ b/libs/api-client/src/.npmignore @@ -0,0 +1 @@ +# empty npmignore to ensure all required files (e.g., in the dist folder) are published by npm \ No newline at end of file diff --git a/libs/api-client/src/.openapi-generator-ignore b/libs/api-client/src/.openapi-generator-ignore new file mode 100644 index 00000000..7484ee59 --- /dev/null +++ b/libs/api-client/src/.openapi-generator-ignore @@ -0,0 +1,23 @@ +# OpenAPI Generator Ignore +# Generated by openapi-generator https://github.com/openapitools/openapi-generator + +# Use this file to prevent files from being overwritten by the generator. +# The patterns follow closely to .gitignore or .dockerignore. + +# As an example, the C# client generator defines ApiClient.cs. +# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: +#ApiClient.cs + +# You can match any string of characters against a directory, file or extension with a single asterisk (*): +#foo/*/qux +# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux + +# You can recursively match patterns against a directory, file or extension with a double asterisk (**): +#foo/**/qux +# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux + +# You can also negate patterns with an exclamation (!). +# For example, you can ignore all files in a docs folder with the file extension .md: +#docs/*.md +# Then explicitly reverse the ignore rule for a single file: +#!docs/README.md diff --git a/libs/api-client/src/.openapi-generator/FILES b/libs/api-client/src/.openapi-generator/FILES new file mode 100644 index 00000000..a80cd4f0 --- /dev/null +++ b/libs/api-client/src/.openapi-generator/FILES @@ -0,0 +1,8 @@ +.gitignore +.npmignore +api.ts +base.ts +common.ts +configuration.ts +git_push.sh +index.ts diff --git a/libs/api-client/src/.openapi-generator/VERSION b/libs/api-client/src/.openapi-generator/VERSION new file mode 100644 index 00000000..0df17dd0 --- /dev/null +++ b/libs/api-client/src/.openapi-generator/VERSION @@ -0,0 +1 @@ +6.2.1 \ No newline at end of file diff --git a/libs/api-client/src/api.ts b/libs/api-client/src/api.ts new file mode 100644 index 00000000..9e7e908d --- /dev/null +++ b/libs/api-client/src/api.ts @@ -0,0 +1,1096 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * DVP + * API for the Digital Verification Platform + * + * The version of the OpenAPI document: 0.1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import globalAxios, { + AxiosInstance, + AxiosPromise, + AxiosRequestConfig, +} from 'axios'; +import { Configuration } from './configuration'; +// Some imports not used depending on template conditions +// @ts-ignore +import { + assertParamExists, + createRequestFunction, + DUMMY_BASE_URL, + serializeDataIfNeeded, + setSearchParams, + toPathString, +} from './common'; +// @ts-ignore +import { BaseAPI, BASE_PATH, RequestArgs } from './base'; + +/** + * A JSON-LD Verifiable Credential without a proof. + * @export + * @interface Credential + */ +export interface Credential { + [key: string]: any; + + /** + * The JSON-LD context of the credential. + * @type {Array} + * @memberof Credential + */ + '@context': Array; + /** + * The ID of the credential. + * @type {string} + * @memberof Credential + */ + id?: string; + /** + * The JSON-LD type of the credential. + * @type {Array} + * @memberof Credential + */ + type: Array; + /** + * The issuanceDate + * @type {string} + * @memberof Credential + */ + issuanceDate: string; + /** + * The expirationDate + * @type {string} + * @memberof Credential + */ + expirationDate?: string; + /** + * The subject + * @type {object} + * @memberof Credential + */ + credentialSubject: object; + /** + * + * @type {Issuer} + * @memberof Credential + */ + issuer: Issuer; +} +/** + * + * @export + * @interface DocumentUploadRequest + */ +export interface DocumentUploadRequest { + /** + * strigified document to upload + * @type {string} + * @memberof DocumentUploadRequest + */ + document: string; + /** + * + * @type {string} + * @memberof DocumentUploadRequest + */ + documentId?: string; + /** + * Key used for encryption + * @type {string} + * @memberof DocumentUploadRequest + */ + encryptionKey?: string; +} +/** + * + * @export + * @interface DocumentUploadResponse + */ +export interface DocumentUploadResponse { + /** + * + * @type {string} + * @memberof DocumentUploadResponse + */ + documentId: string; + /** + * Key used for encryption + * @type {string} + * @memberof DocumentUploadResponse + */ + encryptionKey: string; +} +/** + * + * @export + * @interface EncryptedDocumentObject + */ +export interface EncryptedDocumentObject { + /** + * + * @type {EncryptedDocumentObjectDocument} + * @memberof EncryptedDocumentObject + */ + document: EncryptedDocumentObjectDocument; +} +/** + * + * @export + * @interface EncryptedDocumentObjectDocument + */ +export interface EncryptedDocumentObjectDocument { + /** + * Encrypted verifiable credential + * @type {string} + * @memberof EncryptedDocumentObjectDocument + */ + cipherText: string; + /** + * Initialisation vector + * @type {string} + * @memberof EncryptedDocumentObjectDocument + */ + iv: string; + /** + * Message authentication code (MAC) + * @type {string} + * @memberof EncryptedDocumentObjectDocument + */ + tag: string; + /** + * Encryption algorithm identifier (OA) + * @type {string} + * @memberof EncryptedDocumentObjectDocument + */ + type: string; +} +/** + * An object containing references to the source of an error. + * @export + * @interface ErrorSource + */ +export interface ErrorSource { + /** + * A JSON Pointer which describes which property in the request object to which an error message relates. For more details on JSON pointers see [RFC6901](https://tools.ietf.org/html/rfc6901). + * @type {string} + * @memberof ErrorSource + */ + pointer?: string; + /** + * Describes the location of the data to which the error message is related. - **\"REQUEST\"** - Indicates the message relates to a _property_ within the request object. The `pointer` property should be populated in this case. - **\"QUERY\"** - Indicates the message relates to a _query_ parameter. The `parameter` property should be populated in this case. - **\"ID\"** - Indicates the message relates to the identifier of the REST resource. The `parameter` property _may optionally_ be populated in this case. + * @type {string} + * @memberof ErrorSource + */ + location?: ErrorSourceLocationEnum; + /** + * A string indicating which URI query parameter caused the error. + * @type {string} + * @memberof ErrorSource + */ + parameter?: string; +} + +export const ErrorSourceLocationEnum = { + Request: 'REQUEST', + Query: 'QUERY', + Id: 'ID', +} as const; + +export type ErrorSourceLocationEnum = + typeof ErrorSourceLocationEnum[keyof typeof ErrorSourceLocationEnum]; + +/** + * A schema for the `errors` array. + * @export + * @interface ErrorsArray + */ +export interface ErrorsArray { + /** + * + * @type {Array} + * @memberof ErrorsArray + */ + errors?: Array; +} +/** + * The response returned when one or more errors have been encountered. + * @export + * @interface ErrorsResponseSchema + */ +export interface ErrorsResponseSchema { + /** + * + * @type {Array} + * @memberof ErrorsResponseSchema + */ + errors: Array; +} +/** + * + * @export + * @interface IssueCredentialRequest + */ +export interface IssueCredentialRequest { + /** + * + * @type {string} + * @memberof IssueCredentialRequest + */ + signingMethod?: IssueCredentialRequestSigningMethodEnum; + /** + * + * @type {Credential} + * @memberof IssueCredentialRequest + */ + credential: Credential; +} + +export const IssueCredentialRequestSigningMethodEnum = { + Svip: 'SVIP', + Oa: 'OA', +} as const; + +export type IssueCredentialRequestSigningMethodEnum = + typeof IssueCredentialRequestSigningMethodEnum[keyof typeof IssueCredentialRequestSigningMethodEnum]; + +/** + * + * @export + * @interface IssueCredentialResponse + */ +export interface IssueCredentialResponse { + /** + * + * @type {VerifiableCredential} + * @memberof IssueCredentialResponse + */ + verifiableCredential?: VerifiableCredential; +} +/** + * @type Issuer + * A JSON-LD Verifiable Credential Issuer. + * @export + */ +export type Issuer = IssuerOneOf | string; + +/** + * + * @export + * @interface IssuerOneOf + */ +export interface IssuerOneOf { + /** + * + * @type {string} + * @memberof IssuerOneOf + */ + id: string; + /** + * + * @type {string} + * @memberof IssuerOneOf + */ + name?: string; +} +/** + * A JSON-LD Linked Data proof. + * @export + * @interface LinkedDataProof + */ +export interface LinkedDataProof { + /** + * Linked Data Signature Suite used to produce proof. + * @type {string} + * @memberof LinkedDataProof + */ + type?: string; + /** + * Date the proof was created. + * @type {string} + * @memberof LinkedDataProof + */ + created?: string; + /** + * A value chosen by the verifier to mitigate authentication proof replay attacks. + * @type {string} + * @memberof LinkedDataProof + */ + challenge?: string; + /** + * The domain of the proof to restrict its use to a particular target. + * @type {string} + * @memberof LinkedDataProof + */ + domain?: string; + /** + * A value chosen by the creator of a proof to randomize proof values for privacy purposes. + * @type {string} + * @memberof LinkedDataProof + */ + nonce?: string; + /** + * Verification Method used to verify proof. + * @type {string} + * @memberof LinkedDataProof + */ + verificationMethod?: string; + /** + * The purpose of the proof to be used with verificationMethod. + * @type {string} + * @memberof LinkedDataProof + */ + proofPurpose?: string; + /** + * Detached JSON Web Signature. + * @type {string} + * @memberof LinkedDataProof + */ + jws?: string; + /** + * Value of the Linked Data proof. + * @type {string} + * @memberof LinkedDataProof + */ + proofValue?: string; +} +/** + * An object containing the details of a particular error. + * @export + * @interface ModelError + */ +export interface ModelError { + /** + * A unique identifier for the error occurrence, to provide traceability in application logs. + * @type {string} + * @memberof ModelError + */ + id?: string; + /** + * A provider-specific or enterprise defined error code. Codes must be in uppercase. + * @type {string} + * @memberof ModelError + */ + code: string; + /** + * A provider-specific or enterprise defined error message. + * @type {string} + * @memberof ModelError + */ + detail: string; + /** + * + * @type {ErrorSource} + * @memberof ModelError + */ + source?: ErrorSource; + /** + * A URL which leads to further details about the error (e.g. A help page). + * @type {string} + * @memberof ModelError + */ + helpUrl?: string; + /** + * Help text which can provide further assistance on the error. + * @type {string} + * @memberof ModelError + */ + helpText?: string; +} +/** + * A JSON-LD Verifiable Credential with a proof. + * @export + * @interface VerifiableCredential + */ +export interface VerifiableCredential { + /** + * The JSON-LD context of the credential. + * @type {Array} + * @memberof VerifiableCredential + */ + '@context': Array; + /** + * The ID of the credential. + * @type {string} + * @memberof VerifiableCredential + */ + id?: string; + /** + * The JSON-LD type of the credential. + * @type {Array} + * @memberof VerifiableCredential + */ + type: Array; + /** + * The issuanceDate + * @type {string} + * @memberof VerifiableCredential + */ + issuanceDate: string; + /** + * The expirationDate + * @type {string} + * @memberof VerifiableCredential + */ + expirationDate?: string; + /** + * The subject + * @type {object} + * @memberof VerifiableCredential + */ + credentialSubject: object; + /** + * + * @type {Issuer} + * @memberof VerifiableCredential + */ + issuer: Issuer; + /** + * + * @type {LinkedDataProof} + * @memberof VerifiableCredential + */ + proof?: LinkedDataProof; +} +/** + * + * @export + * @interface VerifiableCredentialAllOf + */ +export interface VerifiableCredentialAllOf { + /** + * + * @type {LinkedDataProof} + * @memberof VerifiableCredentialAllOf + */ + proof?: LinkedDataProof; +} +/** + * Object summarizing a verification + * @export + * @interface VerificationResult + */ +export interface VerificationResult { + /** + * The checks performed + * @type {Array} + * @memberof VerificationResult + */ + checks?: Array; + /** + * Warnings + * @type {Array} + * @memberof VerificationResult + */ + warnings?: Array; + /** + * Errors + * @type {Array} + * @memberof VerificationResult + */ + errors?: Array; +} +/** + * + * @export + * @interface VerifyCredentialRequest + */ +export interface VerifyCredentialRequest { + /** + * + * @type {VerifiableCredential} + * @memberof VerifyCredentialRequest + */ + verifiableCredential?: VerifiableCredential; +} + +/** + * CredentialsApi - axios parameter creator + * @export + */ +export const CredentialsApiAxiosParamCreator = function ( + configuration?: Configuration +) { + return { + /** + * Issues a credential and returns it in the response body. + * @summary Issues a credential and returns it in the response body. + * @param {IssueCredentialRequest} [issueCredentialRequest] Parameters for issuing the credential. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + issueCredential: async ( + issueCredentialRequest?: IssueCredentialRequest, + options: AxiosRequestConfig = {} + ): Promise => { + const localVarPath = `/issue`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { + method: 'POST', + ...baseOptions, + ...options, + }; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = + baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = { + ...localVarHeaderParameter, + ...headersFromBaseOptions, + ...options.headers, + }; + localVarRequestOptions.data = serializeDataIfNeeded( + issueCredentialRequest, + localVarRequestOptions, + configuration + ); + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * Verifies a verifiableCredential and returns a verificationResult in the response body. + * @summary Verifies a verifiableCredential and returns a verificationResult in the response body. + * @param {VerifyCredentialRequest} [verifyCredentialRequest] Parameters for verifying a verifiableCredential. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + verifyCredential: async ( + verifyCredentialRequest?: VerifyCredentialRequest, + options: AxiosRequestConfig = {} + ): Promise => { + const localVarPath = `/verify`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { + method: 'POST', + ...baseOptions, + ...options, + }; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = + baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = { + ...localVarHeaderParameter, + ...headersFromBaseOptions, + ...options.headers, + }; + localVarRequestOptions.data = serializeDataIfNeeded( + verifyCredentialRequest, + localVarRequestOptions, + configuration + ); + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + }; +}; + +/** + * CredentialsApi - functional programming interface + * @export + */ +export const CredentialsApiFp = function (configuration?: Configuration) { + const localVarAxiosParamCreator = + CredentialsApiAxiosParamCreator(configuration); + return { + /** + * Issues a credential and returns it in the response body. + * @summary Issues a credential and returns it in the response body. + * @param {IssueCredentialRequest} [issueCredentialRequest] Parameters for issuing the credential. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async issueCredential( + issueCredentialRequest?: IssueCredentialRequest, + options?: AxiosRequestConfig + ): Promise< + ( + axios?: AxiosInstance, + basePath?: string + ) => AxiosPromise + > { + const localVarAxiosArgs = await localVarAxiosParamCreator.issueCredential( + issueCredentialRequest, + options + ); + return createRequestFunction( + localVarAxiosArgs, + globalAxios, + BASE_PATH, + configuration + ); + }, + /** + * Verifies a verifiableCredential and returns a verificationResult in the response body. + * @summary Verifies a verifiableCredential and returns a verificationResult in the response body. + * @param {VerifyCredentialRequest} [verifyCredentialRequest] Parameters for verifying a verifiableCredential. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async verifyCredential( + verifyCredentialRequest?: VerifyCredentialRequest, + options?: AxiosRequestConfig + ): Promise< + ( + axios?: AxiosInstance, + basePath?: string + ) => AxiosPromise + > { + const localVarAxiosArgs = + await localVarAxiosParamCreator.verifyCredential( + verifyCredentialRequest, + options + ); + return createRequestFunction( + localVarAxiosArgs, + globalAxios, + BASE_PATH, + configuration + ); + }, + }; +}; + +/** + * CredentialsApi - factory interface + * @export + */ +export const CredentialsApiFactory = function ( + configuration?: Configuration, + basePath?: string, + axios?: AxiosInstance +) { + const localVarFp = CredentialsApiFp(configuration); + return { + /** + * Issues a credential and returns it in the response body. + * @summary Issues a credential and returns it in the response body. + * @param {IssueCredentialRequest} [issueCredentialRequest] Parameters for issuing the credential. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + issueCredential( + issueCredentialRequest?: IssueCredentialRequest, + options?: any + ): AxiosPromise { + return localVarFp + .issueCredential(issueCredentialRequest, options) + .then((request) => request(axios, basePath)); + }, + /** + * Verifies a verifiableCredential and returns a verificationResult in the response body. + * @summary Verifies a verifiableCredential and returns a verificationResult in the response body. + * @param {VerifyCredentialRequest} [verifyCredentialRequest] Parameters for verifying a verifiableCredential. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + verifyCredential( + verifyCredentialRequest?: VerifyCredentialRequest, + options?: any + ): AxiosPromise { + return localVarFp + .verifyCredential(verifyCredentialRequest, options) + .then((request) => request(axios, basePath)); + }, + }; +}; + +/** + * CredentialsApi - interface + * @export + * @interface CredentialsApi + */ +export interface CredentialsApiInterface { + /** + * Issues a credential and returns it in the response body. + * @summary Issues a credential and returns it in the response body. + * @param {IssueCredentialRequest} [issueCredentialRequest] Parameters for issuing the credential. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof CredentialsApiInterface + */ + issueCredential( + issueCredentialRequest?: IssueCredentialRequest, + options?: AxiosRequestConfig + ): AxiosPromise; + + /** + * Verifies a verifiableCredential and returns a verificationResult in the response body. + * @summary Verifies a verifiableCredential and returns a verificationResult in the response body. + * @param {VerifyCredentialRequest} [verifyCredentialRequest] Parameters for verifying a verifiableCredential. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof CredentialsApiInterface + */ + verifyCredential( + verifyCredentialRequest?: VerifyCredentialRequest, + options?: AxiosRequestConfig + ): AxiosPromise; +} + +/** + * CredentialsApi - object-oriented interface + * @export + * @class CredentialsApi + * @extends {BaseAPI} + */ +export class CredentialsApi extends BaseAPI implements CredentialsApiInterface { + /** + * Issues a credential and returns it in the response body. + * @summary Issues a credential and returns it in the response body. + * @param {IssueCredentialRequest} [issueCredentialRequest] Parameters for issuing the credential. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof CredentialsApi + */ + public issueCredential( + issueCredentialRequest?: IssueCredentialRequest, + options?: AxiosRequestConfig + ) { + return CredentialsApiFp(this.configuration) + .issueCredential(issueCredentialRequest, options) + .then((request) => request(this.axios, this.basePath)); + } + + /** + * Verifies a verifiableCredential and returns a verificationResult in the response body. + * @summary Verifies a verifiableCredential and returns a verificationResult in the response body. + * @param {VerifyCredentialRequest} [verifyCredentialRequest] Parameters for verifying a verifiableCredential. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof CredentialsApi + */ + public verifyCredential( + verifyCredentialRequest?: VerifyCredentialRequest, + options?: AxiosRequestConfig + ) { + return CredentialsApiFp(this.configuration) + .verifyCredential(verifyCredentialRequest, options) + .then((request) => request(this.axios, this.basePath)); + } +} + +/** + * DefaultApi - axios parameter creator + * @export + */ +export const DefaultApiAxiosParamCreator = function ( + configuration?: Configuration +) { + return { + /** + * + * @summary Get encrypted document by Id + * @param {string} documentId The encrypted documents object Id + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + storageDocumentsDocumentIdGet: async ( + documentId: string, + options: AxiosRequestConfig = {} + ): Promise => { + // verify required parameter 'documentId' is not null or undefined + assertParamExists( + 'storageDocumentsDocumentIdGet', + 'documentId', + documentId + ); + const localVarPath = `/storage/documents/{documentId}`.replace( + `{${'documentId'}}`, + encodeURIComponent(String(documentId)) + ); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { + method: 'GET', + ...baseOptions, + ...options, + }; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = + baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = { + ...localVarHeaderParameter, + ...headersFromBaseOptions, + ...options.headers, + }; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary encrypt and upload document + * @param {DocumentUploadRequest} [documentUploadRequest] Parameters uploading the document + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + storageDocumentsPost: async ( + documentUploadRequest?: DocumentUploadRequest, + options: AxiosRequestConfig = {} + ): Promise => { + const localVarPath = `/storage/documents`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { + method: 'POST', + ...baseOptions, + ...options, + }; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = + baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = { + ...localVarHeaderParameter, + ...headersFromBaseOptions, + ...options.headers, + }; + localVarRequestOptions.data = serializeDataIfNeeded( + documentUploadRequest, + localVarRequestOptions, + configuration + ); + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + }; +}; + +/** + * DefaultApi - functional programming interface + * @export + */ +export const DefaultApiFp = function (configuration?: Configuration) { + const localVarAxiosParamCreator = DefaultApiAxiosParamCreator(configuration); + return { + /** + * + * @summary Get encrypted document by Id + * @param {string} documentId The encrypted documents object Id + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async storageDocumentsDocumentIdGet( + documentId: string, + options?: AxiosRequestConfig + ): Promise< + ( + axios?: AxiosInstance, + basePath?: string + ) => AxiosPromise + > { + const localVarAxiosArgs = + await localVarAxiosParamCreator.storageDocumentsDocumentIdGet( + documentId, + options + ); + return createRequestFunction( + localVarAxiosArgs, + globalAxios, + BASE_PATH, + configuration + ); + }, + /** + * + * @summary encrypt and upload document + * @param {DocumentUploadRequest} [documentUploadRequest] Parameters uploading the document + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async storageDocumentsPost( + documentUploadRequest?: DocumentUploadRequest, + options?: AxiosRequestConfig + ): Promise< + ( + axios?: AxiosInstance, + basePath?: string + ) => AxiosPromise + > { + const localVarAxiosArgs = + await localVarAxiosParamCreator.storageDocumentsPost( + documentUploadRequest, + options + ); + return createRequestFunction( + localVarAxiosArgs, + globalAxios, + BASE_PATH, + configuration + ); + }, + }; +}; + +/** + * DefaultApi - factory interface + * @export + */ +export const DefaultApiFactory = function ( + configuration?: Configuration, + basePath?: string, + axios?: AxiosInstance +) { + const localVarFp = DefaultApiFp(configuration); + return { + /** + * + * @summary Get encrypted document by Id + * @param {string} documentId The encrypted documents object Id + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + storageDocumentsDocumentIdGet( + documentId: string, + options?: any + ): AxiosPromise { + return localVarFp + .storageDocumentsDocumentIdGet(documentId, options) + .then((request) => request(axios, basePath)); + }, + /** + * + * @summary encrypt and upload document + * @param {DocumentUploadRequest} [documentUploadRequest] Parameters uploading the document + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + storageDocumentsPost( + documentUploadRequest?: DocumentUploadRequest, + options?: any + ): AxiosPromise { + return localVarFp + .storageDocumentsPost(documentUploadRequest, options) + .then((request) => request(axios, basePath)); + }, + }; +}; + +/** + * DefaultApi - interface + * @export + * @interface DefaultApi + */ +export interface DefaultApiInterface { + /** + * + * @summary Get encrypted document by Id + * @param {string} documentId The encrypted documents object Id + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof DefaultApiInterface + */ + storageDocumentsDocumentIdGet( + documentId: string, + options?: AxiosRequestConfig + ): AxiosPromise; + + /** + * + * @summary encrypt and upload document + * @param {DocumentUploadRequest} [documentUploadRequest] Parameters uploading the document + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof DefaultApiInterface + */ + storageDocumentsPost( + documentUploadRequest?: DocumentUploadRequest, + options?: AxiosRequestConfig + ): AxiosPromise; +} + +/** + * DefaultApi - object-oriented interface + * @export + * @class DefaultApi + * @extends {BaseAPI} + */ +export class DefaultApi extends BaseAPI implements DefaultApiInterface { + /** + * + * @summary Get encrypted document by Id + * @param {string} documentId The encrypted documents object Id + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof DefaultApi + */ + public storageDocumentsDocumentIdGet( + documentId: string, + options?: AxiosRequestConfig + ) { + return DefaultApiFp(this.configuration) + .storageDocumentsDocumentIdGet(documentId, options) + .then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary encrypt and upload document + * @param {DocumentUploadRequest} [documentUploadRequest] Parameters uploading the document + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof DefaultApi + */ + public storageDocumentsPost( + documentUploadRequest?: DocumentUploadRequest, + options?: AxiosRequestConfig + ) { + return DefaultApiFp(this.configuration) + .storageDocumentsPost(documentUploadRequest, options) + .then((request) => request(this.axios, this.basePath)); + } +} diff --git a/libs/api-client/src/base.ts b/libs/api-client/src/base.ts new file mode 100644 index 00000000..7799f211 --- /dev/null +++ b/libs/api-client/src/base.ts @@ -0,0 +1,74 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * DVP + * API for the Digital Verification Platform + * + * The version of the OpenAPI document: 0.1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { Configuration } from './configuration'; +// Some imports not used depending on template conditions +// @ts-ignore +import globalAxios, { AxiosInstance, AxiosRequestConfig } from 'axios'; + +export const BASE_PATH = 'http://localhost:3333/api'.replace(/\/+$/, ''); + +/** + * + * @export + */ +export const COLLECTION_FORMATS = { + csv: ',', + ssv: ' ', + tsv: '\t', + pipes: '|', +}; + +/** + * + * @export + * @interface RequestArgs + */ +export interface RequestArgs { + url: string; + options: AxiosRequestConfig; +} + +/** + * + * @export + * @class BaseAPI + */ +export class BaseAPI { + protected configuration: Configuration | undefined; + + constructor( + configuration?: Configuration, + protected basePath: string = BASE_PATH, + protected axios: AxiosInstance = globalAxios + ) { + if (configuration) { + this.configuration = configuration; + this.basePath = configuration.basePath || this.basePath; + } + } +} + +/** + * + * @export + * @class RequiredError + * @extends {Error} + */ +export class RequiredError extends Error { + override name: 'RequiredError' = 'RequiredError'; + constructor(public field: string, msg?: string) { + super(msg); + } +} diff --git a/libs/api-client/src/common.ts b/libs/api-client/src/common.ts new file mode 100644 index 00000000..8c5d4139 --- /dev/null +++ b/libs/api-client/src/common.ts @@ -0,0 +1,148 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * DVP + * API for the Digital Verification Platform + * + * The version of the OpenAPI document: 0.1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +import { Configuration } from "./configuration"; +import { RequiredError, RequestArgs } from "./base"; +import { AxiosInstance, AxiosResponse } from 'axios'; + +/** + * + * @export + */ +export const DUMMY_BASE_URL = 'https://example.com' + +/** + * + * @throws {RequiredError} + * @export + */ +export const assertParamExists = function (functionName: string, paramName: string, paramValue: unknown) { + if (paramValue === null || paramValue === undefined) { + throw new RequiredError(paramName, `Required parameter ${paramName} was null or undefined when calling ${functionName}.`); + } +} + +/** + * + * @export + */ +export const setApiKeyToObject = async function (object: any, keyParamName: string, configuration?: Configuration) { + if (configuration && configuration.apiKey) { + const localVarApiKeyValue = typeof configuration.apiKey === 'function' + ? await configuration.apiKey(keyParamName) + : await configuration.apiKey; + object[keyParamName] = localVarApiKeyValue; + } +} + +/** + * + * @export + */ +export const setBasicAuthToObject = function (object: any, configuration?: Configuration) { + if (configuration && (configuration.username || configuration.password)) { + object["auth"] = { username: configuration.username, password: configuration.password }; + } +} + +/** + * + * @export + */ +export const setBearerAuthToObject = async function (object: any, configuration?: Configuration) { + if (configuration && configuration.accessToken) { + const accessToken = typeof configuration.accessToken === 'function' + ? await configuration.accessToken() + : await configuration.accessToken; + object["Authorization"] = "Bearer " + accessToken; + } +} + +/** + * + * @export + */ +export const setOAuthToObject = async function (object: any, name: string, scopes: string[], configuration?: Configuration) { + if (configuration && configuration.accessToken) { + const localVarAccessTokenValue = typeof configuration.accessToken === 'function' + ? await configuration.accessToken(name, scopes) + : await configuration.accessToken; + object["Authorization"] = "Bearer " + localVarAccessTokenValue; + } +} + +function setFlattenedQueryParams(urlSearchParams: URLSearchParams, parameter: any, key: string = ""): void { + if (typeof parameter === "object") { + if (Array.isArray(parameter)) { + (parameter as any[]).forEach(item => setFlattenedQueryParams(urlSearchParams, item, key)); + } + else { + Object.keys(parameter).forEach(currentKey => + setFlattenedQueryParams(urlSearchParams, parameter[currentKey], `${key}${key !== '' ? '.' : ''}${currentKey}`) + ); + } + } + else { + if (urlSearchParams.has(key)) { + urlSearchParams.append(key, parameter); + } + else { + urlSearchParams.set(key, parameter); + } + } +} + +/** + * + * @export + */ +export const setSearchParams = function (url: URL, ...objects: any[]) { + const searchParams = new URLSearchParams(url.search); + setFlattenedQueryParams(searchParams, objects); + url.search = searchParams.toString(); +} + +/** + * + * @export + */ +export const serializeDataIfNeeded = function (value: any, requestOptions: any, configuration?: Configuration) { + const nonString = typeof value !== 'string'; + const needsSerialization = nonString && configuration && configuration.isJsonMime + ? configuration.isJsonMime(requestOptions.headers['Content-Type']) + : nonString; + return needsSerialization + ? JSON.stringify(value !== undefined ? value : {}) + : (value || ""); +} + +/** + * + * @export + */ +export const toPathString = function (url: URL) { + return url.pathname + url.search + url.hash +} + +/** + * + * @export + */ +export const createRequestFunction = function (axiosArgs: RequestArgs, globalAxios: AxiosInstance, BASE_PATH: string, configuration?: Configuration) { + return >(axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => { + const axiosRequestArgs = {...axiosArgs.options, url: (configuration?.basePath || basePath) + axiosArgs.url}; + return axios.request(axiosRequestArgs); + }; +} diff --git a/libs/api-client/src/configuration.ts b/libs/api-client/src/configuration.ts new file mode 100644 index 00000000..704a8e46 --- /dev/null +++ b/libs/api-client/src/configuration.ts @@ -0,0 +1,101 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * DVP + * API for the Digital Verification Platform + * + * The version of the OpenAPI document: 0.1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +export interface ConfigurationParameters { + apiKey?: string | Promise | ((name: string) => string) | ((name: string) => Promise); + username?: string; + password?: string; + accessToken?: string | Promise | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise); + basePath?: string; + baseOptions?: any; + formDataCtor?: new () => any; +} + +export class Configuration { + /** + * parameter for apiKey security + * @param name security name + * @memberof Configuration + */ + apiKey?: string | Promise | ((name: string) => string) | ((name: string) => Promise); + /** + * parameter for basic security + * + * @type {string} + * @memberof Configuration + */ + username?: string; + /** + * parameter for basic security + * + * @type {string} + * @memberof Configuration + */ + password?: string; + /** + * parameter for oauth2 security + * @param name security name + * @param scopes oauth2 scope + * @memberof Configuration + */ + accessToken?: string | Promise | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise); + /** + * override base path + * + * @type {string} + * @memberof Configuration + */ + basePath?: string; + /** + * base options for axios calls + * + * @type {any} + * @memberof Configuration + */ + baseOptions?: any; + /** + * The FormData constructor that will be used to create multipart form data + * requests. You can inject this here so that execution environments that + * do not support the FormData class can still run the generated client. + * + * @type {new () => FormData} + */ + formDataCtor?: new () => any; + + constructor(param: ConfigurationParameters = {}) { + this.apiKey = param.apiKey; + this.username = param.username; + this.password = param.password; + this.accessToken = param.accessToken; + this.basePath = param.basePath; + this.baseOptions = param.baseOptions; + this.formDataCtor = param.formDataCtor; + } + + /** + * Check if the given MIME is a JSON MIME. + * JSON MIME examples: + * application/json + * application/json; charset=UTF8 + * APPLICATION/JSON + * application/vnd.company+json + * @param mime - MIME (Multipurpose Internet Mail Extensions) + * @return True if the given MIME is JSON, false otherwise. + */ + public isJsonMime(mime: string): boolean { + const jsonMime: RegExp = new RegExp('^(application\/json|[^;/ \t]+\/[^;/ \t]+[+]json)[ \t]*(;.*)?$', 'i'); + return mime !== null && (jsonMime.test(mime) || mime.toLowerCase() === 'application/json-patch+json'); + } +} diff --git a/libs/api-client/src/git_push.sh b/libs/api-client/src/git_push.sh new file mode 100644 index 00000000..f53a75d4 --- /dev/null +++ b/libs/api-client/src/git_push.sh @@ -0,0 +1,57 @@ +#!/bin/sh +# ref: https://help.github.com/articles/adding-an-existing-project-to-github-using-the-command-line/ +# +# Usage example: /bin/sh ./git_push.sh wing328 openapi-petstore-perl "minor update" "gitlab.com" + +git_user_id=$1 +git_repo_id=$2 +release_note=$3 +git_host=$4 + +if [ "$git_host" = "" ]; then + git_host="github.com" + echo "[INFO] No command line input provided. Set \$git_host to $git_host" +fi + +if [ "$git_user_id" = "" ]; then + git_user_id="GIT_USER_ID" + echo "[INFO] No command line input provided. Set \$git_user_id to $git_user_id" +fi + +if [ "$git_repo_id" = "" ]; then + git_repo_id="GIT_REPO_ID" + echo "[INFO] No command line input provided. Set \$git_repo_id to $git_repo_id" +fi + +if [ "$release_note" = "" ]; then + release_note="Minor update" + echo "[INFO] No command line input provided. Set \$release_note to $release_note" +fi + +# Initialize the local directory as a Git repository +git init + +# Adds the files in the local repository and stages them for commit. +git add . + +# Commits the tracked changes and prepares them to be pushed to a remote repository. +git commit -m "$release_note" + +# Sets the new remote +git_remote=$(git remote) +if [ "$git_remote" = "" ]; then # git remote not defined + + if [ "$GIT_TOKEN" = "" ]; then + echo "[INFO] \$GIT_TOKEN (environment variable) is not set. Using the git credential in your environment." + git remote add origin https://${git_host}/${git_user_id}/${git_repo_id}.git + else + git remote add origin https://${git_user_id}:"${GIT_TOKEN}"@${git_host}/${git_user_id}/${git_repo_id}.git + fi + +fi + +git pull origin master + +# Pushes (Forces) the changes in the local repository up to the remote repository +echo "Git pushing to https://${git_host}/${git_user_id}/${git_repo_id}.git" +git push origin master 2>&1 | grep -v 'To https' diff --git a/libs/api-client/src/index.ts b/libs/api-client/src/index.ts new file mode 100644 index 00000000..e580e028 --- /dev/null +++ b/libs/api-client/src/index.ts @@ -0,0 +1,18 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * DVP + * API for the Digital Verification Platform + * + * The version of the OpenAPI document: 0.1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +export * from "./api"; +export * from "./configuration"; + diff --git a/libs/api-client/tsconfig.json b/libs/api-client/tsconfig.json new file mode 100644 index 00000000..f5b85657 --- /dev/null +++ b/libs/api-client/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/libs/api-client/tsconfig.lib.json b/libs/api-client/tsconfig.lib.json new file mode 100644 index 00000000..2a221f1e --- /dev/null +++ b/libs/api-client/tsconfig.lib.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "declaration": true, + "types": ["node"], + "composite": true + }, + "include": ["**/*.ts"], + "exclude": ["jest.config.ts", "**/*.spec.ts", "**/*.test.ts"] +} diff --git a/libs/api-client/tsconfig.spec.json b/libs/api-client/tsconfig.spec.json new file mode 100644 index 00000000..546f1287 --- /dev/null +++ b/libs/api-client/tsconfig.spec.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": ["jest.config.ts", "**/*.test.ts", "**/*.spec.ts", "**/*.d.ts"] +} diff --git a/libs/api-interfaces/.babelrc b/libs/api-interfaces/.babelrc new file mode 100644 index 00000000..e24a5465 --- /dev/null +++ b/libs/api-interfaces/.babelrc @@ -0,0 +1,10 @@ +{ + "presets": [ + [ + "@nrwl/web/babel", + { + "useBuiltIns": "usage" + } + ] + ] +} diff --git a/libs/api-interfaces/README.md b/libs/api-interfaces/README.md new file mode 100644 index 00000000..b8849651 --- /dev/null +++ b/libs/api-interfaces/README.md @@ -0,0 +1,11 @@ +# api-interfaces + +This library was generated with [Nx](https://nx.dev). + +## Building + +Run `nx build api-interfaces` to build the library. + +## Running unit tests + +Run `nx test api-interfaces` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/api-interfaces/jest.config.ts b/libs/api-interfaces/jest.config.ts new file mode 100644 index 00000000..d7409137 --- /dev/null +++ b/libs/api-interfaces/jest.config.ts @@ -0,0 +1,15 @@ +/* eslint-disable */ +export default { + displayName: 'api-interfaces', + preset: '../../jest.preset.js', + globals: { + 'ts-jest': { + tsconfig: '/tsconfig.spec.json', + }, + }, + transform: { + '^.+\\.[tj]s$': 'ts-jest', + }, + moduleFileExtensions: ['ts', 'js', 'html'], + coverageDirectory: '../../coverage/libs/api-interfaces', +}; diff --git a/libs/api-interfaces/package.json b/libs/api-interfaces/package.json new file mode 100644 index 00000000..235a921d --- /dev/null +++ b/libs/api-interfaces/package.json @@ -0,0 +1,5 @@ +{ + "name": "@dvp/api-interfaces", + "version": "0.0.1", + "type": "commonjs" +} diff --git a/libs/api-interfaces/project.json b/libs/api-interfaces/project.json new file mode 100644 index 00000000..74ac6b96 --- /dev/null +++ b/libs/api-interfaces/project.json @@ -0,0 +1,35 @@ +{ + "name": "api-interfaces", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/api-interfaces/src", + "projectType": "library", + "implicitDependencies": ["api-client"], + "targets": { + "build": { + "executor": "@nrwl/js:tsc", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/libs/api-interfaces", + "main": "libs/api-interfaces/src/index.ts", + "tsConfig": "libs/api-interfaces/tsconfig.lib.json", + "assets": ["libs/api-interfaces/*.md"] + } + }, + "lint": { + "executor": "@nrwl/linter:eslint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["libs/api-interfaces/**/*.ts"] + } + }, + "test": { + "executor": "@nrwl/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/api-interfaces/jest.config.ts", + "passWithNoTests": true + } + } + }, + "tags": [] +} diff --git a/libs/api-interfaces/src/common/index.ts b/libs/api-interfaces/src/common/index.ts new file mode 100644 index 00000000..148705f5 --- /dev/null +++ b/libs/api-interfaces/src/common/index.ts @@ -0,0 +1,4 @@ +export interface UserProfile { + sub?: string; + abn?: string; +} diff --git a/libs/api-interfaces/src/index.ts b/libs/api-interfaces/src/index.ts new file mode 100644 index 00000000..bb3b3a98 --- /dev/null +++ b/libs/api-interfaces/src/index.ts @@ -0,0 +1,4 @@ +export * from './storage'; +export * from './vc'; +export * from './validate'; +export * from './common'; diff --git a/libs/api-interfaces/src/storage/index.ts b/libs/api-interfaces/src/storage/index.ts new file mode 100644 index 00000000..85674ee7 --- /dev/null +++ b/libs/api-interfaces/src/storage/index.ts @@ -0,0 +1 @@ +export * from './storage'; diff --git a/libs/api-interfaces/src/storage/storage.ts b/libs/api-interfaces/src/storage/storage.ts new file mode 100644 index 00000000..92b6d87f --- /dev/null +++ b/libs/api-interfaces/src/storage/storage.ts @@ -0,0 +1,43 @@ +import { S3ClientConfig } from '@aws-sdk/client-s3'; + +export interface S3Config { + bucketName: string; + clientConfig: S3ClientConfig; +} + +export interface StorageClient { + getDocumentStorePath(): string; + getDocument( + documentId: string + ): Promise<{ document: EncryptedDocument } | null>; + isDocumentExists(documentId: string): Promise; + uploadDocument(document: string, documentId: string): Promise; + deleteDocument(documentId: string): Promise; +} + +export interface EncryptedDocument { + cipherText: string; + iv: string; + tag: string; + type: string; +} + +export interface ErrorObject { + id?: string; + code: string; + detail: string; + source?: { + pointer?: string; + location?: 'REQUEST' | 'QUERY' | 'ID'; + parameter?: string; + }; + helpUrl?: string; + helpText?: string; +} + +export type QRPayload = { + payload: { + uri: string; + key: string; + }; +}; diff --git a/libs/api-interfaces/src/validate/index.ts b/libs/api-interfaces/src/validate/index.ts new file mode 100644 index 00000000..9431d1c5 --- /dev/null +++ b/libs/api-interfaces/src/validate/index.ts @@ -0,0 +1 @@ +export * from "./validate"; diff --git a/libs/api-interfaces/src/validate/validate.ts b/libs/api-interfaces/src/validate/validate.ts new file mode 100644 index 00000000..f3c08e8f --- /dev/null +++ b/libs/api-interfaces/src/validate/validate.ts @@ -0,0 +1,4 @@ +export enum CredentialSchemaType { + AANZFTA_COO = 'AANZFTA_COO', + GENERIC = 'GENERIC', +} diff --git a/libs/api-interfaces/src/vc/index.ts b/libs/api-interfaces/src/vc/index.ts new file mode 100644 index 00000000..1715e105 --- /dev/null +++ b/libs/api-interfaces/src/vc/index.ts @@ -0,0 +1,4 @@ +export * from './issue'; +export * from './keys'; +export * from './vc'; +export * from './verify'; diff --git a/libs/api-interfaces/src/vc/issue.ts b/libs/api-interfaces/src/vc/issue.ts new file mode 100644 index 00000000..0fc3bdad --- /dev/null +++ b/libs/api-interfaces/src/vc/issue.ts @@ -0,0 +1,13 @@ +import { OAVerifiableCredential, VerifiableCredential } from './vc'; + +export type IssuerFunction = ( + credential: VerifiableCredential | OAVerifiableCredential, + verificationMethod: string, + signingKey: string +) => Promise; + +export interface IssuedDocument { + verifiableCredential: VerifiableCredential; + documentId: string; + encryptionKey: string; +} diff --git a/libs/api-interfaces/src/vc/keys.ts b/libs/api-interfaces/src/vc/keys.ts new file mode 100644 index 00000000..fbef8252 --- /dev/null +++ b/libs/api-interfaces/src/vc/keys.ts @@ -0,0 +1,42 @@ +// borrowed from @transmute/did-key.js/dist/index.d.ts + +export interface PublicKeyJwk { + kty: 'EC' | 'OKP'; + crv: 'Ed25519' | 'X25519' | 'P-256' | 'P-384' | 'secp256k1'; + x: string; + y?: string; +} + +export interface PrivateKeyJwk extends PublicKeyJwk { + d: string; +} + +export interface KeyCommonProps { + id: string; + type: string; + controller: string; +} + +export interface JwkPairCommonProps { + publicKeyJwk: any; + privateKeyJwk: any; +} + +export interface LdPairCommonProps { + publicKeyBase58: string; + privateKeyBase58: string; +} + +export interface JwkKeyPair extends KeyCommonProps, JwkPairCommonProps {} +export interface LdKeyPair extends KeyCommonProps, LdPairCommonProps {} +export type DidKey = JwkKeyPair | LdKeyPair; + +export interface DidDocument { + id: string; + verificationMethod: any[]; +} + +export interface DidGeneration { + didDocument: DidDocument; + keys: DidKey[]; +} diff --git a/libs/api-interfaces/src/vc/vc.ts b/libs/api-interfaces/src/vc/vc.ts new file mode 100644 index 00000000..68270b8d --- /dev/null +++ b/libs/api-interfaces/src/vc/vc.ts @@ -0,0 +1,52 @@ +import { + CredentialStatus, + OpenAttestationDocument, + WrappedDocument, + OpenAttestationMetadata, +} from '@govtechsg/open-attestation/dist/types/3.0/types'; + +import { VerifiableCredential as ApiVerifiableCredential } from '@dvp/api-client'; + +export interface Message { + message: string; +} + +export interface DocumentMetadata { + documentNumber?: string; + freeTradeAgreement?: string; + importingJurisdiction?: string; + exporterOrManufacturerAbn?: string; + importerName?: string; + consignmentReferenceNumber?: string; + documentDeclaration?: boolean; +} + +export interface CredentialSubject { + [string: string]: any; + links?: { + self?: { + href: string; + }; + }; +} + +export interface Issuer { + id: string; + name: string; + type: string; +} + +//TODO: extend as needed +export interface VerifiableCredential extends ApiVerifiableCredential { + credentialSubject: CredentialSubject; + credentialStatus?: CredentialStatus; + openAttestationMetadata?: OpenAttestationMetadata; +} + +export interface OAVerifiableCredential + extends Omit { + credentialSubject: CredentialSubject; +} + +export type WrappedVerifiableCredential = + WrappedDocument; diff --git a/libs/api-interfaces/src/vc/verify.ts b/libs/api-interfaces/src/vc/verify.ts new file mode 100644 index 00000000..4e7723e4 --- /dev/null +++ b/libs/api-interfaces/src/vc/verify.ts @@ -0,0 +1,11 @@ +import { VerifiableCredential } from './vc'; + +export interface VerificationResult { + checks: string[]; + warnings?: string[]; + errors: string[]; +} + +export type VerifierFunction = ( + vc: VerifiableCredential +) => Promise; diff --git a/libs/api-interfaces/tsconfig.json b/libs/api-interfaces/tsconfig.json new file mode 100644 index 00000000..f5b85657 --- /dev/null +++ b/libs/api-interfaces/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/libs/api-interfaces/tsconfig.lib.json b/libs/api-interfaces/tsconfig.lib.json new file mode 100644 index 00000000..7bfc80f7 --- /dev/null +++ b/libs/api-interfaces/tsconfig.lib.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "include": ["**/*.ts"], + "exclude": ["jest.config.ts", "**/*.spec.ts", "**/*.test.ts"] +} diff --git a/libs/api-interfaces/tsconfig.spec.json b/libs/api-interfaces/tsconfig.spec.json new file mode 100644 index 00000000..546f1287 --- /dev/null +++ b/libs/api-interfaces/tsconfig.spec.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": ["jest.config.ts", "**/*.test.ts", "**/*.spec.ts", "**/*.d.ts"] +} diff --git a/libs/server-common/.babelrc b/libs/server-common/.babelrc new file mode 100644 index 00000000..e24a5465 --- /dev/null +++ b/libs/server-common/.babelrc @@ -0,0 +1,10 @@ +{ + "presets": [ + [ + "@nrwl/web/babel", + { + "useBuiltIns": "usage" + } + ] + ] +} diff --git a/libs/server-common/README.md b/libs/server-common/README.md new file mode 100644 index 00000000..69a5bde7 --- /dev/null +++ b/libs/server-common/README.md @@ -0,0 +1,11 @@ +# server-common + +This library was generated with [Nx](https://nx.dev). + +## Building + +Run `nx build server-common` to build the library. + +## Running unit tests + +Run `nx test server-common` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/server-common/jest.config.ts b/libs/server-common/jest.config.ts new file mode 100644 index 00000000..043fc4be --- /dev/null +++ b/libs/server-common/jest.config.ts @@ -0,0 +1,23 @@ +/* eslint-disable */ +export default { + displayName: 'server-common', + preset: '../../jest.preset.js', + globals: { + 'ts-jest': { + tsconfig: '/tsconfig.spec.json', + }, + }, + testEnvironment: 'node', + transform: { + '^.+\\.[tj]s$': 'ts-jest', + }, + setupFilesAfterEnv: ['../../jest.proxy.setup.ts'], + moduleFileExtensions: ['ts', 'js', 'html'], + coverageDirectory: '../../coverage/libs/server-common', + testMatch: ['**/**/*.spec.{ts, js}', '**/**/*.test.{ts, js}'], + coverageThreshold: { + global: { + lines: 80, + }, + }, +}; diff --git a/libs/server-common/package.json b/libs/server-common/package.json new file mode 100644 index 00000000..087b84f3 --- /dev/null +++ b/libs/server-common/package.json @@ -0,0 +1,5 @@ +{ + "name": "@dvp/server-common", + "version": "0.0.1", + "type": "commonjs" +} diff --git a/libs/server-common/project.json b/libs/server-common/project.json new file mode 100644 index 00000000..476629e6 --- /dev/null +++ b/libs/server-common/project.json @@ -0,0 +1,35 @@ +{ + "name": "server-common", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/server-common/src", + "projectType": "library", + "implicitDependencies": ["api-client"], + "targets": { + "build": { + "executor": "@nrwl/js:tsc", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/libs/server-common", + "main": "libs/server-common/src/index.ts", + "tsConfig": "libs/server-common/tsconfig.lib.json", + "assets": ["libs/server-common/*.md"] + } + }, + "lint": { + "executor": "@nrwl/linter:eslint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["libs/server-common/**/*.ts"] + } + }, + "test": { + "executor": "@nrwl/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/server-common/jest.config.ts", + "passWithNoTests": true + } + } + }, + "tags": [] +} diff --git a/libs/server-common/src/context/index.ts b/libs/server-common/src/context/index.ts new file mode 100644 index 00000000..d41c6db9 --- /dev/null +++ b/libs/server-common/src/context/index.ts @@ -0,0 +1 @@ +export * from './request.invocation.context'; diff --git a/libs/server-common/src/context/request.invocation.context.test.ts b/libs/server-common/src/context/request.invocation.context.test.ts new file mode 100644 index 00000000..80ab2373 --- /dev/null +++ b/libs/server-common/src/context/request.invocation.context.test.ts @@ -0,0 +1,118 @@ +import { getMockReq } from '@jest-mock/express'; +import { RequestInvocationContext } from './request.invocation.context'; + +describe('Request invocation context implementation', (): void => { + it('should extract the correlation ID from the request', (): void => { + const mockRequest = getMockReq({ + method: 'GET', + headers: { + 'Correlation-ID': 'NUMPTYHEAD1', + }, + }); + + mockRequest.route = { path: '/log' }; + const invocationContext = new RequestInvocationContext(mockRequest); + expect(invocationContext.correlationId).toBeDefined(); + expect(invocationContext.correlationId).toBe('NUMPTYHEAD1'); + }); + + it('should create one if the correlation ID cannot be extracted from the request', (): void => { + const mockRequest = getMockReq({ + method: 'GET', + headers: {}, + }); + + mockRequest.route = { path: '/log' }; + const invocationContext = new RequestInvocationContext(mockRequest); + expect(invocationContext.correlationId).not.toBeUndefined(); + }); + + it('should extract the remote IP address correctly', (): void => { + const mockRequest = getMockReq({ + method: 'POST', + headers: { + 'X-Forwarded-For': '10.10.10.1,20.20.20.1,30.30.30.1', + }, + body: { + key1: 'value1', + key2: 'value2', + }, + socket: { + remoteAddress: '5.5.5.109', + }, + }); + + mockRequest.route = { path: '/log' }; + const invocationContext1 = new RequestInvocationContext(mockRequest); + expect(invocationContext1.ipAddress).toBe('10.10.10.1'); + + mockRequest.headers['X-Forwarded-For'] = '8.8.8.23'; + + const invocationContext2 = new RequestInvocationContext(mockRequest); + expect(invocationContext2.ipAddress).toBe('8.8.8.23'); + + mockRequest.headers['X-Forwarded-For'] = undefined; + + const invocationContext3 = new RequestInvocationContext(mockRequest); + expect(invocationContext3.ipAddress).toBe('5.5.5.109'); + }); + + it('should extract the correlation ID correctly', (): void => { + const mockRequest = getMockReq({ + method: 'PUT', + headers: { + 'Correlation-ID': 'abc123', + }, + body: { + key1: 'value1', + key2: 55, + }, + }); + mockRequest.route = { path: '/log' }; + const invocationContext1 = new RequestInvocationContext(mockRequest); + expect(invocationContext1.correlationId).toBe('abc123'); + + mockRequest.headers['Correlation-ID'] = undefined; + const invocationContext2 = new RequestInvocationContext(mockRequest); + expect(invocationContext2.correlationId).toBeDefined(); + }); + + it('should extract the userId from the access token and add the userId and default userAbn to the invocation context', (): void => { + const mockRequest = getMockReq({ + method: 'GET', + header: (() => + 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U') as any, + }); + + const invocationContext = new RequestInvocationContext(mockRequest); + + expect(invocationContext.userId).toBe('1234567890'); + expect(invocationContext.userAbn).toBe('41161080146'); + }); + + it('should extract the userId and userAbn from the access token and add them to the invocation context', (): void => { + const mockRequest = getMockReq({ + method: 'GET', + header: (() => + 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiYWJuIjoiMDAwMDAwMDAwIn0.TyNWetCHZ35Vy6Y-ERQNLo1_Wx1LBNeDDqYbz2bYvZU') as any, + }); + + const invocationContext = new RequestInvocationContext(mockRequest); + + expect(invocationContext.userId).toBe('1234567890'); + expect(invocationContext.userAbn).toBe('000000000'); + }); + + it('should assign null to the userId and userAbn if it was unable to extract the sub and abn properties from the access token', (): void => { + const mockRequest = getMockReq({ + method: 'GET', + header: (() => + 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.Et9HFtf9R3GEMA0IICOfFMVXY7kkTX1wr4qCyhIf58U') as any, + }); + + const invocationContext = new RequestInvocationContext(mockRequest); + + expect(invocationContext.userId).toBe(null); + expect(invocationContext.userAbn).toBe(null); + }); +}); diff --git a/libs/server-common/src/context/request.invocation.context.ts b/libs/server-common/src/context/request.invocation.context.ts new file mode 100644 index 00000000..97af2376 --- /dev/null +++ b/libs/server-common/src/context/request.invocation.context.ts @@ -0,0 +1,161 @@ +import { Request } from 'express'; +import { generate } from 'shortid'; +import { decode } from 'jsonwebtoken'; +import { UserProfile } from '@dvp/api-interfaces'; + +/** + * An interface for a context that can be used to provide contextual information about the current + * runtime environment. + * + * The [[InvocationContext]] is closely associated to the [[Logger]], and as such it exposes all fields + * that the logger supports. In addition, it provides the ability to store custom metadata in the + * [[InvocationContext]]. Custom metadata will be formatted into a dedicated section of each log message + * that is reserved for this content. + * + * The [[InvocationContext]] provides the ability to include contextual information when sending output + * to the [[Logger]], where this contextual information is not easily able to be gleaned at the time + * the [[Logger]] is invoked. + * + * For example, if the [[Logger]] is invoked inside a utility module, it may be difficult to provide + * information about the current user (since this information would normally not be provided when + * invoking the utility functionality). The [[InvocationContext]] provides a means to encapsulate + * contextual information that is important for logging, and to pass it between application layers. + * + * Note that the [[InvocationContext]] may also be used by applications during processing, to perform + * other application-specific processing that requires information about the current runtime context. + */ +export interface InvocationContext { + /** The OpenID Connect access token for this context. This is not directly used for logging, + * but may be used for chaining invocations through multiple services. */ + accessToken?: string; + + /** The correlation ID for log messages that use this context. */ + correlationId?: string; + + /** The transaction ID for this context. This is not directly used for logging, but may be used + * for chaining invocations through multiple services. */ + transactionId?: string; + + /** The request ID for this context. This is not directly used for logging, but may be used for + * chaining invocations through multiple services. */ + requestId?: string; + + /** The users ID for this context. This is not directly used for logging, but may be used + * for chaining invocations through multiple services. */ + userId: string | null; + + /** The ABN for this context. This is not directly used for logging, but may be used + * for chaining invocations through multiple services. */ + userAbn: string | null; + + /** The IP address for log messages that use this context. */ + ipAddress?: string; + + /** The operation name for log messages that use this context. */ + operationId: string; + + /** The operation outcome for log messages that use this context. */ + operationOutcome?: string; + + /** A dictionary of key / value pairs, for additional metadata for log messages that use this + * context. */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + metadata?: { [key: string]: any }; +} + +export class RequestInvocationContext implements InvocationContext { + public readonly accessToken?: string; + + /** The ID of the user making the request. */ + public readonly userId: string | null; + + /** The ABN associated with the user making the request. */ + public readonly userAbn: string | null; + + /** The correlation ID. */ + public readonly correlationId?: string; + + /** The IP address. */ + public readonly ipAddress: string; + + /** A dictionary of key / value pairs for additional metadata. */ + public readonly metadata?: { [key: string]: string }; + + /** The operation name. */ + public readonly operationId: string; + + /** The request id name. */ + public readonly requestId: string | undefined; + + /** The operation outcome. */ + public readonly operationOutcome?: string; + + /** The transaction id for log messages wrapped in the same transaction that use this context */ + public transactionId?: string; + + /** + * Constructs a new RequestInvocationContext. + * + * @param request The HTTP request from which to populate the data for the invocation context. + */ + public constructor(request: Request, generateTransactionId?: boolean) { + const authHeader = request.header('Authorization'); + + this.accessToken = authHeader?.split(' ')?.[1]; + + this.userId = null; + this.userAbn = null; + + this.correlationId = + (request.headers['Correlation-ID'] as string) || generate(); + this.transactionId = request.headers['Transaction-ID'] as string; + + this.ipAddress = this.extractRemoteIpAddress(request); + this.operationId = + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (request as any)?.openapi?.schema?.operationId || + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (request as any)['operationId']; + + if (generateTransactionId && !this.transactionId) { + this.transactionId = generate(); + } + + this.requestId = request.header('Request-ID'); + + try { + if (this.accessToken) { + const accessTokenPayload = { + abn: '41161080146', // TODO: Remove hard coded abn. + ...(decode(this.accessToken) as UserProfile), + }; + + const { sub, abn } = accessTokenPayload; + + if (sub && abn) { + this.userId = sub; + this.userAbn = abn; + } + } + } catch { + return; + } + } + + /** + * Extracts the remote IP address from the HTTP request and populates it into this object. + * + * @param request The HTTP request from which to extract the remote IP address. + */ + private extractRemoteIpAddress(request: Request): string { + const forwarderChain = request.headers['X-Forwarded-For'] as string; + if (forwarderChain) { + const commaPos = forwarderChain.indexOf(','); + return commaPos > -1 + ? forwarderChain.substring(0, commaPos) + : forwarderChain; + } + + return request?.socket?.remoteAddress || ''; + } +} diff --git a/libs/server-common/src/db/db.ts b/libs/server-common/src/db/db.ts new file mode 100644 index 00000000..7239137b --- /dev/null +++ b/libs/server-common/src/db/db.ts @@ -0,0 +1,26 @@ +import { Table } from 'dynamodb-onetable'; +import { Dynamo } from 'dynamodb-onetable/Dynamo'; +import { DynamoSchema } from './schema'; + +/* + Single-table schema and setup. +*/ +export const initializeDynamoDataTable = ( + dynamoClient: Dynamo, + tableName: string +) => { + const table = new Table({ + name: tableName, + client: dynamoClient, + logger: true, + partial: false, + + schema: DynamoSchema, + }); + + return { + Document: table.getModel('Document'), + RevocationCounter: table.getModel('RevocationCounter'), + DocumentSchema: table.getModel('DocumentSchema'), + }; +}; diff --git a/libs/server-common/src/db/index.ts b/libs/server-common/src/db/index.ts new file mode 100644 index 00000000..638bf8a7 --- /dev/null +++ b/libs/server-common/src/db/index.ts @@ -0,0 +1,2 @@ +export * from './db'; +export * from './schema'; diff --git a/libs/server-common/src/db/schema.ts b/libs/server-common/src/db/schema.ts new file mode 100644 index 00000000..7d67b3fc --- /dev/null +++ b/libs/server-common/src/db/schema.ts @@ -0,0 +1,60 @@ +import { Entity } from 'dynamodb-onetable'; + +export const DynamoSchema = { + version: '0.0.1', + indexes: { + primary: { hash: 'pk', sort: 'sk' }, + gs1: { + hash: 'gs1pk', + sort: 'gs1sk', + }, + }, + models: { + Document: { + pk: { type: String, value: 'Abn#${abn}' }, + sk: { type: String, value: 'Document#${id}' }, + id: { type: String, required: true }, + createdBy: { type: String, required: true }, + abn: { type: String, required: true }, + s3Path: { type: String, required: true }, + decryptionKey: { type: String, required: true }, + + gs1pk: { type: String, value: 'Document' }, + gs1sk: { type: String, value: 'Document#${id}' }, + + documentNumber: { type: String }, + freeTradeAgreement: { type: String }, + importingJurisdiction: { type: String }, + exporterOrManufacturerAbn: { type: String }, + importerName: { type: String }, + consignmentReferenceNumber: { type: String }, + documentDeclaration: { type: Boolean }, + }, + RevocationCounter: { + pk: { type: String, value: 'RevocationCounter' }, + sk: { type: String, value: 'RevocationCounter' }, + path: { type: String, required: true }, + counter: { type: Number, required: true }, + }, + DocumentSchema: { + pk: { type: String, value: 'DocumentSchema' }, + sk: { type: String, value: 'DocumentSchema#${name}#${type}' }, + name: { type: String, required: true }, + type: { type: String, required: true, enum: ['full', 'partial'] }, + schemaPath: { type: String }, + uiSchemaPath: { type: String }, + }, + } as const, + params: { + isoDates: true, + timestamps: true, + }, +}; + +export type DocumentType = Entity; +export type RevocationType = Entity< + typeof DynamoSchema.models.RevocationCounter +>; +export type DocumentSchemaType = Entity< + typeof DynamoSchema.models.DocumentSchema +>; diff --git a/libs/server-common/src/error/api/ajvSchemaValidationError.ts b/libs/server-common/src/error/api/ajvSchemaValidationError.ts new file mode 100644 index 00000000..9861af5d --- /dev/null +++ b/libs/server-common/src/error/api/ajvSchemaValidationError.ts @@ -0,0 +1,50 @@ +import { ApiErrors } from './apiErrors'; +import { ErrorObject } from 'ajv'; +import { ApiError } from './apiError'; + +class AjvSchemaValidationError extends ApiErrors { + /** HTTP Status code for error that occured */ + public httpStatusCode = 422; + + constructor(errors?: ErrorObject, unknown>[]) { + super(); + if (errors) { + this.addAJVErrors(errors); + } + } + + /** + * + * @param errors ajv validation errors array + */ + public addAJVErrors( + errors: ErrorObject, unknown>[] + ) { + errors.forEach((error) => { + const params: { missingProperty?: string; additionalProperty?: string } = + error.params; + + const required = + params?.missingProperty && + `${error.instancePath}/${params.missingProperty}`; + + const additionalProperty = + params?.additionalProperty && + `${error.instancePath}/${params?.additionalProperty}`; + + const path = + required ?? + additionalProperty ?? + error.instancePath ?? + error.schemaPath; + + this.addErrorDetail( + ApiError.VALIDATION_ERROR_ID, + `ValidationError`, + `${path}: ${error.message}` + ); + }); + } +} + +export { AjvSchemaValidationError }; diff --git a/libs/server-common/src/error/api/apiError.ts b/libs/server-common/src/error/api/apiError.ts new file mode 100644 index 00000000..65753811 --- /dev/null +++ b/libs/server-common/src/error/api/apiError.ts @@ -0,0 +1,277 @@ +import { ApiErrorSource } from './apiErrorSource'; + +/** + * An interface describing an API error. + * + * This interface has been designed to comply with the Home Affairs Enterprise + * Integration Service standards for API error messages. + * + * @see [Error Handling](https://confluence.bcz.gov.au/display/EI/Error+Handling) + */ +interface ApiErrorBase { + /** Identifier of the specific error. */ + id?: string; + + /** An application-specific error code. */ + code: string; + + /** A human-readable explanation specific to this occurrence of the problem. */ + detail: string; + + /** An object containing references to the source of the error. */ + source?: ApiErrorSource; + + /** A URL which leads to further details about the error (e.g. help page). */ + helpUrl?: string; + + /** Help text which can provide further assistance on the error. */ + helpText?: string; +} + +/** + * A standard set of [[ApiErrorBase]] objects. + */ +const STANDARD_API_ERRORS_LIST: ApiErrorBase[] = [ + { + id: 'DVPAPI-001', + code: 'SystemError', + detail: 'An internal system error has occurred.', + helpUrl: 'https://www.abf.gov.au/help-and-support/contact-us', + helpText: 'An internal system error has occurred. Please contact Support.', + }, + { + id: 'DVPAPI-002', + code: 'ValidationError', + detail: 'The value [{0}] of the [{1}] field is invalid.', + helpUrl: 'https://www.abf.gov.au/help-and-support/contact-us', + helpText: 'Please check the value of the field and re-submit.', + }, + { + id: 'DVPAPI-003', + code: 'SecurityError', + detail: 'You are not allowed to access [{0}].', + helpUrl: 'https://www.abf.gov.au/help-and-support/contact-us', + helpText: + 'The permissions given to your API client id do not allow you to access the resource. ' + + 'please check that your security credentials are correct, ' + + 'and that you are attempting to access the correct resource.', + }, + { + id: 'DVPAPI-004', + code: 'NotFound', + detail: 'Could not find [{0}].', + helpUrl: 'https://www.abf.gov.au/help-and-support/contact-us', + helpText: + 'The specified resource could not be found. ' + + 'Please check that your security credentials are correct, ' + + 'and that you are attempting to access the correct resource.', + }, + { + id: 'DVPAPI-005', + code: 'NotImplemented', + detail: + 'The [{0}] method has not yet been implemented for the [{1}] resource.', + helpUrl: 'https://www.abf.gov.au/help-and-support/contact-us', + helpText: + 'The specified method for the given resource has yet to be implemented. ' + + 'Please check that you are calling the correct resource with the correct method, ' + + 'or check with the support to determine when the given resource will be implemented.', + }, + { + id: 'DVPAPI-006', + code: 'WebServiceTimeout', + detail: 'A timeout occurred in the [{0}] web service after [{1}] seconds.', + helpUrl: 'https://www.abf.gov.au/help-and-support/contact-us', + helpText: + 'A timeout occurred while attempting to invoke a web service. This may be a temporary ' + + 'condition caused by underlying network conditions.', + }, +]; + +/** + * An object used to encapsulate an API error. + * + * This object has been designed to comply with the Home Affairs Enterprise + * Integration Service standards for API error messages. + * + * @see [Error Handling](https://confluence.bcz.gov.au/display/EI/Error+Handling) + */ +class ApiError implements ApiErrorBase { + /** + * The ApiError Id which represents 'System Errors'. + */ + public static readonly SYSTEM_ERROR_ID = 'DVPAPI-001'; + + /** + * The ApiError Id which represents 'Validation Errors'. + */ + public static readonly VALIDATION_ERROR_ID = 'DVPAPI-002'; + + /** + * The ApiError Id which represents 'Security Errors'. + */ + public static readonly SECURITY_ERROR_ID = 'DVPAPI-003'; + + /** + * The ApiError Id which represents 'Not Found'. + */ + public static readonly NOT_FOUND_ERROR_ID = 'DVPAPI-004'; + + /** + * The ApiError Id which represents 'Not Implemented'. + */ + public static readonly NOT_IMPLEMENTED_ID = 'DVPAPI-005'; + + /** + * The ApiError Id which represents 'Web Service Timeout'. + */ + public static readonly WEB_SERVICE_TIMEOUT_ID = 'DVPAPI-006'; + + /** + * The ApiError Id which represents 'Bad Request Error'. + */ + public static readonly BAD_REQUEST_ID = 'DVPAPI-007'; + + /** Identifier of the specific error. */ + public id?: string; + + /** An application-specific error code. */ + public code: string; + + /** A human-readable explanation specific to this occurrence of the problem. */ + public detail: string; + + /** An object containing references to the source of the error. */ + public source?: ApiErrorSource; + + /** A URL which leads to further details about the error (e.g. help page). */ + public helpUrl?: string; + + /** Help text which can provide further assistance on the error. */ + public helpText?: string; + + /** + * Creates an error, with an 'id', 'code', 'detail', 'source', 'helpUrl' and 'helpText'. + * + * @param {string} id Identifier of the specific error. + * @param {string} code An application specific error code. + * @param {string} detail A human-readable explanation specific to this occurrence of the problem. + * @param {ApiErrorSource} source An object containing references to the source of the error. + * @param {string} helpUrl A URL which leads to further details about the error (e.g. help page). + * @param {string} helpText Help text which can provide further assistance on the error. + */ + public constructor( + id: string | undefined, + code: string, + detail: string, + source?: ApiErrorSource, + helpUrl?: string, + helpText?: string + ) { + this.id = id; + this.code = code; + this.detail = detail; + this.source = source; + this.helpUrl = helpUrl; + this.helpText = helpText; + } + + /** + * Replace the placeholders ('{n}') within a _format_ string with + * parameters. + * + * i.e. + * ``` + * ApiError.format('{0} {1}!', ['Hello', 'World']) + * ``` + * would return: + * ``` + * 'Hello World!' + * ``` + * + * @param format The format string (containing position placeholders of the + * form '{n}' that are to be replaced with the values from the _args_ + * parameter.) + * @param args An array of strings that will be used to replace the '{n}' + * position placeholders within the _format_ string. + * + * @return The input _format_ string with the '{n}' position placeholders + * replaced by the values within the _args_ array. + */ + public static format(format: string, ...args: string[]): string { + let formatted: string = format; + + for (let i = 0; i < args.length; i++) { + formatted = formatted.replace(new RegExp(`\\{${i}\\}`, 'g'), args[i]); + } + + return formatted; + } + + /** + * Create a 'pre-defined' error from the [[STANDARD_API_ERRORS_LIST]] array. + * + * @param {string} id The identifier of the 'pre-defined' [[ApiError]]. The + * other properties within the returned [[ApiError]] will be 'looked-up' in + * the [[STANDARD_API_ERRORS_LIST]] array. + * @param {string[]} args The parameters passed to the + * [[ApiError.STANDARD_API_ERRORS_LIST]] `ApiError.detail` format string. + * These will replace the '{n}' position holders within the `detail` string. + */ + public static fromId(id: string, ...args: string[]): ApiError { + // Attempt to 'lookup' the error with the given 'id' in the 'api.errors.json' file. + const standardError = STANDARD_API_ERRORS_LIST.filter((error) => { + return error.id === id; + }); + + let apiError: ApiError; + + if (standardError.length) { + apiError = new ApiError( + id, + standardError[0].code, + ApiError.format(standardError[0].detail, ...args), + undefined, + standardError[0].helpUrl, + standardError[0].helpText + ); + } else { + apiError = new ApiError(id, 'Error', 'An error has occurred.'); + } + + return apiError; + } + + /** + * Creates a 'minimal' error, with just a 'code' and 'detail'. + * + * @param {string} code An application specific error code. + * @param {string} detail A human-readable explanation specific to this occurrence of the problem. + */ + public static fromCode(code: string, detail: string): ApiError { + return new ApiError(undefined, code, detail); + } + + /** + * Creates an error, with an 'id', 'code', 'detail', 'source', 'helpUrl' and 'helpText'. + * + * @param {string} id Identifier of the specific error. + * @param {string} code An application specific error code. + * @param {string} detail A human-readable explanation specific to this occurrence of the problem. + * @param {ApiErrorSource} source An object containing references to the source of the error. + * @param {string} helpUrl A URL which leads to further details about the error (e.g. help page). + * @param {string} helpText Help text which can provide further assistance on the error. + */ + public static fromDetail( + id: string, + code: string, + detail: string, + source?: ApiErrorSource, + helpUrl?: string, + helpText?: string + ): ApiError { + return new ApiError(id, code, detail, source, helpUrl, helpText); + } +} + +export { ApiError }; diff --git a/libs/server-common/src/error/api/apiErrorSource.ts b/libs/server-common/src/error/api/apiErrorSource.ts new file mode 100644 index 00000000..07d7fd14 --- /dev/null +++ b/libs/server-common/src/error/api/apiErrorSource.ts @@ -0,0 +1,67 @@ +/** + * An enumeration used to describe the location of the data in error within a + * [[ApiErrorSource]]. + * + * Note that these have been defined as string enums so that they can be printed out + * in a human readable format. + * + * @see {@link "https://www.typescriptlang.org/docs/handbook/enums.html#string-enums"} + */ +enum ApiErrorLocation { + + /** + * Indicates the message relates to a property within the request + * object. The pointer property should be populated in this case. + */ + REQUEST = 'REQUEST', + + /** + * Indicates the message relates to a query parameter. The parameter + * property should be populated in this case. + */ + QUERY = 'QUERY', + + /** + * Indicates the message relates to the identifier of the REST resource. The + * parameter property may optionally be populated in this case. + */ + ID = 'ID' +} + +/** + * An object containing references to the source of an API error within an + * [[ApiError]] object. + */ +class ApiErrorSource { + + /** A JSON Pointer [RFC6901] to the associated entity in the request document. */ + public pointer?: string; + + /** An (optional) string indicating the URI query parameter that caused the error. */ + public parameter?: string; + + /** + * An [[ApiErrorLocation]] describing the place within the incoming HTTP + * request { `REQUEST`, `QUERY`, `ID` } within which the error was detected. + */ + public location?: ApiErrorLocation; + + /** + * + * @param {ApiErrorLocation} location An [[ApiErrorLocation]] describing the + * place within the incoming HTTP request within which the error was detected. + * @param {string} pointer A JSON Pointer [RFC6901] to the associated entity + * in the request document. + * @param {string} parameter A string indicating the URI query parameter + * that caused the error. + */ + public constructor(location: ApiErrorLocation, pointer?: string, parameter?: string) { + this.location = location; + this.pointer = pointer; + this.parameter = parameter; + } + +} + +export { ApiErrorSource, ApiErrorLocation }; + diff --git a/libs/server-common/src/error/api/apiErrors.spec.ts b/libs/server-common/src/error/api/apiErrors.spec.ts new file mode 100644 index 00000000..a4db932b --- /dev/null +++ b/libs/server-common/src/error/api/apiErrors.spec.ts @@ -0,0 +1,101 @@ + +import { ApiError } from './apiError'; +import { ApiErrors } from './apiErrors'; +import { ApiErrorLocation, ApiErrorSource } from './apiErrorSource'; + +describe('ApiErrors', () => { + + it('should contain a "SystemError" code when creating a DVPAPI-001 error.', () => { + const apiErrors: ApiErrors = new ApiErrors(); + apiErrors.addErrorById('DVPAPI-001'); + expect(apiErrors.errors[0].code).toMatch(/SystemError/); + }); + + it('should have a helpURL', () => { + const apiErrors: ApiErrors = new ApiErrors(); + apiErrors.addErrorById('DVPAPI-001'); + expect(apiErrors.errors[0].helpUrl).toBeDefined(); + }); + + it('should have helpText', () => { + const apiErrors: ApiErrors = new ApiErrors(); + apiErrors.addErrorById('DVPAPI-001'); + expect(apiErrors.errors[0].helpText).toBeDefined(); + }); + + it('should contain details describing the system error', () => { + const apiErrors: ApiErrors = new ApiErrors(); + apiErrors.addErrorById('DVPAPI-001'); + const error: string = JSON.stringify(apiErrors); + expect(error).toMatch(/An internal system error has occurred./); + }); + + it('should have the code "Error" when creating an "Unknown" standard error', () => { + const apiErrors: ApiErrors = new ApiErrors(); + apiErrors.addErrorById('Unknown'); + expect(apiErrors.errors[0].code).toBe('Error'); + }); + + it('should not contain an "id" when creating an "code" / "description" error', () => { + const apiErrors: ApiErrors = new ApiErrors(); + apiErrors.addErrorCode('UnknownError', 'An unknown error occurred'); + expect(apiErrors.errors[0].id).toBeUndefined(); + }); + + it('should have a formatted details string when generating a standard error', () => { + const apiErrors: ApiErrors = new ApiErrors(); + + apiErrors.addErrorById( + ApiError.VALIDATION_ERROR_ID, + 'value1', + 'field1' + ); + + expect(apiErrors.errors[0].detail).toEqual( + 'The value [value1] of the [field1] field is invalid.' + ); + }); + + it('should contain all specified fields when created using the addErrorDetail method', () => { + const apiErrors: ApiErrors = new ApiErrors(); + + apiErrors.addErrorDetail( + 'DVPAPI-000', + 'DefaultError', + 'This is the default error', + new ApiErrorSource(ApiErrorLocation.REQUEST), + 'http://example.com', + 'No help available' + ); + + expect(apiErrors.errors[0].id).toBe('DVPAPI-000'); + expect(apiErrors.errors[0].code).toBe('DefaultError'); + expect(apiErrors.errors[0].detail).toBe('This is the default error'); + expect(apiErrors.errors[0].helpUrl).toBe('http://example.com'); + expect(apiErrors.errors[0].helpText).toBe('No help available'); + }); + + it('should return a "true" isEmpty() function if there are no errors', () => { + const apiErrors: ApiErrors = new ApiErrors(); + + expect(apiErrors.isEmpty()).toBe(true); + + apiErrors.addErrorById('Unknown'); + + expect(apiErrors.isEmpty()).toBe(false); + }); + + it('should be able to return multiple errors', () => { + const apiErrors: ApiErrors = new ApiErrors(); + + apiErrors.addError(ApiError.fromId('DVPAPI-001')); + apiErrors.addError(ApiError.fromId('DVPAPI-002')); + apiErrors.addError(ApiError.fromId('DVPAPI-003')); + + expect(apiErrors.errors).toBeDefined(); + expect(apiErrors.errors.length).toEqual(3); + expect(apiErrors.errors[0].id).toEqual('DVPAPI-001'); + expect(apiErrors.errors[1].id).toEqual('DVPAPI-002'); + expect(apiErrors.errors[2].id).toEqual('DVPAPI-003'); + }); +}); diff --git a/libs/server-common/src/error/api/apiErrors.ts b/libs/server-common/src/error/api/apiErrors.ts new file mode 100644 index 00000000..bf2261b1 --- /dev/null +++ b/libs/server-common/src/error/api/apiErrors.ts @@ -0,0 +1,89 @@ +import { ApiError } from './apiError'; +import { ApiErrorSource } from './apiErrorSource'; + +/** + * An object that encapsulates a set of RESTful API errors. + * + * This object is expected to be included within the body of non-20X responses + * from an API so as to indicate to the cause of an error. + * + * It has been designed to comply with the Home Affairs Enterprise Integration + * Service standards for API error messages. + * + * @see [Error Handling](https://confluence.bcz.gov.au/display/EI/Error+Handling) + */ +class ApiErrors { + + /** An array of all [[ApiError]]s that have occurred. */ + public errors: ApiError[] = []; + + /** + * Returns true if this object contains any errors. + */ + public isEmpty(): boolean { + return this.errors.length === 0; + } + + /** + * Adds an [[ApiError]] error to the [[errors]] array. + * + * @param error the ApiError to add. + */ + public addError(error: ApiError): ApiErrors { + this.errors.push(error); + return this; + } + + /** + * Add a 'pre-defined' error to the [[errors]] array. + * + * @param {string} id The identifier of the 'pre-defined' ApiError. This + * will be 'looked-up' in the [[ApiError.STANDARD_API_ERRORS_LIST]] array. + * @param {string[]} args The parameters passed to the + * [[ApiError.STANDARD_API_ERRORS_LIST]] `ApiError.detail` format string. + * These will replace the '{n}' position holders within the + * `ApiError.detail` string. + */ + public addErrorById(id: string, ...args: string[]): ApiErrors { + this.errors.push(ApiError.fromId(id, ...args)); + return this; + } + + /** + * Add a (minimal) 'custom' error to the [[errors]] array. + * + * @param code An application specific error code. + * @param detail A human-readable explanation specific to this occurrence of + * the problem. + */ + public addErrorCode(code: string, detail: string): ApiErrors { + this.errors.push(ApiError.fromCode(code, detail)); + return this; + } + + /** + * Add a fully defined 'custom' error, with an 'id', 'code', 'detail', + * 'source', 'helpUrl', and 'helpText'. + * + * @param {string} id Identifier of the specific error. + * @param {string} code An application specific error code. + * @param {string} detail A human-readable explanation specific to this occurrence of the problem. + * @param {ApiErrorSource} source An object containing references to the source of the error. + * @param {string} helpUrl A URL which leads to further details about the error (e.g. help page). + * @param {string} helpText Help text which can provide further assistance on the error. + */ + public addErrorDetail( + id: string, + code: string, + detail: string, + source?: ApiErrorSource, + helpUrl?: string, + helpText?: string + ): ApiErrors { + this.errors.push(ApiError.fromDetail(id, code, detail, source, helpUrl, helpText)); + return this; + } +} + +export { ApiErrors }; + diff --git a/libs/server-common/src/error/api/index.ts b/libs/server-common/src/error/api/index.ts new file mode 100644 index 00000000..159011f0 --- /dev/null +++ b/libs/server-common/src/error/api/index.ts @@ -0,0 +1,5 @@ +export * from './apiError'; +export * from './apiErrors'; +export * from './apiErrorSource'; +export * from './ajvSchemaValidationError'; + diff --git a/libs/server-common/src/error/applicationError.spec.ts b/libs/server-common/src/error/applicationError.spec.ts new file mode 100644 index 00000000..835e3628 --- /dev/null +++ b/libs/server-common/src/error/applicationError.spec.ts @@ -0,0 +1,176 @@ +import { ApiErrors } from './api/apiErrors'; +import { ApplicationError } from './applicationError'; +import { AuthorizationError } from './authorizationError'; +import { NotFoundError } from './notFoundError'; +import { NotImplementedError } from './notImplementedError'; +import { QueryParameterError } from './queryParameterError'; +import { SystemError } from './systemError'; +import { ValidationError } from './validationError'; + +/** + * Check that the `toApiError()` method on an [[ApplicationError]] generates the expected + * RESTful API object. + * + * @param error The [[ApplicationError]] to be checked. + * @param httpsStatusCode The expected HTTP Status Code to be associated with + * the given _error_. + * @param id The expected id to be returned in the RESTful API error. + * @param code The expected error code to be returned in the RESTful API error. + * @param detail The expected detail string to be returned in the RESTful API + * error. + */ +const expectError = ( + error: ApplicationError, + httpStatusCode: number, + id: string, + code: string, + detail: string +): void => { + const apiErrors: ApiErrors = error.toApiError(); + + expect(error.httpStatusCode).toEqual(httpStatusCode); + expect(apiErrors).toBeDefined(); + expect(apiErrors.errors).toBeDefined(); + expect(apiErrors.errors.length).toEqual(1); + expect(apiErrors.errors[0].id).toEqual(id); + expect(apiErrors.errors[0].code).toEqual(code); + expect(apiErrors.errors[0].detail).toEqual(detail); + expect(apiErrors.errors[0].helpUrl).toBeDefined(); + expect(apiErrors.errors[0].helpText).toBeDefined(); +}; + +describe('ApplicationError', () => { + + it('should correctly format an "ApplicationError".', () => { + const error: ApplicationError = new ApplicationError('Error'); + expect(error).toBeDefined(); + expectError(error, 500, 'DVPAPI-001', 'SystemError', 'An internal system error has occurred.'); + }); + + it('should correctly format an empty "SystemError".', () => { + const error: ApplicationError = new SystemError(new Error()); + expect(error).toBeDefined(); + expectError(error, 500, 'DVPAPI-001', 'SystemError', 'An internal system error has occurred.'); + }); + + it('should correctly format a "SystemError".', () => { + const error: ApplicationError = new SystemError(new Error('Unexpected Error')); + expect(error).toBeDefined(); + expectError(error, 500, 'DVPAPI-001', 'SystemError', 'An internal system error has occurred.'); + }); + + it('should correctly format a "ValidationError".', () => { + const error: ApplicationError = new ValidationError('/data/senderReference', 'missing'); + expect(error).toBeDefined(); + expectError( + error, + 422, + 'DVPAPI-002', + 'ValidationError', + 'The value [missing] of the [/data/senderReference] field is invalid.' + ); + }); + + it('should correctly format a "NotFoundError".', () => { + const error: ApplicationError = new NotFoundError('/flights/arrivals/c0c191b3-be3e-42f6-968c-625ed7223627'); + expect(error).toBeDefined(); + expectError( + error, + 404, + 'DVPAPI-004', + 'NotFound', + 'Could not find [/flights/arrivals/c0c191b3-be3e-42f6-968c-625ed7223627].' + ); + }); + + it('should correctly format a "NotImplementedError".', () => { + const error: ApplicationError = new NotImplementedError('/flights/arrivals', 'DELETE'); + expect(error).toBeDefined(); + expectError( + error, + 501, + 'DVPAPI-005', + 'NotImplemented', + 'The [DELETE] method has not yet been implemented for the [/flights/arrivals] resource.' + ); + }); + + it('should correctly format a "QueryParameterError".', () => { + const error1: ApplicationError = new QueryParameterError('sort', 'UNKNOWN'); + expect(error1).toBeDefined(); + expectError( + error1, + 422, + 'DVPAPI-002', + 'ValidationError', + 'The value [UNKNOWN] of the [sort] query parameter is invalid.' + ); + + const error2: ApplicationError = new QueryParameterError( + 'sort', + 'UNKNOWN', + 'Sort must be either ASC or DSC.' + ); + expect(error2).toBeDefined(); + expectError( + error2, + 422, + 'DVPAPI-002', + 'ValidationError', + 'The value [UNKNOWN] of the [sort] query parameter is invalid. Reason: Sort must be either ASC or DSC.' + ); + }); + + it('should correctly format a "AuthorizationError".', () => { + + const error1: ApplicationError = new AuthorizationError('/flights/arrivals/1'); + expect(error1).toBeDefined(); + expectError( + error1, + 500, + 'DVPAPI-003', + 'SecurityError', + 'User is not authorized to access [/flights/arrivals/1]' + ); + + const error2: ApplicationError = new AuthorizationError('/flights/arrivals/1', 'user1'); + expect(error2).toBeDefined(); + expectError( + error2, + 500, + 'DVPAPI-003', + 'SecurityError', + 'User [user1] is not authorized to access [/flights/arrivals/1]' + ); + + const error3: ApplicationError = new AuthorizationError( + '/flights/arrivals/1', + undefined, + 'abn', + '33380054835' + ); + expect(error3).toBeDefined(); + expectError( + error3, + 500, + 'DVPAPI-003', + 'SecurityError', + 'User with [abn] = [33380054835] is not authorized to access [/flights/arrivals/1]' + ); + + const error4: ApplicationError = new AuthorizationError( + '/flights/arrivals/1', + 'user1', + 'abn', + '33380054835' + ); + expect(error4).toBeDefined(); + expectError( + error4, + 500, + 'DVPAPI-003', + 'SecurityError', + 'User [user1] with [abn] = [33380054835] is not authorized to access [/flights/arrivals/1]' + ); + }); +}); diff --git a/libs/server-common/src/error/applicationError.ts b/libs/server-common/src/error/applicationError.ts new file mode 100644 index 00000000..7f213faa --- /dev/null +++ b/libs/server-common/src/error/applicationError.ts @@ -0,0 +1,69 @@ +/** + * This file is a [TypeScript](https://www.typescriptlang.org/) file developed + * for the Department of Home Affairs **DVP** (Integrated Cargo System) + * Capability Uplift Project. + * + * This script defines a TypeScript Type for the [[ApplicationError]] class. + * + * The [[ApplicationError]] class is a custom TypeScript [[Error]] that can be thrown by + * the **DVP** code and converted into a RESTful [[ApiErrors]] object compliant + * with Departmental REST standards. + * + * @see [Error Handling](https://confluence.bcz.gov.au/display/EI/Error+Handling) + * + * @module server-common.error + * @author Brian Kavanagh + * @since 2020-09-07 + */ +import { ApiError } from './api/apiError'; +import { ApiErrors } from './api/apiErrors'; + +/** + * A custom TypeScript [[Error]] that can be thrown by the **DVP** TypeScript + * code and converted into a RESTful [[ApiErrors]] object compliant with + * Departmental standards. + * + * @see [Error Handling](https://confluence.bcz.gov.au/display/EI/Error+Handling) + */ +class ApplicationError extends Error { + + /** + * The default HTTP status code returned by the API if no explicit HTTP + * status code is specified. + */ + private static readonly DEFAULT_HTTP_ERROR = 500; + + /** + * The HTTP Status Code to be returned by the RESTful API when this error is + * encountered. + */ + public httpStatusCode: number; + + /** + * Constructs an [[ApplicationError]]. + * + * @param {string} message A human readable description of the error. + * @param {number} httpStatusCode The (optional) HTTP Status code to be + * returned by the API when this error occurs. + */ + public constructor(message: string, httpStatusCode?: number) { + super(message); + this.httpStatusCode = (httpStatusCode) ? httpStatusCode : ApplicationError.DEFAULT_HTTP_ERROR; + } + + /** + * Convert the current [[ApplicationError]] into an [[ApiErrors]] object suitable + * return within a DVP REST API call. + * + * @return {ApiErrors} An object describing the [[Error]] that has occurred + * in a format compliant with the Departmental REST standards. + */ + public toApiError = (): ApiErrors => { + const apiErrors = new ApiErrors(); + apiErrors.addErrorById(ApiError.SYSTEM_ERROR_ID); + return apiErrors; + }; +} + +export { ApplicationError }; + diff --git a/libs/server-common/src/error/authorizationError.ts b/libs/server-common/src/error/authorizationError.ts new file mode 100644 index 00000000..4843c1ad --- /dev/null +++ b/libs/server-common/src/error/authorizationError.ts @@ -0,0 +1,106 @@ +import { ApiErrors } from './api/apiErrors'; +import { ApplicationError } from './applicationError'; + +/** + * A custom TypeScript [[Error]] that can be thrown by the **DVP** TypeScript + * code to indicate that a client request is unauthorized. + * + * As this inherits from the [[ApplicationError]] class, it can be converted into an + * object compliant with Departmental RESTful API Error Handling standards. + * + * @see [Error Handling](https://confluence.bcz.gov.au/display/EI/Error+Handling) + */ +class AuthorizationError extends ApplicationError { + + /** + * The HTTP status code returned by the API when it encounters an internal + * System Error. + */ + private static readonly AUTHORIZATION_HTTP_ERROR = 500; + + /** + * The resource that is attempting to be accessed. + */ + private resource: string; + + /** + * The user that is attempting to be access the resource. + */ + private user: string | undefined; + + /** + * The name of the attribute being used to determine access (i.e. role, abn, + * etc...) + */ + private attribute: string | undefined; + + /** + * The value of the attribute being used to determine access (i.e. role, + * abn, etc...) + */ + private attributeValue: string | undefined; + + /** + * Constructs an [[AuthorizationError]] for an attempt to access the given + * _resource_. + * + * @param {string} resource The resource that was being attempted to be accessed. + * @param {string | undefined} user The user that is attempting to accessed + * the resource. + * @param {string | undefined} attribute The name of the attribute + * being used to determine access. + * @param { string | undefined} attributeValue The value of the attribute + * being used to determine access. + */ + public constructor( + resource: string, + user?: string, + attribute?: string, + attributeValue?: string + ) { + super( + ((user) ? ('User [' + user + '] ') : 'User ') + + ((attribute) ? (' with [' + attribute + '] = [' + attributeValue + '] ') : '') + + 'is not authorized to access [' + resource + ']', + AuthorizationError.AUTHORIZATION_HTTP_ERROR + ); + + this.resource = resource; + this.user = user; + this.attribute = attribute; + this.attributeValue = attributeValue; + } + + /** + * Convert the current [[AuthorizationError]] into an [[ApiErrors]] object suitable + * return within an DVP REST API Call. + * + * @return {ApiErrors} An object describing the [[Error]] that has occurred + * in a format compliant with the Departmental REST standards. + */ + public override toApiError = (): ApiErrors => { + + const apiErrors = new ApiErrors(); + + const detail = ((this.user) ? ('User [' + this.user + '] ') : 'User ') + + ((this.attribute) ? ('with [' + this.attribute + '] = [' + this.attributeValue + '] ') : '') + + 'is not authorized to access [' + this.resource + ']'; + + apiErrors.addErrorDetail( + 'DVPAPI-003', + 'SecurityError', + detail, + undefined, + 'https://www.abf.gov.au/help-and-support/contact-us', + 'The user associated with the HTTP request made to the API is not ' + + 'authorized to perform the desired operation. ' + + 'Please check that the resource your user is attempting access ' + + 'is correct, and that you are allowed to perform the given API operation.' + ); + + return apiErrors; + + }; +} + +export { AuthorizationError }; diff --git a/libs/server-common/src/error/badRequestError.ts b/libs/server-common/src/error/badRequestError.ts new file mode 100644 index 00000000..7520594b --- /dev/null +++ b/libs/server-common/src/error/badRequestError.ts @@ -0,0 +1,63 @@ +import { ApiErrors } from './api/apiErrors'; +import { ApplicationError } from './applicationError'; + +/** + * A custom TypeScript [[Error]] that can be thrown by the **DVP** TypeScript + * code to indicate that an internal System Error has occurred. + * + * As this inherits from the [[ApplicationError]] class, it can be converted into an + * object compliant with Departmental RESTful API Error Handling standards. + * + * @see [Error Handling](https://confluence.bcz.gov.au/display/EI/Error+Handling) + */ +class BadRequestError extends ApplicationError { + /** + * The HTTP status code returned by the API when it encounters an internal + * System Error. + */ + private static readonly BAD_REQUEST_HTTP_ERROR = 400; + + /** + * The underlying TypeScript [[Error]] which caused this [[BadRequestError]]. + */ + private cause: Error; + + /** + * Constructs a [[BadRequestError]] for a given [[Error]]. + * + * @param {Error} cause The underlying TypeScript [[Error]] which caused this + * [[BadRequestError]]. + */ + public constructor(cause: Error) { + super( + cause?.message?.length !== 0 ? cause.message : 'Bad Request Error', + BadRequestError.BAD_REQUEST_HTTP_ERROR + ); + + this.cause = cause; + } + + /** + * Convert the current [[BadRequestError]] into an [[ApiErrors]] object suitable + * return within an DVP REST API Call. + * + * @return {ApiErrors} An object describing the [[Error]] that has occurred + * in a format compliant with the Departmental REST standards. + */ + public override toApiError = (): ApiErrors => { + const apiErrors = new ApiErrors(); + + apiErrors.addErrorDetail( + 'DVPAPI-002', + 'BadRequestError', + this.message, + undefined, + 'https://www.abf.gov.au/help-and-support/contact-us', + "The HTTP request payload doesn't comply with the expected format. Please check the API specification, and re-submit the HTTP request." + ); + + return apiErrors; + }; +} + +export { BadRequestError }; diff --git a/libs/server-common/src/error/configError.ts b/libs/server-common/src/error/configError.ts new file mode 100644 index 00000000..9437774b --- /dev/null +++ b/libs/server-common/src/error/configError.ts @@ -0,0 +1,19 @@ +/** + * This class is a JavaScript [[Error]] used to indicate that something has + * gone wrong when attempting to retrieve application configuration using a + * [[ConfigFactory]]. + */ +class ConfigError extends Error { + + /** + * Constructs a new [[ConfigError]]. + * + * @param {string} message A description of the configuration error. + */ + constructor(message?: string) { + super(message); + } +} + +export { ConfigError }; + diff --git a/libs/server-common/src/error/index.ts b/libs/server-common/src/error/index.ts new file mode 100644 index 00000000..226ea72f --- /dev/null +++ b/libs/server-common/src/error/index.ts @@ -0,0 +1,11 @@ +export * from './api'; +export * from './applicationError'; +export * from './authorizationError'; +export * from './badRequestError'; +export * from './configError'; +export * from './notFoundError'; +export * from './notImplementedError'; +export * from './queryParameterError'; +export * from './securityError'; +export * from './systemError'; +export * from './validationError'; diff --git a/libs/server-common/src/error/notFoundError.ts b/libs/server-common/src/error/notFoundError.ts new file mode 100644 index 00000000..b2a8f591 --- /dev/null +++ b/libs/server-common/src/error/notFoundError.ts @@ -0,0 +1,62 @@ +import { ApiError } from './api/apiError'; +import { ApiErrors } from './api/apiErrors'; +import { ApplicationError } from './applicationError'; + +/** + * A custom TypeScript [[Error]] that can be thrown by the **DVP** TypeScript + * code to indicate that a given RESTful API resource cannot be found. + * + * As this inherits from the [[ApplicationError]] class, it can be converted into an + * object compliant with Departmental RESTful API Error Handling standards. + * + * @see [Error Handling](https://confluence.bcz.gov.au/display/EI/Error+Handling) + */ +class NotFoundError extends ApplicationError { + + /** + * The HTTP status code returned by the API when a given API method has not + * (yet) been implemented for a given API resource. + */ + private static readonly NOT_FOUND_HTTP_ERROR = 404; + + /** + * The resource + * (i.e. `/flights/arrivals/c0c191b3-be3e-42f6-968c-625ed7223627`) that was not + * found. + */ + private resource: string; + + /** + * Constructs a [[NotFoundError]] for a given _resource_ / _method_. + * + * @param {string} resource The resource (i.e. `/flights/arrivals`) of the + * REST call that has yet to be implemented. + */ + public constructor(resource: string) { + super(`Could not find resource [${resource}].`, NotFoundError.NOT_FOUND_HTTP_ERROR); + this.resource = resource; + } + + /** + * Convert the current [[NotFoundError]] into an [[ApiErrors]] object + * suitable return within an DVP REST API Call. + * + * @return {ApiErrors} An object describing the [[Error]] that has occurred + * in a format compliant with the Departmental REST standards. + */ + public override toApiError = (): ApiErrors => { + + const apiErrors = new ApiErrors(); + + apiErrors.addErrorById( + ApiError.NOT_FOUND_ERROR_ID, + this.resource + ); + + return apiErrors; + + }; +} + +export { NotFoundError }; + diff --git a/libs/server-common/src/error/notImplementedError.ts b/libs/server-common/src/error/notImplementedError.ts new file mode 100644 index 00000000..27c57b4b --- /dev/null +++ b/libs/server-common/src/error/notImplementedError.ts @@ -0,0 +1,74 @@ +import { ApiError } from './api/apiError'; +import { ApiErrors } from './api/apiErrors'; +import { ApplicationError } from './applicationError'; + +/** + * A custom TypeScript [[Error]] that can be thrown by the **DVP** TypeScript + * code to indicate that a given RESTful API method is not (yet) implemented. + * + * As this inherits from the [[ApplicationError]] class, it can be converted into an + * object compliant with Departmental RESTful API Error Handling standards. + * + * @see [Error Handling](https://confluence.bcz.gov.au/display/EI/Error+Handling) + */ +class NotImplementedError extends ApplicationError { + + /** + * The HTTP status code returned by the API when a given API method has not + * (yet) been implemented for a given API resource. + */ + private static readonly NOT_IMPLEMENTED_HTTP_ERROR = 501; + + /** + * The resource (i.e. `/flights/arrivals`) that has yet to be implemented. + */ + private resource: string; + + /** + * The HTTP method (i.e. `GET`, `POST`, `OPTIONS`, etc...) of the REST call + * that is yet to be implemented. + */ + private method: string; + + /** + * Constructs a [[NotImplementedError]] for a given _resource_ / _method_. + * + * @param {string} resource The resource (i.e. `/flights/arrivals`) that has + * yet to be implemented. + * @param {string} method The HTTP method (`GET`, `POST`, `OPTIONS`, etc...) + * of the REST call that is yet to be implemented. + */ + public constructor(resource: string, method: string) { + super( + `The [${method}] method has not yet been implemented for the [${resource}].`, + NotImplementedError.NOT_IMPLEMENTED_HTTP_ERROR + ); + + this.resource = resource; + this.method = method; + } + + /** + * Convert the current [[NotImplementedError]] into an [[ApiErrors]] object + * suitable return within an **DVP** API Call. + * + * @return {ApiErrors} An object describing the [[Error]] that has occurred + * in a format compliant with the Departmental REST standards. + */ + public override toApiError= (): ApiErrors => { + + const apiErrors = new ApiErrors(); + + apiErrors.addErrorById( + ApiError.NOT_IMPLEMENTED_ID, + this.method, + this.resource + ); + + return apiErrors; + + }; +} + +export { NotImplementedError }; + diff --git a/libs/server-common/src/error/queryParameterError.ts b/libs/server-common/src/error/queryParameterError.ts new file mode 100644 index 00000000..ae994adb --- /dev/null +++ b/libs/server-common/src/error/queryParameterError.ts @@ -0,0 +1,94 @@ +import { ApiErrors } from './api/apiErrors'; +import { ApiErrorLocation, ApiErrorSource } from './api/apiErrorSource'; +import { ApplicationError } from './applicationError'; + +/** + * A custom TypeScript [[Error]] that can be thrown by the **DVP** TypeScript + * code to indicate that a client request containers invalid HTTP Query + * Parameters. + * + * As this inherits from the [[ApplicationError]] class, it can be converted into an + * object compliant with Departmental RESTful API Error Handling standards. + * + * @see [Error Handling](https://confluence.bcz.gov.au/display/EI/Error+Handling) + */ +class QueryParameterError extends ApplicationError { + + /** + * The HTTP status code returned by the API when a validation error is + * encountered. + */ + private static readonly VALIDATION_HTTP_ERROR = 422; + + /** + * The name of the query parameter which is invalid. + */ + private parameterName: string; + + /** + * The value of the query parameter which is invalid. + */ + private value: string; + + /** + * An (optional) detailed description of why the query parameter is invalid. + */ + private detail: string | undefined; + + /** + * Constructs an [[QueryParameterError]] for a given _parameterName_ / + * _value_ / _detail_. + * + * @param {string} parameterName The name of the query parameter which is invalid. + * @param {string} value The value of the query parameter which is invalid. + * @param {string?} detail An (optional) detailed description of why the + * query parameter is invalid. + */ + public constructor( + parameterName: string, + value: string, + detail?: string + ) { + super( + `The value [${value}] is not valid for the [${parameterName}] query parameter` + + ((detail) ? ('. Reason: ' + detail) : ''), + QueryParameterError.VALIDATION_HTTP_ERROR + ); + + this.parameterName = parameterName; + this.value = value; + this.detail = detail; + } + + /** + * Convert the current [[QueryParameterError]] into an [[ApiErrors]] object suitable + * return within an DVP REST API Call. + * + * @return {ApiErrors} An object describing the [[Error]] that has occurred + * in a format compliant with the Departmental REST standards. + */ + public override toApiError = (): ApiErrors => { + + const apiErrors = new ApiErrors(); + + const detail = 'The value [' + this.value + '] ' + + 'of the [' + this.parameterName + '] query parameter is invalid' + + ((this.detail) ? ('. Reason: ' + this.detail) : '.'); + + apiErrors.addErrorDetail( + 'DVPAPI-002', + 'ValidationError', + detail, + new ApiErrorSource(ApiErrorLocation.QUERY, undefined, this.parameterName), + 'https://www.abf.gov.au/help-and-support/contact-us', + 'The HTTP request made to the API included a HTTP Query Parameter which did ' + + 'not comply with the expected format. Please check the API specification, ' + + 'and re-submit the HTTP request.' + ); + + return apiErrors; + + }; +} + +export { QueryParameterError }; diff --git a/libs/server-common/src/error/securityError.ts b/libs/server-common/src/error/securityError.ts new file mode 100644 index 00000000..47d92e41 --- /dev/null +++ b/libs/server-common/src/error/securityError.ts @@ -0,0 +1,62 @@ +import { ApiError } from './api/apiError'; +import { ApiErrors } from './api/apiErrors'; +import { ApplicationError } from './applicationError'; + +/** + * A custom TypeScript [[Error]] that can be thrown by the **DVP** TypeScript + * code to indicate that an internal System Error has occurred. + * + * As this inherits from the [[ApplicationError]] class, it can be converted into an + * object compliant with Departmental RESTful API Error Handling standards. + * + * @see [Error Handling](https://confluence.bcz.gov.au/display/EI/Error+Handling) + */ +class SecurityError extends ApplicationError { + + /** + * The HTTP status code returned by the API when it encounters an internal + * Security Error. + */ + private static readonly SECURITY_HTTP_ERROR = 403; + + /** + * The underlying TypeScript [[Error]] which caused this [[SecurityError]]. + */ + private cause: Error; + + /** + * Constructs a [[SecurityError]] for a given [[Error]]. + * + * @param {Error} cause The underlying TypeScript [[Error]] which caused this + * [[SecurityError]]. + * @param message The message for the error. + */ + public constructor(cause: Error, message?: string) { + super( + (message ? message + ': ' : '') + + (cause.message ? ((message ? message + ': ' : '') + cause.message) : 'Security Error'), + SecurityError.SECURITY_HTTP_ERROR + ); + + this.cause = cause; + } + + /** + * Convert the current [[SecurityError]] into an [[ApiErrors]] object suitable + * return within an DVP REST API Call. + * + * @return {ApiErrors} An object describing the [[Error]] that has occurred + * in a format compliant with the Departmental REST standards. + */ + public override toApiError = (): ApiErrors => { + + const apiErrors = new ApiErrors(); + + apiErrors.addErrorById(ApiError.SECURITY_ERROR_ID); + + return apiErrors; + + }; +} + +export { SecurityError }; diff --git a/libs/server-common/src/error/systemError.ts b/libs/server-common/src/error/systemError.ts new file mode 100644 index 00000000..c4e1cc22 --- /dev/null +++ b/libs/server-common/src/error/systemError.ts @@ -0,0 +1,57 @@ +import { ApiError } from './api/apiError'; +import { ApiErrors } from './api/apiErrors'; +import { ApplicationError } from './applicationError'; + +/** + * A custom TypeScript [[Error]] that can be thrown by the **DVP** TypeScript + * code to indicate that an internal System Error has occurred. + * + * As this inherits from the [[ApplicationError]] class, it can be converted into an + * object compliant with Departmental RESTful API Error Handling standards. + * + * @see [Error Handling](https://confluence.bcz.gov.au/display/EI/Error+Handling) + */ +class SystemError extends ApplicationError { + /** + * The HTTP status code returned by the API when it encounters an internal + * System Error. + */ + private static readonly SYSTEM_HTTP_ERROR = 500; + + /** + * The underlying TypeScript [[Error]] which caused this [[SystemError]]. + */ + private cause: Error; + + /** + * Constructs a [[SystemError]] for a given [[Error]]. + * + * @param {Error} cause The underlying TypeScript [[Error]] which caused this + * [[SystemError]]. + */ + public constructor(cause: Error) { + super( + cause?.message?.length !== 0 ? cause.message : 'System Error', + SystemError.SYSTEM_HTTP_ERROR + ); + + this.cause = cause; + } + + /** + * Convert the current [[SystemError]] into an [[ApiErrors]] object suitable + * return within an DVP REST API Call. + * + * @return {ApiErrors} An object describing the [[Error]] that has occurred + * in a format compliant with the Departmental REST standards. + */ + public override toApiError = (): ApiErrors => { + const apiErrors = new ApiErrors(); + + apiErrors.addErrorById(ApiError.SYSTEM_ERROR_ID); + + return apiErrors; + }; +} + +export { SystemError }; diff --git a/libs/server-common/src/error/validationError.ts b/libs/server-common/src/error/validationError.ts new file mode 100644 index 00000000..8d17e7e0 --- /dev/null +++ b/libs/server-common/src/error/validationError.ts @@ -0,0 +1,75 @@ +import { ApiError } from './api/apiError'; +import { ApiErrors } from './api/apiErrors'; +import { ApplicationError } from './applicationError'; + +/** + * A custom TypeScript [[Error]] that can be thrown by the **DVP** TypeScript + * code to indicate that an incoming RESTful API request contains a validation + * error. + * + * As this inherits from the [[ApplicationError]] class, it can be converted into an + * object compliant with Departmental RESTful API Error Handling standards. + * + * @see [Error Handling](https://confluence.bcz.gov.au/display/EI/Error+Handling) + */ +class ValidationError extends ApplicationError { + + /** + * The HTTP status code returned by the API when a validation error is + * encountered. + */ + private static readonly VALIDATION_HTTP_ERROR = 422; + + /** + * A JSON pointer to the field (i.e. `/data/senderReference`) within an + * incoming REST API call that is 'invalid'. + */ + private fieldPointer: string; + + /** + * The value of the field which is invalid. + */ + private value: string; + + /** + * Constructs a [[ValidationError]] for a given _fieldPointer_ / _value_. + * + * @param {string} fieldPointer A JSON pointer to the field + * (i.e. `/data/senderReference`) with an incoming REST API call that is + * 'invalid'. + * @param {string} value The value of the field which is invalid. + */ + public constructor(fieldPointer: string, value: string) { + super( + `The value [${value}] is not valid for the [${fieldPointer}] field.`, + ValidationError.VALIDATION_HTTP_ERROR + ); + + this.fieldPointer = fieldPointer; + this.value = value; + } + + /** + * Convert the current [[ValidationError]] into an [[ApiErrors]] object suitable + * return within an **DVP** API Call. + * + * @return {ApiErrors} An object describing the [[Error]] that has occurred + * in a format compliant with the Departmental REST standards. + */ + public override toApiError = (): ApiErrors => { + + const apiErrors = new ApiErrors(); + + apiErrors.addErrorById( + ApiError.VALIDATION_ERROR_ID, + this.value, + this.fieldPointer + ); + + return apiErrors; + + }; +} + +export { ValidationError }; + diff --git a/libs/server-common/src/fixtures/genericvc/degree_invalid.json b/libs/server-common/src/fixtures/genericvc/degree_invalid.json new file mode 100644 index 00000000..5f6a5210 --- /dev/null +++ b/libs/server-common/src/fixtures/genericvc/degree_invalid.json @@ -0,0 +1,20 @@ +{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1" + ], + "id": "http://example.edu/credentials/3732", + "type": ["VerifiableCredential", "UniversityDegreeCredential"], + "issuer": "did:key:z6MkqPEJBkvWyz8XsPQf22NNfWD2qSYfD6sFFqFwfwA915U6", + "issuanceDate": "2010-01-01T00:00:00Z", + "invalidField": "it's not in the context", + "credentialSubject": { + "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", + "degree": { + "type": "BachelorDegree", + "name": "Bachelor of Science and Arts" + } + }, + + "proof": { "this": "proof field shouldn't be here since it's not issued yet" } +} diff --git a/libs/server-common/src/fixtures/genericvc/degree_signed.json b/libs/server-common/src/fixtures/genericvc/degree_signed.json new file mode 100644 index 00000000..9350c48e --- /dev/null +++ b/libs/server-common/src/fixtures/genericvc/degree_signed.json @@ -0,0 +1,24 @@ +{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1" + ], + "id": "http://example.edu/credentials/3732", + "type": ["VerifiableCredential", "UniversityDegreeCredential"], + "credentialSubject": { + "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", + "degree": { + "name": "Bachelor of Science and Arts", + "type": "BachelorDegree" + } + }, + "issuer": "did:key:z6Mksr8sziCfheZ9x8KL8ATZLovEipQrcpXNT63ovrfkf4d9", + "issuanceDate": "2010-01-01T00:00:00Z", + "proof": { + "type": "Ed25519Signature2018", + "proofPurpose": "assertionMethod", + "verificationMethod": "did:key:z6Mksr8sziCfheZ9x8KL8ATZLovEipQrcpXNT63ovrfkf4d9#z6Mksr8sziCfheZ9x8KL8ATZLovEipQrcpXNT63ovrfkf4d9", + "created": "2022-08-15T04:40:15.882Z", + "jws": "eyJhbGciOiJFZERTQSIsImNyaXQiOlsiYjY0Il0sImI2NCI6ZmFsc2V9..uaDB5ryv_3cHLMzxFfyg7qUiakOYfWkDXbYRKUqEc6AKNk1wZxxPx1a-j8uDxgfEvwqtUsWEjus6avxj6u00DQ" + } +} diff --git a/libs/server-common/src/fixtures/genericvc/degree_unsigned.json b/libs/server-common/src/fixtures/genericvc/degree_unsigned.json new file mode 100644 index 00000000..e858351e --- /dev/null +++ b/libs/server-common/src/fixtures/genericvc/degree_unsigned.json @@ -0,0 +1,17 @@ +{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1" + ], + "id": "http://example.edu/credentials/3732", + "type": ["VerifiableCredential", "UniversityDegreeCredential"], + "issuer": "did:key:z6Mksr8sziCfheZ9x8KL8ATZLovEipQrcpXNT63ovrfkf4d9", + "issuanceDate": "2010-01-01T00:00:00Z", + "credentialSubject": { + "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", + "degree": { + "type": "BachelorDegree", + "name": "Bachelor of Science and Arts" + } + } +} diff --git a/libs/server-common/src/fixtures/genericvc/test_report_signed.json b/libs/server-common/src/fixtures/genericvc/test_report_signed.json new file mode 100644 index 00000000..abe8f18d --- /dev/null +++ b/libs/server-common/src/fixtures/genericvc/test_report_signed.json @@ -0,0 +1,131 @@ +{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/traceability/v1" + ], + "id": "http://localhost:8080/credentials/61858f5a-682a-4857-b759-f3487bae0658", + "issuanceDate": "2010-01-01T19:23:24Z", + "type": ["VerifiableCredential", "MillTestReportCertificate"], + "relatedLink": [ + { + "type": "LinkRole", + "target": "http://localhost:8080/templates/1a0d2c30-02a0-44cf-b1dc-9fe32652e90d", + "linkRelationship": "template", + "name": "Template" + }, + { + "type": "LinkRole", + "target": "http://localhost:8080/batches/a76e12e2-b495-4a03-a32a-5952d14a9798", + "linkRelationship": "batch", + "name": "Batch" + } + ], + "issuer": "did:key:z6MktWjP95fMqCMrfNULcdszFeTVUCE1zcgz3Hv5bVAisHgk", + "credentialSubject": { + "type": ["MillTestReport"], + "manufacturer": { + "type": ["Organization"], + "name": "Langworth, McGlynn and Koch", + "description": "Exclusive motivating concept", + "address": { + "type": ["PostalAddress"], + "streetAddress": "67663 Roob Highway", + "addressLocality": "O'Connellmouth", + "addressRegion": "New York", + "postalCode": "56036", + "addressCountry": "Philippines" + }, + "email": "Eva_Macejkovic@example.net", + "phoneNumber": "555-438-8521", + "faxNumber": "555-948-7194" + }, + "product": { + "type": ["SteelProduct"], + "heatNumber": "4732", + "specification": "ASTM-91357", + "grade": "4122", + "originalCountryOfMeltAndPour": "Madagascar", + "inspection": { + "type": ["InspectionReport"], + "observation": [ + { + "type": ["Observation"], + "property": { + "type": ["ChemicalProperty"], + "name": "Actinium" + }, + "measurement": { + "type": ["MeasuredValue"], + "value": "62.813", + "unitCode": "P1" + } + }, + { + "type": ["Observation"], + "property": { + "type": ["MechanicalProperty"], + "identifier": "ISO 1352", + "name": "Torque-controlled fatigue testing", + "description": "ISO 1352:2011 specifies the conditions for performing torsional, constant-amplitude, nominally elastic stress fatigue tests on metallic specimens without deliberately introducing stress concentrations. The tests are carried out at ambient temperature (ideally at between 10 °C and 35 °C) in air by applying a pure couple to the specimen about its longitudinal axis." + }, + "measurement": { + "type": ["MeasuredValue"], + "value": "00.00", + "unitCode": "UNKNOWN" + } + }, + { + "type": ["Observation"], + "property": { + "type": ["ChemicalProperty"], + "name": "Oxygen" + }, + "measurement": { + "type": ["MeasuredValue"], + "value": "37.187", + "unitCode": "P1" + } + } + ] + } + }, + "purchase": { + "type": ["Purchase"], + "customer": { + "type": ["Person"], + "email": "Savannah_Homenick@example.com", + "phoneNumber": "555-536-5801" + } + }, + "shipment": { + "type": ["ParcelDelivery"], + "deliveryAddress": { + "type": ["PostalAddress"], + "organizationName": "Cassin, Kautzer and Bauch", + "streetAddress": "2723 Alana Park", + "addressLocality": "South Millie", + "addressRegion": "Maryland", + "postalCode": "58776", + "addressCountry": "Cote d'Ivoire" + }, + "originAddress": { + "type": ["PostalAddress"], + "organizationName": "Conn and Sons", + "streetAddress": "9424 Alexa Locks", + "addressLocality": "West Josefina", + "addressRegion": "New Hampshire", + "postalCode": "99598", + "addressCountry": "Gabon" + }, + "deliveryMethod": "Truck transport", + "trackingNumber": "547132175583" + } + }, + "proof": { + "type": "Ed25519Signature2018", + "created": "2010-01-01T19:23:24Z", + "verificationMethod": "did:key:z6MktWjP95fMqCMrfNULcdszFeTVUCE1zcgz3Hv5bVAisHgk#z6MktWjP95fMqCMrfNULcdszFeTVUCE1zcgz3Hv5bVAisHgk", + "proofPurpose": "assertionMethod", + "jws": "eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..M5DWqdH00x4l_b-I9UnG-3vFpizdiploi77tlUe2-iw8Eg5mRMpVUWEwZZ7duNIOk4waXXvonCSJz2kMgjkZDQ" + } +} diff --git a/libs/server-common/src/fixtures/index.ts b/libs/server-common/src/fixtures/index.ts new file mode 100644 index 00000000..04505e76 --- /dev/null +++ b/libs/server-common/src/fixtures/index.ts @@ -0,0 +1 @@ +export * from './oa-docs'; diff --git a/libs/server-common/src/fixtures/oa-docs.ts b/libs/server-common/src/fixtures/oa-docs.ts new file mode 100644 index 00000000..2fc771df --- /dev/null +++ b/libs/server-common/src/fixtures/oa-docs.ts @@ -0,0 +1,205 @@ +export const OA_CREDENTIAL = { + version: 'https://schema.openattestation.com/3.0/schema.json', + '@context': [ + 'https://www.w3.org/2018/credentials/v1', + 'https://schemata.openattestation.com/com/openattestation/1.0/DrivingLicenceCredential.json', + 'https://schemata.openattestation.com/com/openattestation/1.0/OpenAttestation.v3.json', + 'https://schemata.openattestation.com/com/openattestation/1.0/CustomContext.json', + ], + reference: 'SERIAL_NUMBER_123', + name: 'Republic of Singapore Driving Licence', + issuanceDate: '2010-01-01T19:23:24Z', + validFrom: '2010-01-01T19:23:24Z', + issuer: { + id: 'https://example.com', + type: 'OpenAttestationIssuer', + name: 'DEMO STORE', + }, + type: [ + 'VerifiableCredential', + 'DrivingLicenceCredential', + 'OpenAttestationCredential', + ], + credentialSubject: { + id: 'did:example:SERIAL_NUMBER_123', + class: [ + { + type: '3', + effectiveDate: '2010-01-01T19:23:24Z', + }, + { + type: '3A', + effectiveDate: '2010-01-01T19:23:24Z', + }, + ], + }, + openAttestationMetadata: { + template: { + name: 'CUSTOM_TEMPLATE', + type: 'EMBEDDED_RENDERER', + url: 'https://localhost:3000/renderer', + }, + proof: { + type: 'OpenAttestationProofMethod', + method: 'DID', + value: 'did:ethr:0x1245e5B64D785b25057f7438F715f4aA5D965733', + revocation: { + type: 'NONE', + }, + }, + identityProof: { + type: 'DID', + identifier: 'did:ethr:0x1245e5B64D785b25057f7438F715f4aA5D965733', + }, + }, + attachments: [ + { + fileName: 'sample.pdf', + mimeType: 'application/pdf', + data: 'BASE64_ENCODED_FILE', + }, + ], +}; + +export const OA_SIGNED = { + version: 'https://schema.openattestation.com/3.0/schema.json', + '@context': [ + 'https://www.w3.org/2018/credentials/v1', + 'https://schemata.openattestation.com/com/openattestation/1.0/DrivingLicenceCredential.json', + 'https://schemata.openattestation.com/com/openattestation/1.0/OpenAttestation.v3.json', + 'https://schemata.openattestation.com/com/openattestation/1.0/CustomContext.json', + ], + reference: 'SERIAL_NUMBER_123', + name: 'Republic of Singapore Driving Licence', + issuanceDate: '2010-01-01T19:23:24Z', + validFrom: '2010-01-01T19:23:24Z', + issuer: { + id: 'https://example.com', + type: 'OpenAttestationIssuer', + name: 'DEMO STORE', + }, + type: [ + 'VerifiableCredential', + 'DrivingLicenceCredential', + 'OpenAttestationCredential', + ], + credentialSubject: { + id: 'did:example:SERIAL_NUMBER_123', + class: [ + { + type: '3', + effectiveDate: '2010-01-01T19:23:24Z', + }, + { + type: '3A', + effectiveDate: '2010-01-01T19:23:24Z', + }, + ], + }, + openAttestationMetadata: { + template: { + name: 'CUSTOM_TEMPLATE', + type: 'EMBEDDED_RENDERER', + url: 'https://localhost:3000/renderer', + }, + proof: { + type: 'OpenAttestationProofMethod', + method: 'DID', + value: 'did:ethr:0x1245e5B64D785b25057f7438F715f4aA5D965733', + revocation: { + type: 'NONE', + }, + }, + identityProof: { + type: 'DID', + identifier: 'did:ethr:0x1245e5B64D785b25057f7438F715f4aA5D965733', + }, + }, + attachments: [ + { + fileName: 'sample.pdf', + mimeType: 'application/pdf', + data: 'BASE64_ENCODED_FILE', + }, + ], + proof: { + type: 'OpenAttestationMerkleProofSignature2018', + proofPurpose: 'assertionMethod', + targetHash: + '82f51d6eb620e4264dff0ac2b9d99a965a88ff51e46192bb4808ea969ee67402', + proofs: [ + 'a1c633145bc0f37105fe510d335376c1919a6cf51030628877288bdee5541c22', + 'c3d7c5908f25eba67baf7f607932f1924acdb7a6cf04ad5408dba251bf0a47bc', + '94c07ddcc4a2ade59e3120dce9f19f0f4ad80a58943555f4b51af4668b1c1d62', + ], + merkleRoot: + 'f43045b0c57072a044e810b798e32b8c1de1d0d0c5774d55c8eed1f3fdec6438', + salts: + 'W3sidmFsdWUiOiIyMTJjMDNmYmIzNmMyMjY2NTU0OGM5OGM5ZjE2ZTYwYTc2MjBjZGM0ZjczMjY3NDMxYzA0ZjYzY2U0MTYxMmVkIiwicGF0aCI6InZlcnNpb24ifSx7InZhbHVlIjoiMDRjZTAzMDliNWU3NTNmOTFiMjdmNmE2Nzg2N2VkZWY2OTY3ZmU4YzAyZmEwNTE5ZjY3Mzc3NDI1ZWI5OTE5ZiIsInBhdGgiOiJAY29udGV4dFswXSJ9LHsidmFsdWUiOiIzNWQ2Njg2YTZjYTI1NmQ3NWRmMzE2YmJlZjUxMjYwYzdiZGIxZWJkODRlZTM3OTM3OTI1MGYxYTVkNjk3ZGRlIiwicGF0aCI6IkBjb250ZXh0WzFdIn0seyJ2YWx1ZSI6IjMyZjIyZjJmMzQwYTgwNmRkMTE0NzQ3NjQ3ZTkwZjlmNjNjNmY4ZDUyMzk5YzI0OWVhN2U4YWZmMWVhNjA3MGUiLCJwYXRoIjoiQGNvbnRleHRbMl0ifSx7InZhbHVlIjoiMGE2NWFkNjQxZmRjYTkxNTdmMmQ5M2UyNmUzZTM4NmU3MTg3NTQyNWI0M2E4OTZkNmJlNWMxY2Q0MTkwNTFhOCIsInBhdGgiOiJAY29udGV4dFszXSJ9LHsidmFsdWUiOiJhZjEzZWMzODgyM2VkODIyZmNjY2Q5YWY2YWRmOWUxNWFjZWIzZjNmZmE2Njk2OGMyZjhmMjk2MzYxNDRhNzYwIiwicGF0aCI6InJlZmVyZW5jZSJ9LHsidmFsdWUiOiI4MDAxNjA5OTUzZmFiYzZjZTZjOGIyOTVmMDdjNTM2MDhhMjQ0ZWIxMmFmMzJlZjQyYjZmYTc1MmFmNDAzOTYxIiwicGF0aCI6Im5hbWUifSx7InZhbHVlIjoiMzc5NDQyY2NiZWMyMTYxNWZmNzM4MDQ2MmEzZjlmYmUyZjc1MmQ5M2IwZGQyZjRjNTM4MGJlYWZmZDVlMjA3OSIsInBhdGgiOiJpc3N1YW5jZURhdGUifSx7InZhbHVlIjoiN2M0MzYyMmNjMTk2ZDJmOWQ5NDU2NjM3NWU2NzRlODk2OTNkYjRjZDA5YjE1MjI0MjJjODIyZGYxN2EyYjI0ZCIsInBhdGgiOiJ2YWxpZEZyb20ifSx7InZhbHVlIjoiNTIwN2M1NGY1MWNhMzlmYWJmZWM3NmFlMDU3YzEzYTc3Y2Q1YTQ3YTVkYTZkYzcwMGQwYjVkNzZhZDQxOTYwNiIsInBhdGgiOiJpc3N1ZXIuaWQifSx7InZhbHVlIjoiODExZWU1MmE0MTQ0MTMzNjc4OWEwOWYzNjIwODcwYWVmNTAzNzEzYzE4YTFlNjY2YzU1MTk5ZjhiYTY5ODRlNCIsInBhdGgiOiJpc3N1ZXIudHlwZSJ9LHsidmFsdWUiOiJiOWUzZDllYjNhYzUwMThjNTEzMDFjNjNkNTIyMWMzY2M3NTZlOGFjYzcxYmY4ZmQxMjlhYmMyNDA5OGRhNzRmIiwicGF0aCI6Imlzc3Vlci5uYW1lIn0seyJ2YWx1ZSI6Ijc1M2FjYTQ3NmI4YWRhNjBjNjQ2ZmUwZjhmMTNiNjhlMzBkMDU5NGVkY2NkYTAxMDJmZTVmMmI0OGRmY2ZlZjYiLCJwYXRoIjoidHlwZVswXSJ9LHsidmFsdWUiOiJjMWY4NDM1YmI4YzQxMzkyMDFlMGY1ZjYwZDkwMDI1M2RkNmJhZDJkODljYjg4N2JlNzdkNmRjYTQ3NmQxMzMxIiwicGF0aCI6InR5cGVbMV0ifSx7InZhbHVlIjoiZWY2MTllMDAzNWRkYThlODhlNTQwZTExMjIyOTk0ZjM4YTM0NTBhYThjM2I5ZjQ1MDFmMmQ3ZmFmN2UzYTNhYiIsInBhdGgiOiJ0eXBlWzJdIn0seyJ2YWx1ZSI6ImFjODRlNjUzMjNhMWQ1MjdiNTQzMmQ3NmM5NWMxOWQ3ODM3MzExOTg1YzY0ODg1NjY1ZDU2NTEyYTE2MDI3NWIiLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3QuaWQifSx7InZhbHVlIjoiMDhiYWI3ZGY4YzA1N2JlZGUwYzE4NTA3MTcwMjU5NjkwMDg5MjM0ZDE1NGQyNzlhMThiMzExY2YwZTUyZDVlNCIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC5jbGFzc1swXS50eXBlIn0seyJ2YWx1ZSI6IjFhZDhhYmQyMzhhNmE5NzhjYzdlMmE0NzIwN2FhYjRlZjM1Njc3MDAwMTlmNDg0YzMzMTlkYTI3YzAzMjIwNTEiLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3QuY2xhc3NbMF0uZWZmZWN0aXZlRGF0ZSJ9LHsidmFsdWUiOiJiMzdjMzBjNGUzMGM1ZGI4MGZiMzYxMGFkM2U0NmZmMzNmNTc2ZTQ3MGIzZjNjNmIzMDI4ZGUzYWYwNTRjOTVkIiwicGF0aCI6ImNyZWRlbnRpYWxTdWJqZWN0LmNsYXNzWzFdLnR5cGUifSx7InZhbHVlIjoiMTVhNDE2YjhjNWI0MmM3N2E4MGMzYmZmMTRlZmY2YWQ3NzMyMTZmYjAzZjc1OTJiMzM5MGIxZThlMDY1NmJjMiIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC5jbGFzc1sxXS5lZmZlY3RpdmVEYXRlIn0seyJ2YWx1ZSI6ImQ5NzgyNmEyMjI0YTI4MDE2NDVhMmZjOTVmYWFiNzU1NjkzOGE4ZTcwZWNmNDY2ZDE5NzlmYTYzODNjZWRmN2UiLCJwYXRoIjoib3BlbkF0dGVzdGF0aW9uTWV0YWRhdGEudGVtcGxhdGUubmFtZSJ9LHsidmFsdWUiOiI5NzIxMmEwMmQ0NGVlZDA5OTA0YzBhZGY2NTkxNzg0OGQ2YmExMjQxMWIwNTI3Y2IzZjNiNDk0NzQ3Nzc5ODI3IiwicGF0aCI6Im9wZW5BdHRlc3RhdGlvbk1ldGFkYXRhLnRlbXBsYXRlLnR5cGUifSx7InZhbHVlIjoiNTRjZDQ5ZmMxMGUxZjdhN2RmOTE2ZmQ0OGQwNTAzYmY4ODU5NjNhMTZkN2U2MDFiNTFlOWMwZGQxNjkxMTEwOCIsInBhdGgiOiJvcGVuQXR0ZXN0YXRpb25NZXRhZGF0YS50ZW1wbGF0ZS51cmwifSx7InZhbHVlIjoiZDBiMzc0ZDlkZWQxNmZkNDkyZmVlYmZlMTZlOWQ0MWMwMWRhNTgxMmI5NjQ3MmNiNWEzOWQ2MTA5M2JhYjNiNCIsInBhdGgiOiJvcGVuQXR0ZXN0YXRpb25NZXRhZGF0YS5wcm9vZi50eXBlIn0seyJ2YWx1ZSI6IjdkNzRkMjUwZjRhNGU1ZDY5Y2RiM2YxNTJhNmJlNTVhYzNjMWYwYWYxZDMzY2RlNTYxMzBjODc1OTIwYWYxMDMiLCJwYXRoIjoib3BlbkF0dGVzdGF0aW9uTWV0YWRhdGEucHJvb2YubWV0aG9kIn0seyJ2YWx1ZSI6IjUxYmFhY2RjZGM5ZTk4NTg0NTkyMDU2MDhmM2Q5Y2YwYmI5YTI1NmI0ZGUxOGIxMWJlNjU0OTMyN2I5MGUxMzgiLCJwYXRoIjoib3BlbkF0dGVzdGF0aW9uTWV0YWRhdGEucHJvb2YudmFsdWUifSx7InZhbHVlIjoiNzFjZThjMjM3ZGZjNjkxYzBlZDBlMTRkYWM5YmIzOTg5OTJhNWQwNGM3MDlmM2EwNjI0NDE1MTNlZjYxNDhlYiIsInBhdGgiOiJvcGVuQXR0ZXN0YXRpb25NZXRhZGF0YS5wcm9vZi5yZXZvY2F0aW9uLnR5cGUifSx7InZhbHVlIjoiMjQwNWY4NmE5NGNkY2Y5NzExYzI0NzRlZTBmMDc5MDRiNThmZmRmYjNhNjMyMWNjYWEwMDRhZGJlZmIxMjRmZiIsInBhdGgiOiJvcGVuQXR0ZXN0YXRpb25NZXRhZGF0YS5pZGVudGl0eVByb29mLnR5cGUifSx7InZhbHVlIjoiOTdlZmE5MjdlMTVmMGFlYmIxNmEyOWY3NjM2YTVlZmRjYmNhYWU5YzdhM2MwMGFmZDczZmM3MDUzMzNhMjZmOCIsInBhdGgiOiJvcGVuQXR0ZXN0YXRpb25NZXRhZGF0YS5pZGVudGl0eVByb29mLmlkZW50aWZpZXIifSx7InZhbHVlIjoiZTUzYWQ0ODk1Mjg3YTExZjE1MzRiNDRmNDI5ZDgyOTlkZGFmOTkzMTNjZGE1MGE2ZTAyMTE0ZTc2Yjg0NGY4ZSIsInBhdGgiOiJhdHRhY2htZW50c1swXS5maWxlTmFtZSJ9LHsidmFsdWUiOiIxNDU0MGY2ZGViMDBjOGI2OTc3MmE4ZTljNzU1OGU0MTgxMGI1Y2FmYTQzNGQ1NWFjMTIxZTk3MmE1NDY0MDMzIiwicGF0aCI6ImF0dGFjaG1lbnRzWzBdLm1pbWVUeXBlIn0seyJ2YWx1ZSI6IjNiZTUyODM3ZjMwNzcyNDYzZjY1NmM4ZDQ3ZGM1ZWRiZjcyMmY4ZDQ5ZWFiZTI0NWVjZGM2ZWJhODIxNzFjYTAiLCJwYXRoIjoiYXR0YWNobWVudHNbMF0uZGF0YSJ9XQ==', + privacy: { + obfuscated: [], + }, + key: 'did:ethr:0x1245e5B64D785b25057f7438F715f4aA5D965733#controller', + signature: + '0x4a054aaf881da5130a3b19160a7f0ce1afef1fe093c83be438c8c40b0a04ace5142f3d48a23c8ce7c32fabe04fc69e6fd4c94cf3ceb4adb4f3da5fa937208d4e1c', + }, +}; + +export const NON_OA_CREDENTIAL = { + version: 'https://schema.openattestation.com/3.0/schema.json', + '@context': [ + 'https://www.w3.org/2018/credentials/v1', + 'https://schemata.openattestation.com/com/openattestation/1.0/DrivingLicenceCredential.json', + 'https://schemata.openattestation.com/com/openattestation/1.0/OpenAttestation.v3.json', + 'https://schemata.openattestation.com/com/openattestation/1.0/CustomContext.json', + ], + reference: 'SERIAL_NUMBER_123', + name: 'Republic of Singapore Driving Licence', + issuanceDate: '2010-01-01T19:23:24Z', + validFrom: '2010-01-01T19:23:24Z', + issuer: { + id: 'https://example.com', + type: 'OpenAttestationIssuer', + name: 'DEMO STORE', + }, + type: ['VerifiableCredential', 'DrivingLicenceCredential'], + credentialSubject: { + id: 'did:example:SERIAL_NUMBER_123', + class: [ + { + type: '3', + effectiveDate: '2010-01-01T19:23:24Z', + }, + { + type: '3A', + effectiveDate: '2010-01-01T19:23:24Z', + }, + ], + }, + openAttestationMetadata: { + template: { + name: 'CUSTOM_TEMPLATE', + type: 'EMBEDDED_RENDERER', + url: 'https://localhost:3000/renderer', + }, + proof: { + type: 'OpenAttestationProofMethod', + method: 'DID', + value: 'did:ethr:0x1245e5B64D785b25057f7438F715f4aA5D965733', + revocation: { + type: 'NONE', + }, + }, + identityProof: { + type: 'DID', + identifier: 'did:ethr:0x1245e5B64D785b25057f7438F715f4aA5D965733', + }, + }, + attachments: [ + { + fileName: 'sample.pdf', + mimeType: 'application/pdf', + data: 'BASE64_ENCODED_FILE', + }, + ], +}; diff --git a/libs/server-common/src/fixtures/oav2/did-revocation-ocsp-signed.json b/libs/server-common/src/fixtures/oav2/did-revocation-ocsp-signed.json new file mode 100644 index 00000000..27dbe359 --- /dev/null +++ b/libs/server-common/src/fixtures/oav2/did-revocation-ocsp-signed.json @@ -0,0 +1,68 @@ +{ + "version": "https://schema.openattestation.com/2.0/schema.json", + "data": { + "id": "119450a6-e966-4cfc-b422-46a15944e5aa:string:SGCNM21566327", + "recipient": { + "name": "bec209c5-591b-4221-af7c-b30ac645e807:string:AUS FREIGHT", + "address": { + "street": "f1a3c0c6-154b-4749-9f86-a52da8ada385:string:101 APPLE ROAD", + "country": "6e12ae04-f675-4f58-9558-6a389765bf02:string:AUSTRALIA" + } + }, + "consignment": { + "description": "2dc3a053-21af-4fce-8e89-8210aa14123f:string:16667 CARTONS OF RED WINE", + "quantity": { + "value": "7ab472a4-7ff0-4a6b-b8d7-1f10e27c6056:string:5000", + "unit": "9c6169b6-9c69-46c2-add1-b269dd3f5b52:string:LITRES" + }, + "countryOfOrigin": "899126dd-df52-409a-9e3b-c7bdf8263715:string:AUSTRALIA", + "outwardBillNo": "a4575114-eef0-406f-b5b3-d4af37695928:string:AQSIQ170923150", + "dateOfDischarge": "ddc1931f-f51e-469e-9cde-989c9098e04d:string:2018-01-26", + "dateOfDeparture": "68066b60-1d0a-4a8a-b3b6-b39e34841253:string:2018-01-30", + "countryOfFinalDestination": "ad80faa3-6db3-41fd-bac3-767f26ea8afe:string:CHINA", + "outgoingVehicleNo": "c32dd0e2-5324-474d-8bf5-ad1ec6a62771:string:COSCO JAPAN 074E/30-JAN" + }, + "declaration": { + "name": "e0fffed5-98f0-48a0-be58-cf52badd1efd:string:PETER LEE", + "designation": "409088a3-e414-4df4-855d-8cb8999b0b09:string:SHIPPING MANAGER", + "date": "b5346ea9-55d3-4eee-ae4c-4add815bfec4:string:2018-01-28" + }, + "$template": { + "name": "803c4074-0750-4cb9-9faf-1468fb2fe011:string:CERTIFICATE_OF_NON_MANIPULATION", + "type": "cf6b9e55-e119-44e8-97b5-3b4b5399b359:string:EMBEDDED_RENDERER", + "url": "5f922b43-7780-4b88-8bd6-8fc68cdc6884:string:https://demo-cnm.openattestation.com" + }, + "issuers": [ + { + "id": "a0142f9f-1bdc-45db-acb8-afa775409e36:string:did:ethr:0xE712878f6E8d5d4F9e87E10DA604F9cB564C9a89", + "name": "59e59aea-026f-449c-83b6-04c6c89d5985:string:Demo Issuer", + "revocation": { + "type": "8f78272a-0dab-4b36-915b-7447e45d1c7b:string:OCSP_RESPONDER", + "location": "7acf98ff-53f4-492d-90e3-c2db3f757565:string:https://www.ica.gov.sg/ocsp" + }, + "identityProof": { + "type": "bdfa535a-2479-4480-8914-d285fe3db418:string:DNS-DID", + "location": "717ca05b-b154-4fff-95d7-b0e47705eb02:string:example.tradetrust.io", + "key": "a88609e2-f020-440c-bf34-f569457e903c:string:did:ethr:0xE712878f6E8d5d4F9e87E10DA604F9cB564C9a89#controller" + } + } + ] + }, + "signature": { + "type": "SHA3MerkleProof", + "targetHash": "4d26a49266ba73f57276b0865d995c4c6ae8be52fe54988e85b4cbf222f49e74", + "proof": [ + "bbe0afee0378a14d947e16f6e850f6b50f41218e4dc0e39af8043ac802b550b7" + ], + "merkleRoot": "53b4a76854688ee7857442d01f33d1805e3a237377fd0e5d53a43cda30dd742c" + }, + "proof": [ + { + "type": "OpenAttestationSignature2018", + "created": "2021-10-28T07:58:41.042Z", + "proofPurpose": "assertionMethod", + "verificationMethod": "did:ethr:0xE712878f6E8d5d4F9e87E10DA604F9cB564C9a89#controller", + "signature": "0x869f6956092f78d2e56e2bee67635fedcdfb6cfd5825f5a88ed1102cf15744ce4bddf629fa6a10dd739711c6c8dc79589038b59d99483a00eedd43cca219b6b61c" + } + ] +} diff --git a/libs/server-common/src/fixtures/oav2/did-revocation-store-signed-no-location.json b/libs/server-common/src/fixtures/oav2/did-revocation-store-signed-no-location.json new file mode 100644 index 00000000..1b0e2f67 --- /dev/null +++ b/libs/server-common/src/fixtures/oav2/did-revocation-store-signed-no-location.json @@ -0,0 +1,64 @@ +{ + "version": "https://schema.openattestation.com/2.0/schema.json", + "data": { + "id": "0d68bfdc-be22-4f3b-b93a-9be49b005eec:string:SGCNM21566325", + "$template": { + "name": "821ffc56-91b6-47c7-9137-a6e928eda8f3:string:CERTIFICATE_OF_NON_MANIPULATION", + "type": "8b70d29e-f3b7-4e8b-85cd-cbb50a31a530:string:EMBEDDED_RENDERER", + "url": "672d733e-8d15-400c-acd2-055da7f47ea0:string:https://demo-cnm.openattestation.com" + }, + "issuers": [ + { + "id": "678c27ca-0555-4333-b3c6-9c598bfe98a6:string:did:ethr:0xE712878f6E8d5d4F9e87E10DA604F9cB564C9a89", + "name": "5b8fe517-4f4f-48d1-a007-b3007e130220:string:DEMO STORE", + "revocation": { + "type": "20b3bb4d-e9fe-4479-8f22-165dd228d4c7:string:REVOCATION_STORE" + }, + "identityProof": { + "type": "1f01cb81-e107-430c-805c-88a0687f26d7:string:DID", + "key": "d9785ba2-08b9-4a15-8ad1-ba2746ea1814:string:did:ethr:0xE712878f6E8d5d4F9e87E10DA604F9cB564C9a89#controller" + } + } + ], + "recipient": { + "name": "eab9094a-f4ab-499b-9a1e-0c480784f0f1:string:SG FREIGHT", + "address": { + "street": "e56867b5-e1b9-4a0d-91a2-7c23896eda06:string:101 ORCHARD ROAD", + "country": "e74b558b-d937-4478-88fb-5f1d8fb718e4:string:SINGAPORE" + } + }, + "consignment": { + "description": "9bc9cf4d-8bd9-4d67-8519-ae92f8463b29:string:16667 CARTONS OF RED WINE", + "quantity": { + "value": "a0811fd0-9fd0-485e-a91b-f3290aa16777:number:5000", + "unit": "381c64e1-670b-49dc-b238-800df31a8238:string:LITRES" + }, + "countryOfOrigin": "e6f1c08e-bc12-4caf-88d0-c6e8d5693f8f:string:AUSTRALIA", + "outwardBillNo": "a6ffd1c7-c42d-40df-9c39-c6b2044c574c:string:AQSIQ170923150", + "dateOfDischarge": "89e1172f-0a39-4887-855a-51f9dfe08e1f:string:2018-01-26", + "dateOfDeparture": "8df514b9-c48f-4a96-a1ac-d3e0ae7c50ee:string:2018-01-30", + "countryOfFinalDestination": "3d6c734e-d7c8-433a-a4a3-8213d3140f4b:string:CHINA", + "outgoingVehicleNo": "42a2fd22-977f-48ad-ad86-9a0d28541389:string:COSCO JAPAN 074E/30-JAN" + }, + "declaration": { + "name": "d8f1f66b-88f0-42c5-8b66-8429f2bc78a2:string:PETER LEE", + "designation": "30d17b4e-274c-49b2-80d5-5c1cfd1af15d:string:SHIPPING MANAGER", + "date": "6462d1dd-9d6d-423f-b43d-63c1c3c12866:string:2018-01-28" + } + }, + "signature": { + "type": "SHA3MerkleProof", + "targetHash": "b6db3e59d4354116451b603abc194f146df23946f637e9b25c3899a7697ed121", + "proof": [], + "merkleRoot": "b6db3e59d4354116451b603abc194f146df23946f637e9b25c3899a7697ed121" + }, + "proof": [ + { + "type": "OpenAttestationSignature2018", + "created": "2021-02-17T02:06:22.110Z", + "proofPurpose": "assertionMethod", + "verificationMethod": "did:ethr:0xE712878f6E8d5d4F9e87E10DA604F9cB564C9a89#controller", + "signature": "0x4cced3e9865ab16f6a32464e73f32fca6ed42e377bdc36fe2e049716cd2bdc812db49a3540f93ecc615c98518afcb7602c133d261d6bdb8f6bded799964241d21b" + } + ] +} diff --git a/libs/server-common/src/fixtures/oav2/did-revocation-store-signed-not-revoked.json b/libs/server-common/src/fixtures/oav2/did-revocation-store-signed-not-revoked.json new file mode 100644 index 00000000..bae06f8b --- /dev/null +++ b/libs/server-common/src/fixtures/oav2/did-revocation-store-signed-not-revoked.json @@ -0,0 +1,65 @@ +{ + "version": "https://schema.openattestation.com/2.0/schema.json", + "data": { + "id": "72eb4a82-fa01-4c8f-8f7d-1f6180cf51c9:string:SGCNM21566325", + "$template": { + "name": "984627c6-ba6d-48fd-a334-399f98db24ca:string:CERTIFICATE_OF_NON_MANIPULATION", + "type": "3d092147-9c75-45cb-a61e-524e89182e83:string:EMBEDDED_RENDERER", + "url": "294a26db-61e7-48c0-ba40-c114498bada3:string:https://demo-cnm.openattestation.com" + }, + "issuers": [ + { + "id": "74f34c92-9546-4ad6-8e53-18ae5038f115:string:did:ethr:0xE712878f6E8d5d4F9e87E10DA604F9cB564C9a89", + "name": "0c03ace1-0798-4409-8904-dae660b009bc:string:DEMO STORE", + "revocation": { + "type": "ab820123-35e7-4ed6-be5e-bda31d574c1b:string:REVOCATION_STORE", + "location": "893b3a91-a829-4b49-a6e4-d33aa48d4104:string:0x8bA63EAB43342AAc3AdBB4B827b68Cf4aAE5Caca" + }, + "identityProof": { + "type": "3606cabd-4730-48ed-ac5f-4c29a0c2b3d0:string:DID", + "key": "305d26b4-c4cf-49ce-b2b3-16940b3278af:string:did:ethr:0xE712878f6E8d5d4F9e87E10DA604F9cB564C9a89#controller" + } + } + ], + "recipient": { + "name": "62687bfd-b2bf-4212-b5f7-2a086079a308:string:AUS FREIGHT", + "address": { + "street": "9ff1ef9d-2a8c-48f0-a892-15696f32e766:string:101 APPLE ROAD", + "country": "5bdd81d2-36a0-4318-8ac5-b375fea68fce:string:AUSTRALIA" + } + }, + "consignment": { + "description": "40e36535-c8c6-47d3-8935-2def709639cf:string:16667 CARTONS OF RED WINE", + "quantity": { + "value": "780f0837-f8cb-4f60-87e1-8261add6f16a:number:5000", + "unit": "2556742d-9f78-4f6f-b426-e49656977da2:string:LITRES" + }, + "countryOfOrigin": "52153780-c5c4-487d-8b99-46f6dc7c4141:string:AUSTRALIA", + "outwardBillNo": "b6d6be17-df63-4b65-9aa8-5dfd78d2cd7a:string:AQSIQ170923150", + "dateOfDischarge": "32231fa7-e42b-4a11-8259-24f496ff4e1a:string:2018-01-26", + "dateOfDeparture": "a2c15153-da9d-484d-bc7f-8a9a564b7fed:string:2018-01-30", + "countryOfFinalDestination": "b6140f05-2a83-419f-9891-57e4c623550d:string:CHINA", + "outgoingVehicleNo": "da56b7f8-b7c4-4798-88e9-b535e374ed8b:string:COSCO JAPAN 074E/30-JAN" + }, + "declaration": { + "name": "f6f7ebcb-c612-4117-9530-df12e0f3af88:string:PETER LEE", + "designation": "31dfb266-3536-4150-94cf-fc0372a7dcce:string:SHIPPING MANAGER", + "date": "f9ef39f6-48b2-4a55-846b-4c88e11b318f:string:2018-01-28" + } + }, + "signature": { + "type": "SHA3MerkleProof", + "targetHash": "4799fa5f133c93f83f95799e7af4a63428ce1a96ed6a00688c7f9defc94346a3", + "proof": [], + "merkleRoot": "4799fa5f133c93f83f95799e7af4a63428ce1a96ed6a00688c7f9defc94346a3" + }, + "proof": [ + { + "type": "OpenAttestationSignature2018", + "created": "2021-02-10T07:31:35.429Z", + "proofPurpose": "assertionMethod", + "verificationMethod": "did:ethr:0xE712878f6E8d5d4F9e87E10DA604F9cB564C9a89#controller", + "signature": "0x0d8e657bd714bfc55ca0238d037c62123f3e2f251b88b13afd4c5224cd69c76b751913399deed7899115ac70cd78908527dfe3a987459a20dd803ecc5720668c1c" + } + ] +} diff --git a/libs/server-common/src/fixtures/oav2/did-revocation-store-signed-revoked.json b/libs/server-common/src/fixtures/oav2/did-revocation-store-signed-revoked.json new file mode 100644 index 00000000..196e06fc --- /dev/null +++ b/libs/server-common/src/fixtures/oav2/did-revocation-store-signed-revoked.json @@ -0,0 +1,65 @@ +{ + "version": "https://schema.openattestation.com/2.0/schema.json", + "data": { + "id": "f0d3a8eb-eacb-40b1-9438-fe95887a504c:string:SGCNM21566325", + "$template": { + "name": "076ca2d1-a172-4e6e-af99-a6e045616ed9:string:CERTIFICATE_OF_NON_MANIPULATION", + "type": "f120703a-48f9-45e2-a473-58392d1607a3:string:EMBEDDED_RENDERER", + "url": "63086fcc-e972-4f2a-b381-7cf79568cb36:string:https://demo-cnm.openattestation.com" + }, + "issuers": [ + { + "id": "83e31480-7867-4849-b73d-639632672dce:string:did:ethr:0xE712878f6E8d5d4F9e87E10DA604F9cB564C9a89", + "name": "e682b4d1-f659-40db-a346-57ebd4303b47:string:DEMO STORE", + "revocation": { + "type": "7e35d9b5-9ac8-45b8-b725-19859c028f9e:string:REVOCATION_STORE", + "location": "b8b7c529-ad71-4e27-a3b1-2bc1654ed87e:string:0x8bA63EAB43342AAc3AdBB4B827b68Cf4aAE5Caca" + }, + "identityProof": { + "type": "778b2d3e-6125-4a39-8b87-23f1fdaf458b:string:DID", + "key": "b55daa38-cecf-4015-990e-d1a0e267c939:string:did:ethr:0xE712878f6E8d5d4F9e87E10DA604F9cB564C9a89#controller" + } + } + ], + "recipient": { + "name": "e4e7f5de-10b0-4859-ab8b-ae47dd0abf8e:string:SG FREIGHT", + "address": { + "street": "cf5c882b-b3c4-4054-adea-90e1ca585b9f:string:101 ORCHARD ROAD", + "country": "b6e091b4-7488-42cd-b264-61bc5efae948:string:SINGAPORE" + } + }, + "consignment": { + "description": "b9dfb54d-a117-4c4f-bb84-96d2cd7e6f46:string:16667 CARTONS OF RED WINE", + "quantity": { + "value": "8124e260-26c8-4423-81e8-c032c411e23a:number:5000", + "unit": "84d531af-92af-491e-91b1-276ff2b335b7:string:LITRES" + }, + "countryOfOrigin": "f3678611-41da-4a69-abd8-20047d023bf4:string:AUSTRALIA", + "outwardBillNo": "d849715c-5355-4588-9ef3-99f14e5127ce:string:AQSIQ170923150", + "dateOfDischarge": "1c2c70cf-dff0-47d8-928e-348299147284:string:2018-01-26", + "dateOfDeparture": "64635c75-a920-4b2a-a4f8-1c9027768e03:string:2018-01-30", + "countryOfFinalDestination": "df8a189e-62da-4016-8a1b-bd449b883d9f:string:CHINA", + "outgoingVehicleNo": "eda15d4d-f75f-4ad8-bd82-6b3f04e5982c:string:COSCO JAPAN 074E/30-JAN" + }, + "declaration": { + "name": "f448ed4a-8c8b-4955-8812-daa5484f4b67:string:PETER LEE", + "designation": "15f04d46-830b-401e-a2a7-f5537a9e0df8:string:SHIPPING MANAGER", + "date": "a395955f-ec21-45db-83d4-f7c90c1ee082:string:2018-01-28" + } + }, + "signature": { + "type": "SHA3MerkleProof", + "targetHash": "65f1e3c2a042dc9648f26f08257fd47a3739e40606d2dc887fe7566c8290144c", + "proof": [], + "merkleRoot": "65f1e3c2a042dc9648f26f08257fd47a3739e40606d2dc887fe7566c8290144c" + }, + "proof": [ + { + "type": "OpenAttestationSignature2018", + "created": "2021-02-10T07:31:35.422Z", + "proofPurpose": "assertionMethod", + "verificationMethod": "did:ethr:0xE712878f6E8d5d4F9e87E10DA604F9cB564C9a89#controller", + "signature": "0xd5abc45e61c89a5cddffe470b8f34a32f4ceb40d3596cd517f6ac6717a12e5a316ff2147846d4f81d13a9b4910e575569412f17b9a1cc251d5e385e14669bd051b" + } + ] +} diff --git a/libs/server-common/src/fixtures/oav3/broken.json b/libs/server-common/src/fixtures/oav3/broken.json new file mode 100644 index 00000000..5367a4aa --- /dev/null +++ b/libs/server-common/src/fixtures/oav3/broken.json @@ -0,0 +1,75 @@ +{ + "version": "https://schema.openattestation.com/3.0/schema.json", + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://schemata.openattestation.com/com/openattestation/1.0/DrivingLicenceCredential.json", + "https://schemata.openattestation.com/com/openattestation/1.0/OpenAttestation.v3.json", + "https://schemata.openattestation.com/com/openattestation/1.0/CustomContext.json" + ], + "reference": "SERIAL_NUMBER_123", + "name": "Republic of Singapore Driving Licence", + "issuanceDate": "2010-01-01T19:23:24Z", + "validFrom": "2010-01-01T19:23:24Z", + "ishooAhhh": { + "id": "https://example.com", + "type": "OpenAttestationIsuer", + "name": "DEMO STORE" + }, + "type": [ + "VerifiableCredential", + "DrivingLicenceCredential", + "OpenAttestationCredential" + ], + "credentialSubj": { + "id": "did:example:SERIAL_NUMBER_123", + "class": [ + { + "type": "3", + "effectiveDate": "2010-01-01T19:23:24Z" + }, + { + "type": "3A", + "effectiveDate": "2010-01-01T19:23:24Z" + } + ] + }, + "openAttestationMetadata": { + "template": { + "name": "CUSTOM_TEMPLATE", + "type": "EMBEDDED_RENDERER", + "url": "https://localhost:3000/renderer" + }, + "proof": { + "type": "OpenAttestationProofMethod", + "method": "DID", + "value": "did:ethr:0xB26B4941941C51a4885E5B7D3A1B861E54405f90", + "revocation": { + "type": "NONE" + } + }, + "identityProof": { + "type": "DID", + "identifier": "did:ethr:0xB26B4941941C51a4885E5B7D3A1B861E54405f90" + } + }, + "attachments": [ + { + "fileName": "sample.pdf", + "mimeType": "application/pdf", + "data": "BASE64_ENCODED_FILE" + } + ], + "proof": { + "type": "OpenAttestationMerkleProofSignature2018", + "proofPurpose": "assertionMethod", + "targetHash": "c2d9eec66e7d1d7a01d7baf759c1182f7c467c5157c5cb995573d2268bdf10de", + "proofs": [], + "merkleRoot": "c2d9eec66e7d1d7a01d7baf759c1182f7c467c5157c5cb995573d2268bdf10de", + "salts": "W3sidmFsdWUiOiJlNDVkYjU0MTEwN2RkMDQyMTgyYmE3MWI3YTNmY2M5YWMwZjhjZTRkOTkwMDE3OGQ0YzVlY2NmZmVmN2NhNzBjIiwicGF0aCI6InZlcnNpb24ifSx7InZhbHVlIjoiMzI3ZjA3M2Y1YTMxZTg2MTI5Mzk2MDliNThjNzU5ZGIxNWIwZmVkYjNkZGMyZTI0OTA3OTFhNmUwZGNkNDRmYiIsInBhdGgiOiJAY29udGV4dFswXSJ9LHsidmFsdWUiOiIyZjg3OTA1OTc4ZWJlZTlmMDY5NDk4MDFlOWY2YzEzNjY4YWY4MDc0MTkwMTg3Y2Q2ZDQ3MDYwMjkzNzQwODNiIiwicGF0aCI6IkBjb250ZXh0WzFdIn0seyJ2YWx1ZSI6IjYwZjFkNDMyYmRmYmQ1MDZmZmEzNTgyMmU4MmNmODQ2YTFkMTg2ZjExNTU3NzkxODFhZDk1Mzc4MjVmNWE0YTIiLCJwYXRoIjoiQGNvbnRleHRbMl0ifSx7InZhbHVlIjoiMGMyMWIxM2Y5MDc2N2Y3NmYxMDkzNjZhMWQxOTIzZmZkZThiMGZmZDNlNTdiYTJhODA4YmRjNzM4YjVjZmZjOSIsInBhdGgiOiJAY29udGV4dFszXSJ9LHsidmFsdWUiOiJiMzViNGM5MThmZjcyOGU1ZDVkYTA0NTAzMmY0ZmY0NGQ0M2ZiYmFhYjhlNDc4NGMwZDMzMjU4N2NkYmVlNTYxIiwicGF0aCI6InJlZmVyZW5jZSJ9LHsidmFsdWUiOiI2ZWVhYTc2NmFiNzU0ZDk3ZGI1ZmQ2MzcyZDI1NDBiZTU2ODhiYWM3ZjZiNGQwOTE1NGM2YWZmNDRhNjM5YjNmIiwicGF0aCI6Im5hbWUifSx7InZhbHVlIjoiN2NiNmIyYjE4ODg2OGE4OTk3ZGE5MjI5MjZmNDUzMDQ2Zjg1YzBmYzYzYWUzMDU5MzUzNjBmMGNjMGM4OWY4NCIsInBhdGgiOiJpc3N1YW5jZURhdGUifSx7InZhbHVlIjoiMTU0MGM5MDc5NmZhZjY4YTdhNTIyYjU3ZjJmZDE2NmU3M2JjNzgxZWNjMzRiY2Y2M2ZiYmFjNGU5YjZhMjk2MCIsInBhdGgiOiJ2YWxpZEZyb20ifSx7InZhbHVlIjoiYTMxMzI1YjU1YzY3NmNmNDM3NzQ4YWYxY2RlMTQ5ZGMwMmFhNmNlMjE1YjNlYjkxNjU5ZDJlYjc1OWI1ZjA3ZiIsInBhdGgiOiJpc3N1ZXIuaWQifSx7InZhbHVlIjoiMjg1NGZjM2I3ZmFhODYyODE5MjAwOWUwMGNhOTI1N2U0NDViMGMyZTE1MjJhZDZjYTc5ODE4Y2FlMWExMGFiMSIsInBhdGgiOiJpc3N1ZXIudHlwZSJ9LHsidmFsdWUiOiJjMTIxMmRhZjJlZjJiYzdlZWI3MzgyZjhjOTgwZTRiNmFmNDcwMDliZTlhOGJmZDJmYjVlZTcxOGYwZDkyZWE4IiwicGF0aCI6Imlzc3Vlci5uYW1lIn0seyJ2YWx1ZSI6ImNlMDBiZTk4Nzc4OWVkNWU5MjA4ZDY0ZTAwMzViMmYxNGJiZmYyNjg2ZGQxMzAyNTkyYjA0ZmE1NGQ2ZjM1MjkiLCJwYXRoIjoidHlwZVswXSJ9LHsidmFsdWUiOiJiNjQ5MThlZTQ3ZTZmNDMyZGY4MGVkOTA0M2ZjZDgwZWI5MGQyY2FiYTI5NTc1MGE2ODU1MjlhNGU3Y2MzYzEwIiwicGF0aCI6InR5cGVbMV0ifSx7InZhbHVlIjoiY2NlZjFlZDVlMGViNDM3NDIzYWRhYzBiNWRhYzdkMWY3MjYyMGYwZDA1ZTU4MWQxZTQwZDMyYjc3MjU4NjA0NyIsInBhdGgiOiJ0eXBlWzJdIn0seyJ2YWx1ZSI6ImNhZjQ4MTE5MGMzZTg5NDMyMGIxZjc3ZDY0ODU2YTM3NGQyOGZkYTExZDkzMjZjMjkyZmZiY2IyMzk4OWY4NTYiLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3QuaWQifSx7InZhbHVlIjoiMzVhNDA4MzYyOWYzNjNmMzQ0NzY0MTE4MDIzZTk5M2IwMTkxY2I2OTNjODAwOWVlZGQ3YWI2NmFiZWRmOWY4NCIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC5jbGFzc1swXS50eXBlIn0seyJ2YWx1ZSI6IjExZmNjMmJhZDgyYmU0YzY1YzYyMzZjY2QzMGEzYjFjMmJhMTU3YzVhYmRlZjgxODg4NWJhODU1YTJjNTMyNzUiLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3QuY2xhc3NbMF0uZWZmZWN0aXZlRGF0ZSJ9LHsidmFsdWUiOiIzZDA5NWQ5MzcxNjZkNGQ0NzkzNDUyNzEwN2YyMTBhYmE0ZGQ1ZmZjZTc1NDU1OWU3YmE1ZWU5ZGEyZGQzZDNlIiwicGF0aCI6ImNyZWRlbnRpYWxTdWJqZWN0LmNsYXNzWzFdLnR5cGUifSx7InZhbHVlIjoiMjgwMzUzMDA3NDE4YjFlZTBlZjk3YmM2ZTUxOWNhNTMzNWEyNGJkZWVhMTZkMzdhYzMzYTU0Y2NjMjgyNzNjZiIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC5jbGFzc1sxXS5lZmZlY3RpdmVEYXRlIn0seyJ2YWx1ZSI6IjQ2NmQwNjVlMDgyNmQ1ODlhMGYxMWViNzFjZTIyNDhmOTEzNjAxOWQ0MzBlNmZjMjZhMzQ3NjJhZGVlNGM0NDYiLCJwYXRoIjoib3BlbkF0dGVzdGF0aW9uTWV0YWRhdGEudGVtcGxhdGUubmFtZSJ9LHsidmFsdWUiOiI1YzBmZDYwMzI3MDM0NzViM2IyZDQ0Y2JhMDMwYjU4ZWZiNzQyMDc3MTdmNDljYTcxZDNmYzBiYzM3NTU5ODE0IiwicGF0aCI6Im9wZW5BdHRlc3RhdGlvbk1ldGFkYXRhLnRlbXBsYXRlLnR5cGUifSx7InZhbHVlIjoiMzRjNzU2YzZiYzc2ZmFmNzFjZjU0MTYxOGE4MjNlYmQxN2RhOTZhZjNlMzc2YjFlYTM2NWM5ZDIwNjliZWMzMCIsInBhdGgiOiJvcGVuQXR0ZXN0YXRpb25NZXRhZGF0YS50ZW1wbGF0ZS51cmwifSx7InZhbHVlIjoiYjhjZjNmYTgxZDJiMTE2Mjk1MWViZGE5NjgzZmQzYjA3ZGM1YzVkMDEyZjBhNWNhOGJkNWU4YmY1ZWIwZjliYSIsInBhdGgiOiJvcGVuQXR0ZXN0YXRpb25NZXRhZGF0YS5wcm9vZi50eXBlIn0seyJ2YWx1ZSI6IjY0ODM2ZmE0YmZhMThhMzMwMTEzMGRjMjUxYWZkYTZiY2M3OTY1Y2ExYjk4MjlhMDM5ODY0MDM0ODJlZTE2ZDgiLCJwYXRoIjoib3BlbkF0dGVzdGF0aW9uTWV0YWRhdGEucHJvb2YubWV0aG9kIn0seyJ2YWx1ZSI6IjM3ZjhmODU3ZGU2MzQ0MmI2M2EwZjJjZWMyOTMzZmE0Y2RjMGI4YjNmMGFlZDBiOTdhYWU4ODEzMDg4MTdhZTciLCJwYXRoIjoib3BlbkF0dGVzdGF0aW9uTWV0YWRhdGEucHJvb2YudmFsdWUifSx7InZhbHVlIjoiYmU5NjE0NWYxYjIwNTVlZjQwMDYwOGJkMmE1ZTgxNDdlNzkxNTQ3NTc2ZWNjMzVlNzZlMTEyZmM5NDAzYjAwMCIsInBhdGgiOiJvcGVuQXR0ZXN0YXRpb25NZXRhZGF0YS5wcm9vZi5yZXZvY2F0aW9uLnR5cGUifSx7InZhbHVlIjoiYTExNTNiNWU5MTg0MTE3ZTZkOTUyYjFkYzY0NDViYzg2YWI0ZGJhYjFjMTdlMGQyMWRkM2ZlMjAwNThlODRjZSIsInBhdGgiOiJvcGVuQXR0ZXN0YXRpb25NZXRhZGF0YS5pZGVudGl0eVByb29mLnR5cGUifSx7InZhbHVlIjoiZmE4ZWNhZWQ0OGU1NDZmZThiYTJiN2QzYjMzOTIxZDRhMGIwOTBjZmYyZGVhMjgzZjVhNDVlNzQ1ZmFjYjAyYyIsInBhdGgiOiJvcGVuQXR0ZXN0YXRpb25NZXRhZGF0YS5pZGVudGl0eVByb29mLmlkZW50aWZpZXIifSx7InZhbHVlIjoiODUwMDE4NWViMmM2MzA0Yjk4Y2RkZjQ3YTYwY2U1ODk3OTAxYjQ1MTc4ODFmMTRjN2Q2NmRkMTA4MDM5MTJkZCIsInBhdGgiOiJhdHRhY2htZW50c1swXS5maWxlTmFtZSJ9LHsidmFsdWUiOiIyMjYxYzU3ZmVkNDBhYjg5YTA2Yjg3NzgzNWNjODVjNjk0NTkyOWUwMzUwYmU1ODUyYWJhOTdlNDIzZmM3NGExIiwicGF0aCI6ImF0dGFjaG1lbnRzWzBdLm1pbWVUeXBlIn0seyJ2YWx1ZSI6ImYzYWZmYmZlY2VmYmZhZjcwN2ZhNGQyMDQwYTI5YzZhZTQwYzQ3YTdiZGZkYTU2MTc2YWE2MTNkM2E1NjZhNGQiLCJwYXRoIjoiYXR0YWNobWVudHNbMF0uZGF0YSJ9XQ==", + "privacy": { + "obfuscated": [] + }, + "key": "did:ethr:0xB26B4941941C51a4885E5B7D3A1B861E54405f90#controller", + "signature": "0xa1baa430d937e29790880670e6d17bae1468cf25e22780a2b69e88d2b154d0c51b7d379ac3fbb3d5433dcd42abe03a38fe54a0d57670e02af70d52d25c7f666a1b" + } +} diff --git a/libs/server-common/src/fixtures/oav3/did-invalid-signed.json b/libs/server-common/src/fixtures/oav3/did-invalid-signed.json new file mode 100644 index 00000000..574c7927 --- /dev/null +++ b/libs/server-common/src/fixtures/oav3/did-invalid-signed.json @@ -0,0 +1,70 @@ +{ + "version": "https://schema.openattestation.com/3.0/schema.json", + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://schemata.openattestation.com/com/openattestation/1.0/DrivingLicenceCredential.json", + "https://schemata.openattestation.com/com/openattestation/1.0/OpenAttestation.v3.json", + "https://schemata.openattestation.com/com/openattestation/1.0/CustomContext.json" + ], + "reference": "SERIAL_NUMBER_123", + "name": "Republic of Singapore Driving Licence", + "issuanceDate": "2010-01-01T19:23:24Z", + "validFrom": "2010-01-01T19:23:24Z", + "issuer": { + "id": "https://example.com", + "name": "DEMO STORE" + }, + "type": ["VerifiableCredential", "DrivingLicenceCredential"], + "credentialSubject": { + "id": "did:example:SERIAL_NUMBER_123", + "class": [ + { + "type": "This Value Has Been Changed Since Signing", + "effectiveDate": "2010-01-01T19:23:24Z" + }, + { + "type": "3A", + "effectiveDate": "2010-01-01T19:23:24Z" + } + ] + }, + "openAttestationMetadata": { + "template": { + "name": "CUSTOM_TEMPLATE", + "type": "EMBEDDED_RENDERER", + "url": "https://localhost:3000/renderer" + }, + "proof": { + "type": "OpenAttestationProofMethod", + "method": "DID", + "value": "did:ethr:0xE712878f6E8d5d4F9e87E10DA604F9cB564C9a89", + "revocation": { + "type": "NONE" + } + }, + "identityProof": { + "type": "DNS-DID", + "identifier": "notinuse.tradetrust.io" + } + }, + "attachments": [ + { + "fileName": "sample.pdf", + "mimeType": "application/pdf", + "data": "BASE64_ENCODED_FILE" + } + ], + "proof": { + "type": "OpenAttestationMerkleProofSignature2018", + "proofPurpose": "assertionMethod", + "targetHash": "1e3c1e44c93a45869c54bb6d71c9ca7316aa76a5489cf3a8cf69552262accfd4", + "proofs": [], + "merkleRoot": "1e3c1e44c93a45869c54bb6d71c9ca7316aa76a5489cf3a8cf69552262accfd4", + "salts": "W3sidmFsdWUiOiI3ZDQ2YTZhNGRhYzMzNDUwMTYzNTJiM2QzMjcwOTYzODc2YjliYzY3ODdjZWI2ODhjY2RkODY3YzlmMzM3MjkzIiwicGF0aCI6InZlcnNpb24ifSx7InZhbHVlIjoiYTQ0ZmE3ZWMzMTgzMzliNDFmMDFiNDI5MGEwMGI0NDUzZDJiOTJhYjQzMWNiOWZhOWNkYWQ3MDNjM2E3MDc1YSIsInBhdGgiOiJAY29udGV4dFswXSJ9LHsidmFsdWUiOiJkMzc4ODk5ZjAwNjhhODdkY2EyNzk0N2U0YWM2N2RhNTE5ZDRlOTI0N2Y5ZjMzZDY4NDk3OTI2ZTMzZWRhMWIwIiwicGF0aCI6IkBjb250ZXh0WzFdIn0seyJ2YWx1ZSI6ImVmZTQ0ZTU4NGIwNTViYjVjZDZjMWRmN2JlNGUzN2UxMTk0MmE5ODU0ZmViNzdlNWY4ZDlkNTY3YTRjMDRiMDgiLCJwYXRoIjoiQGNvbnRleHRbMl0ifSx7InZhbHVlIjoiYzhiOWRmZDI2ZTc5ZjhjMDRlOWU3ZjllZjdiOWRmM2Y3MzljNzFhZmNlYzg4NGViNDE3MjA1NmE3N2YwNDI0MiIsInBhdGgiOiJAY29udGV4dFszXSJ9LHsidmFsdWUiOiIzZGFlOGM1ZWQxNjRiN2FjMzljZDk3ODg3MDM0YzEwMGI3M2VlNTRmZmU3NzE3NTZlNDMzMDBjYThhZjJlZGZhIiwicGF0aCI6InJlZmVyZW5jZSJ9LHsidmFsdWUiOiJiMDgzN2E0NTFmNDYyMjBlYmJjNTkxNmFmNDQ3ZDFmNjg0NjIxY2M0Mzk2Mzg1NmM5OTY5MWEyNmFjNGE5NmYzIiwicGF0aCI6Im5hbWUifSx7InZhbHVlIjoiMmM0ODI1NzA3MzE5MTBmM2YzOThkNzIxMTk5MzcwYzdkNTI4YjQ1ZGYwMDY0OTI2ZTJiMjhkMzg5MjJkNTIyMyIsInBhdGgiOiJpc3N1YW5jZURhdGUifSx7InZhbHVlIjoiMjA4NWQzN2UxYTU4NjdlNjVkYzdjNTExZjYwNzAwOTg1NzI5YmFjMTRhNWIwNjkyNDc5ODA1ZDM0NTkxZTdmMyIsInBhdGgiOiJ2YWxpZEZyb20ifSx7InZhbHVlIjoiOGU4YjIzZWRhOGE3ZTE4MWM0MzgyMTk4OGRhMDE3OThhZTU1Yjc5ZDk0OWQxNGExMjYxYTU2MTQzNDc2YTNkZSIsInBhdGgiOiJpc3N1ZXIuaWQifSx7InZhbHVlIjoiMWFlN2QxZjViMGU1MTA2MjM3ODc5ODJhZDEzNzYzZmQ5MjZlYWUxMWNiOWMxYmZkZjU4NzQzZTEwNWNhYTAxMSIsInBhdGgiOiJpc3N1ZXIubmFtZSJ9LHsidmFsdWUiOiI1YjFhYWY3NzcyNmU4YTA0OTI3NGM1Njk4NDc2YWM5OTY0OGI1ODY0NTAyMWRjNGJjODc5OWY0NjQyZDY5YTA5IiwicGF0aCI6InR5cGVbMF0ifSx7InZhbHVlIjoiNDI4MTgzODJiZjdiYmE4OTIwYmQ2M2JkOGJhMGQwMmNhNGEyODVhYTMyZTIyYzI2NGY1M2Y1NDM5NzNhZjE2MCIsInBhdGgiOiJ0eXBlWzFdIn0seyJ2YWx1ZSI6ImFiYzdlMGRiYTlmZWMxNjlmMmM2NjY3NjFkYjdmM2JmZWE2ZTM2ODVhZTI5MjMwYTExZTU5ZGVhNWMzNzg1MmQiLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3QuaWQifSx7InZhbHVlIjoiOWUxNWMxNDAyYTBkNDM0YWMzM2Q5YzAzMGYwMDViNzA1ZWZlN2VmMDkxNDZlZmVlYmIwZTNkN2ZkYjkwNDA4MyIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC5jbGFzc1swXS50eXBlIn0seyJ2YWx1ZSI6ImYzYzY4MTM4NWNjODNhZjc2NDk1MjJlZWE5OGUyZmMxZjViYTZiMTRiZTU2NWE4MGNmYWIzYWQ2OTE0OTU5YWQiLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3QuY2xhc3NbMF0uZWZmZWN0aXZlRGF0ZSJ9LHsidmFsdWUiOiJjNzc4M2JmMmQ3ZGFjODA1MDA5OTc3ODZlMDU5YTYzZDg5NzJiNzhhZjEyZjE0NjNkZDNmZTgxMmFhNjEzMTlkIiwicGF0aCI6ImNyZWRlbnRpYWxTdWJqZWN0LmNsYXNzWzFdLnR5cGUifSx7InZhbHVlIjoiMGI1YzllZWQ0YmY5YWUzOTVmOTI0MjYyMGFjNDgwOGMwZjQ1ZGY3ZjY3N2FhZjA1NWMwZjg1YzZlZjI3NjVlNSIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC5jbGFzc1sxXS5lZmZlY3RpdmVEYXRlIn0seyJ2YWx1ZSI6IjQzNzcxZjg2YmRhZmI0NzBmZjkxNDdiYjA0MGUzMjNkYzY2NDcwZGZmYWQzOGRkYmEzMmE4ZmEyNDMwNmFlMTQiLCJwYXRoIjoib3BlbkF0dGVzdGF0aW9uTWV0YWRhdGEudGVtcGxhdGUubmFtZSJ9LHsidmFsdWUiOiJmZTVjODUxNmQ2YTk3NDZjMTBkM2NlMzQ0NWM5YmFiZGM0YTE5NTI3MTJjNjIyMDNkOTAwOGFjODk0NTA4ZTRhIiwicGF0aCI6Im9wZW5BdHRlc3RhdGlvbk1ldGFkYXRhLnRlbXBsYXRlLnR5cGUifSx7InZhbHVlIjoiYmNlNmZhZDI0MDlhM2FkN2Y2OGYxYmRmZDhjNDFjMDAzYzM5OWE4M2EyMDYyNjZjNWM2ZWNkNWI0OTI4NDE1OCIsInBhdGgiOiJvcGVuQXR0ZXN0YXRpb25NZXRhZGF0YS50ZW1wbGF0ZS51cmwifSx7InZhbHVlIjoiOWFhODc4Y2RhYjQ0NTAwMWQyODY0YzYwY2M4MzcwMTQ3YmZiZGViMzEyZDA0Mjk2ODJiZWVmYmE2NGVhZGJjYSIsInBhdGgiOiJvcGVuQXR0ZXN0YXRpb25NZXRhZGF0YS5wcm9vZi50eXBlIn0seyJ2YWx1ZSI6ImUwZGRhZmFjNThhZTk5ZjU3MWY3MmQ0MzcwNDZmNjk5MmI4M2MzNzEwODg5ODFlYWQxM2Q2NTgwZTNmOGRhMjIiLCJwYXRoIjoib3BlbkF0dGVzdGF0aW9uTWV0YWRhdGEucHJvb2YubWV0aG9kIn0seyJ2YWx1ZSI6ImNhYjdkYmYzMDRhMzgwMmFlMmQxMzZkZjE3MzA5YzUzMDBhMWEzYzI4NTY5ODAxODc2MmY3MTA0YWM5MTE4ODYiLCJwYXRoIjoib3BlbkF0dGVzdGF0aW9uTWV0YWRhdGEucHJvb2YudmFsdWUifSx7InZhbHVlIjoiZDVhNTEyMWM3MDYyMjkxMzE2MDYzYzU0YmU3ZDdjMTAxMDZkMThhMmQ3ZDc2YWRlZDBkMDhlN2NhYjAwMDY5MCIsInBhdGgiOiJvcGVuQXR0ZXN0YXRpb25NZXRhZGF0YS5wcm9vZi5yZXZvY2F0aW9uLnR5cGUifSx7InZhbHVlIjoiNjViMGQxN2Q4MzBmMjVkYzkwMmIzOWRiNjQ4ODM3ODM0OTNhMDI5MDQ3NDQwM2EwZWJlNGQ4OTM3OTMzNWRhNiIsInBhdGgiOiJvcGVuQXR0ZXN0YXRpb25NZXRhZGF0YS5pZGVudGl0eVByb29mLnR5cGUifSx7InZhbHVlIjoiYTFhMDk2NzFjNDVjYmQxNDgyMmFmMTFhNDZlYmRjYWY3MGQwNzZlZmQxOWY1N2NjYjNhMzJmYWU1ODliNDg5ZSIsInBhdGgiOiJvcGVuQXR0ZXN0YXRpb25NZXRhZGF0YS5pZGVudGl0eVByb29mLmlkZW50aWZpZXIifSx7InZhbHVlIjoiYzc5OTE5NWIxNzI4NzM1MTllN2Q1ZWQ4ZjZiY2U1Mjc2MDBmOGFjMjFhNWMxYWI2ZTllN2QwNzY3MTIwYjQyMSIsInBhdGgiOiJhdHRhY2htZW50c1swXS5maWxlTmFtZSJ9LHsidmFsdWUiOiIzOTQ3ZTM4OWQyNzZiYjAzYzY1Yzg5MjVlYTAxMDQ4Njk4ZTUyNGM3NzcyZjY2MWYzMjhkZjI5YjlkMGZhZTIzIiwicGF0aCI6ImF0dGFjaG1lbnRzWzBdLm1pbWVUeXBlIn0seyJ2YWx1ZSI6IjQyMWUyMTBlOWQ0MjY0Y2YwZWE5ZmQ0NWQzMjI3NDUzOTE0YWMzZjc5Y2QzY2IwNmYxZjllYjk5ZjJhMjU0YzEiLCJwYXRoIjoiYXR0YWNobWVudHNbMF0uZGF0YSJ9XQ==", + "privacy": { + "obfuscated": [] + }, + "key": "did:ethr:0xE712878f6E8d5d4F9e87E10DA604F9cB564C9a89#controller", + "signature": "0x2acad5edbece2e3febd232c01dedb30ae9fa3a0b2e33354a48700d14617b5af52b587d1c67ba9ca4b39b27902de4df868a2a6de89d6d530cf1d8c55e65cc13f31c" + } +} diff --git a/libs/server-common/src/fixtures/oav3/did-invalid.json b/libs/server-common/src/fixtures/oav3/did-invalid.json new file mode 100644 index 00000000..eb95fe70 --- /dev/null +++ b/libs/server-common/src/fixtures/oav3/did-invalid.json @@ -0,0 +1,74 @@ +{ + "version": "https://schema.openattestation.com/3.0/schema.json", + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://schemata.openattestation.com/com/openattestation/1.0/DrivingLicenceCredential.json", + "https://schemata.openattestation.com/com/openattestation/1.0/OpenAttestation.v3.json", + "https://schemata.openattestation.com/com/openattestation/1.0/CustomContext.json" + ], + "reference": "SERIAL_NUMBER_123", + "name": "Republic of Singapore Driving Licence", + "issuanceDate": "2010-01-01T19:23:24Z", + "validFrom": "2010-01-01T19:23:24Z", + "issuer": { + "id": "https://example.com", + "name": "DEMO STORE" + }, + "type": [ + "VerifiableCredential", + "OpenAttestationCredential", + "DrivingLicenceCredential" + ], + "credentialSubject": { + "id": "did:example:SERIAL_NUMBER_123", + "class": [ + { + "type": "This Value Has Been Changed Since Signing", + "effectiveDate": "2010-01-01T19:23:24Z" + }, + { + "type": "3A", + "effectiveDate": "2010-01-01T19:23:24Z" + } + ] + }, + "openAttestationMetadata": { + "template": { + "name": "CUSTOM_TEMPLATE", + "type": "EMBEDDED_RENDERER", + "url": "https://localhost:3000/renderer" + }, + "proof": { + "type": "OpenAttestationProofMethod", + "method": "DID", + "value": "did:ethr:0xE712878f6E8d5d4F9e87E10DA604F9cB564C9a89", + "revocation": { + "type": "NONE" + } + }, + "identityProof": { + "type": "DNS-DID", + "identifier": "notinuse.tradetrust.io" + } + }, + "attachments": [ + { + "fileName": "sample.pdf", + "mimeType": "application/pdf", + "data": "BASE64_ENCODED_FILE" + } + ], + "proof": { + "type": "OpenAttestationMerkleProofSignature2018", + "proofPurpose": "assertionMethod", + "targetHash": "1e3c1e44c93a45869c54bb6d71c9ca7316aa76a5489cf3a8cf69552262accfd4", + "proofs": [], + "merkleRoot": "1e3c1e44c93a45869c54bb6d71c9ca7316aa76a5489cf3a8cf69552262accfd4", + "salts": "W3sidmFsdWUiOiI3ZDQ2YTZhNGRhYzMzNDUwMTYzNTJiM2QzMjcwOTYzODc2YjliYzY3ODdjZWI2ODhjY2RkODY3YzlmMzM3MjkzIiwicGF0aCI6InZlcnNpb24ifSx7InZhbHVlIjoiYTQ0ZmE3ZWMzMTgzMzliNDFmMDFiNDI5MGEwMGI0NDUzZDJiOTJhYjQzMWNiOWZhOWNkYWQ3MDNjM2E3MDc1YSIsInBhdGgiOiJAY29udGV4dFswXSJ9LHsidmFsdWUiOiJkMzc4ODk5ZjAwNjhhODdkY2EyNzk0N2U0YWM2N2RhNTE5ZDRlOTI0N2Y5ZjMzZDY4NDk3OTI2ZTMzZWRhMWIwIiwicGF0aCI6IkBjb250ZXh0WzFdIn0seyJ2YWx1ZSI6ImVmZTQ0ZTU4NGIwNTViYjVjZDZjMWRmN2JlNGUzN2UxMTk0MmE5ODU0ZmViNzdlNWY4ZDlkNTY3YTRjMDRiMDgiLCJwYXRoIjoiQGNvbnRleHRbMl0ifSx7InZhbHVlIjoiYzhiOWRmZDI2ZTc5ZjhjMDRlOWU3ZjllZjdiOWRmM2Y3MzljNzFhZmNlYzg4NGViNDE3MjA1NmE3N2YwNDI0MiIsInBhdGgiOiJAY29udGV4dFszXSJ9LHsidmFsdWUiOiIzZGFlOGM1ZWQxNjRiN2FjMzljZDk3ODg3MDM0YzEwMGI3M2VlNTRmZmU3NzE3NTZlNDMzMDBjYThhZjJlZGZhIiwicGF0aCI6InJlZmVyZW5jZSJ9LHsidmFsdWUiOiJiMDgzN2E0NTFmNDYyMjBlYmJjNTkxNmFmNDQ3ZDFmNjg0NjIxY2M0Mzk2Mzg1NmM5OTY5MWEyNmFjNGE5NmYzIiwicGF0aCI6Im5hbWUifSx7InZhbHVlIjoiMmM0ODI1NzA3MzE5MTBmM2YzOThkNzIxMTk5MzcwYzdkNTI4YjQ1ZGYwMDY0OTI2ZTJiMjhkMzg5MjJkNTIyMyIsInBhdGgiOiJpc3N1YW5jZURhdGUifSx7InZhbHVlIjoiMjA4NWQzN2UxYTU4NjdlNjVkYzdjNTExZjYwNzAwOTg1NzI5YmFjMTRhNWIwNjkyNDc5ODA1ZDM0NTkxZTdmMyIsInBhdGgiOiJ2YWxpZEZyb20ifSx7InZhbHVlIjoiOGU4YjIzZWRhOGE3ZTE4MWM0MzgyMTk4OGRhMDE3OThhZTU1Yjc5ZDk0OWQxNGExMjYxYTU2MTQzNDc2YTNkZSIsInBhdGgiOiJpc3N1ZXIuaWQifSx7InZhbHVlIjoiMWFlN2QxZjViMGU1MTA2MjM3ODc5ODJhZDEzNzYzZmQ5MjZlYWUxMWNiOWMxYmZkZjU4NzQzZTEwNWNhYTAxMSIsInBhdGgiOiJpc3N1ZXIubmFtZSJ9LHsidmFsdWUiOiI1YjFhYWY3NzcyNmU4YTA0OTI3NGM1Njk4NDc2YWM5OTY0OGI1ODY0NTAyMWRjNGJjODc5OWY0NjQyZDY5YTA5IiwicGF0aCI6InR5cGVbMF0ifSx7InZhbHVlIjoiNDI4MTgzODJiZjdiYmE4OTIwYmQ2M2JkOGJhMGQwMmNhNGEyODVhYTMyZTIyYzI2NGY1M2Y1NDM5NzNhZjE2MCIsInBhdGgiOiJ0eXBlWzFdIn0seyJ2YWx1ZSI6ImFiYzdlMGRiYTlmZWMxNjlmMmM2NjY3NjFkYjdmM2JmZWE2ZTM2ODVhZTI5MjMwYTExZTU5ZGVhNWMzNzg1MmQiLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3QuaWQifSx7InZhbHVlIjoiOWUxNWMxNDAyYTBkNDM0YWMzM2Q5YzAzMGYwMDViNzA1ZWZlN2VmMDkxNDZlZmVlYmIwZTNkN2ZkYjkwNDA4MyIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC5jbGFzc1swXS50eXBlIn0seyJ2YWx1ZSI6ImYzYzY4MTM4NWNjODNhZjc2NDk1MjJlZWE5OGUyZmMxZjViYTZiMTRiZTU2NWE4MGNmYWIzYWQ2OTE0OTU5YWQiLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3QuY2xhc3NbMF0uZWZmZWN0aXZlRGF0ZSJ9LHsidmFsdWUiOiJjNzc4M2JmMmQ3ZGFjODA1MDA5OTc3ODZlMDU5YTYzZDg5NzJiNzhhZjEyZjE0NjNkZDNmZTgxMmFhNjEzMTlkIiwicGF0aCI6ImNyZWRlbnRpYWxTdWJqZWN0LmNsYXNzWzFdLnR5cGUifSx7InZhbHVlIjoiMGI1YzllZWQ0YmY5YWUzOTVmOTI0MjYyMGFjNDgwOGMwZjQ1ZGY3ZjY3N2FhZjA1NWMwZjg1YzZlZjI3NjVlNSIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC5jbGFzc1sxXS5lZmZlY3RpdmVEYXRlIn0seyJ2YWx1ZSI6IjQzNzcxZjg2YmRhZmI0NzBmZjkxNDdiYjA0MGUzMjNkYzY2NDcwZGZmYWQzOGRkYmEzMmE4ZmEyNDMwNmFlMTQiLCJwYXRoIjoib3BlbkF0dGVzdGF0aW9uTWV0YWRhdGEudGVtcGxhdGUubmFtZSJ9LHsidmFsdWUiOiJmZTVjODUxNmQ2YTk3NDZjMTBkM2NlMzQ0NWM5YmFiZGM0YTE5NTI3MTJjNjIyMDNkOTAwOGFjODk0NTA4ZTRhIiwicGF0aCI6Im9wZW5BdHRlc3RhdGlvbk1ldGFkYXRhLnRlbXBsYXRlLnR5cGUifSx7InZhbHVlIjoiYmNlNmZhZDI0MDlhM2FkN2Y2OGYxYmRmZDhjNDFjMDAzYzM5OWE4M2EyMDYyNjZjNWM2ZWNkNWI0OTI4NDE1OCIsInBhdGgiOiJvcGVuQXR0ZXN0YXRpb25NZXRhZGF0YS50ZW1wbGF0ZS51cmwifSx7InZhbHVlIjoiOWFhODc4Y2RhYjQ0NTAwMWQyODY0YzYwY2M4MzcwMTQ3YmZiZGViMzEyZDA0Mjk2ODJiZWVmYmE2NGVhZGJjYSIsInBhdGgiOiJvcGVuQXR0ZXN0YXRpb25NZXRhZGF0YS5wcm9vZi50eXBlIn0seyJ2YWx1ZSI6ImUwZGRhZmFjNThhZTk5ZjU3MWY3MmQ0MzcwNDZmNjk5MmI4M2MzNzEwODg5ODFlYWQxM2Q2NTgwZTNmOGRhMjIiLCJwYXRoIjoib3BlbkF0dGVzdGF0aW9uTWV0YWRhdGEucHJvb2YubWV0aG9kIn0seyJ2YWx1ZSI6ImNhYjdkYmYzMDRhMzgwMmFlMmQxMzZkZjE3MzA5YzUzMDBhMWEzYzI4NTY5ODAxODc2MmY3MTA0YWM5MTE4ODYiLCJwYXRoIjoib3BlbkF0dGVzdGF0aW9uTWV0YWRhdGEucHJvb2YudmFsdWUifSx7InZhbHVlIjoiZDVhNTEyMWM3MDYyMjkxMzE2MDYzYzU0YmU3ZDdjMTAxMDZkMThhMmQ3ZDc2YWRlZDBkMDhlN2NhYjAwMDY5MCIsInBhdGgiOiJvcGVuQXR0ZXN0YXRpb25NZXRhZGF0YS5wcm9vZi5yZXZvY2F0aW9uLnR5cGUifSx7InZhbHVlIjoiNjViMGQxN2Q4MzBmMjVkYzkwMmIzOWRiNjQ4ODM3ODM0OTNhMDI5MDQ3NDQwM2EwZWJlNGQ4OTM3OTMzNWRhNiIsInBhdGgiOiJvcGVuQXR0ZXN0YXRpb25NZXRhZGF0YS5pZGVudGl0eVByb29mLnR5cGUifSx7InZhbHVlIjoiYTFhMDk2NzFjNDVjYmQxNDgyMmFmMTFhNDZlYmRjYWY3MGQwNzZlZmQxOWY1N2NjYjNhMzJmYWU1ODliNDg5ZSIsInBhdGgiOiJvcGVuQXR0ZXN0YXRpb25NZXRhZGF0YS5pZGVudGl0eVByb29mLmlkZW50aWZpZXIifSx7InZhbHVlIjoiYzc5OTE5NWIxNzI4NzM1MTllN2Q1ZWQ4ZjZiY2U1Mjc2MDBmOGFjMjFhNWMxYWI2ZTllN2QwNzY3MTIwYjQyMSIsInBhdGgiOiJhdHRhY2htZW50c1swXS5maWxlTmFtZSJ9LHsidmFsdWUiOiIzOTQ3ZTM4OWQyNzZiYjAzYzY1Yzg5MjVlYTAxMDQ4Njk4ZTUyNGM3NzcyZjY2MWYzMjhkZjI5YjlkMGZhZTIzIiwicGF0aCI6ImF0dGFjaG1lbnRzWzBdLm1pbWVUeXBlIn0seyJ2YWx1ZSI6IjQyMWUyMTBlOWQ0MjY0Y2YwZWE5ZmQ0NWQzMjI3NDUzOTE0YWMzZjc5Y2QzY2IwNmYxZjllYjk5ZjJhMjU0YzEiLCJwYXRoIjoiYXR0YWNobWVudHNbMF0uZGF0YSJ9XQ==", + "privacy": { + "obfuscated": [] + }, + "key": "did:ethr:0xE712878f6E8d5d4F9e87E10DA604F9cB564C9a89#controller", + "signature": "0x2acad5edbece2e3febd232c01dedb30ae9fa3a0b2e33354a48700d14617b5af52b587d1c67ba9ca4b39b27902de4df868a2a6de89d6d530cf1d8c55e65cc13f31c" + } +} diff --git a/libs/server-common/src/fixtures/oav3/did-revocation-store-signed-no-location.json b/libs/server-common/src/fixtures/oav3/did-revocation-store-signed-no-location.json new file mode 100644 index 00000000..117b934d --- /dev/null +++ b/libs/server-common/src/fixtures/oav3/did-revocation-store-signed-no-location.json @@ -0,0 +1,70 @@ +{ + "version": "https://schema.openattestation.com/3.0/schema.json", + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://schemata.openattestation.com/com/openattestation/1.0/DrivingLicenceCredential.json", + "https://schemata.openattestation.com/com/openattestation/1.0/OpenAttestation.v3.json", + "https://schemata.openattestation.com/com/openattestation/1.0/CustomContext.json" + ], + "reference": "SERIAL_NUMBER_123", + "name": "Republic of Singapore Driving Licence", + "issuanceDate": "2010-01-01T19:23:24Z", + "validFrom": "2010-01-01T19:23:24Z", + "issuer": { + "id": "https://example.com", + "name": "DEMO STORE" + }, + "type": ["VerifiableCredential", "DrivingLicenceCredential"], + "credentialSubject": { + "id": "did:example:SERIAL_NUMBER_123", + "class": [ + { + "type": "3", + "effectiveDate": "2010-01-01T19:23:24Z" + }, + { + "type": "3A", + "effectiveDate": "2010-01-01T19:23:24Z" + } + ] + }, + "openAttestationMetadata": { + "template": { + "name": "CUSTOM_TEMPLATE", + "type": "EMBEDDED_RENDERER", + "url": "https://localhost:3000/renderer" + }, + "proof": { + "type": "OpenAttestationProofMethod", + "method": "DID", + "value": "did:ethr:0xE712878f6E8d5d4F9e87E10DA604F9cB564C9a89", + "revocation": { + "type": "REVOCATION_STORE" + } + }, + "identityProof": { + "type": "DID", + "identifier": "did:ethr:0xE712878f6E8d5d4F9e87E10DA604F9cB564C9a89" + } + }, + "attachments": [ + { + "fileName": "sample.pdf", + "mimeType": "application/pdf", + "data": "BASE64_ENCODED_FILE" + } + ], + "proof": { + "type": "OpenAttestationMerkleProofSignature2018", + "proofPurpose": "assertionMethod", + "targetHash": "082c7801b60aab4e54072c739ca628a9a119e885043d8ef624c336afc7b786dd", + "proofs": [], + "merkleRoot": "082c7801b60aab4e54072c739ca628a9a119e885043d8ef624c336afc7b786dd", + "salts": "W3sidmFsdWUiOiJiYzg1NDk1YzhhZjRjZDUyNDc1MzIwM2Q1NGM3MGU2ZTI0ZmI0NjYxMTE4MzkyNDFkMGYwZTZlZmQ2MGE3N2M2IiwicGF0aCI6InZlcnNpb24ifSx7InZhbHVlIjoiMmQxYjhmYmY3MDI1MzljZTc4ZGZmNWQzNjQ1MGY2ZGY3YjU1YmUzMTIzOGM2ZDAwNWY0NjgzOTYyODBhOTJmMiIsInBhdGgiOiJAY29udGV4dFswXSJ9LHsidmFsdWUiOiI0ZmQwYjU3Yjg3M2JmZjIyMTYzZDllNDA1ZTU2YzQzYmIyZTU5MWIwZTY0Y2Q1ODI4NTk3OGI3ZWIwMGM2OGFlIiwicGF0aCI6IkBjb250ZXh0WzFdIn0seyJ2YWx1ZSI6IjYzZWYzZjU0MTdiZDViYjdmZjMyMDBlMzA2ZGY5MGE0MDY1NmM4MTk3ZTk1MWE4YjA1NDRlMjRiNWQ1N2UzYjYiLCJwYXRoIjoiQGNvbnRleHRbMl0ifSx7InZhbHVlIjoiMWIxZDVlMDcwM2RmOGQ4NWI3MTRhNzI2M2FmYTQwM2E3MDQ1MDc5NGVhNDc3MDhhZTY5NTdmMWQyNDU5NWFiOCIsInBhdGgiOiJAY29udGV4dFszXSJ9LHsidmFsdWUiOiJkZTM4OGEwZjMzMGM3NzI2YzY3ZTM3NzI1MTYzZDhmYTkyYjZkNjMyMmFiMTlkYTMwYjg1ODVhNTdlZGFjZTNhIiwicGF0aCI6InJlZmVyZW5jZSJ9LHsidmFsdWUiOiI0MTIyMTViZTRkMzY1MjhmYjgxMDRjNDkwMGJjYjBlNjM4YTYxYmQyZjgyOTQ5OGE0YTg2NWQyMzMyYzM2MWNhIiwicGF0aCI6Im5hbWUifSx7InZhbHVlIjoiYzYxMmNlZjc2MTA2MjI1YmQwYzZhN2U3MzY5MDczY2ZhNjAwYTU3MDk3Njg0NzQ5ZDI0NTU3OTQ5ODBjNTE3ZSIsInBhdGgiOiJpc3N1YW5jZURhdGUifSx7InZhbHVlIjoiNzYxY2I2ZDRlM2Y5YTFiYmZlYjkyODEwZjJmNzAwZjcyNTBmMTVjNWUyYWM5NGMxZjk5NmVhMmM2MTA5ZmJlNiIsInBhdGgiOiJ2YWxpZEZyb20ifSx7InZhbHVlIjoiZWQyMTlkMGUxZTY5YjJiYTgzZmVlNWQ1NjU3YWNmOWMwODlhZGM4ZjVjZmViMzA4Njk4NWRiZWM4ZjI1ZjFmZSIsInBhdGgiOiJpc3N1ZXIuaWQifSx7InZhbHVlIjoiNTM0ODhlZDgxZmE2MDI4MDA0NDg1ZjAwZWVmY2RmZWMxNTBlNjlmYWJlMTNjMjY3OGVhNGVkYWUzMDI5NzFhYSIsInBhdGgiOiJpc3N1ZXIubmFtZSJ9LHsidmFsdWUiOiJiNjMzYWZhNjY4OGE3NTQyMjU4ZDVkNTRlMTA0ZTY1MmNlMjk0MjA5ZTgwM2NjOTFjZGE4NDU5MTllZjBhN2MxIiwicGF0aCI6InR5cGVbMF0ifSx7InZhbHVlIjoiODFkZjYxNTFiODczNzQ1Y2YxYzdiYjM5NWE0YTllMGEyNjQ0NjQwZjY4NDQ0MWEzNDY3OGE2NDMxMGViYTg0ZiIsInBhdGgiOiJ0eXBlWzFdIn0seyJ2YWx1ZSI6IjIyNmNmMTJlOThiOWIwMjYwZDY2ZDI4ZjgxYjY4OGM4YzhjMDZiMzYzOWE3MDRlNDgxOGUzMDY0OTRiYTVjOGUiLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3QuaWQifSx7InZhbHVlIjoiYjdlN2U1MzEwNzZkMWY1MDBjODgwNzhkMDkwMjdhYzM3YjE3NGY3YjY0ZTRmMTU5M2RhZGIyY2UxYzEzNGUxYiIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC5jbGFzc1swXS50eXBlIn0seyJ2YWx1ZSI6ImYzNWU2MDU4N2IwYTUzOTQ3YTk5NTQ5ZTUzNzQxYjAxMTY1NjU0ZjkzYmVjYzE5ZGNlYjRlYzY0MDA4YzRjZmMiLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3QuY2xhc3NbMF0uZWZmZWN0aXZlRGF0ZSJ9LHsidmFsdWUiOiIwZjI2NmU1NjMxNDEwNzMxMmVjMWZjMmM2OTQ4NDA2MzQ2MGMzODNmODc1NmI4Nzc5YjMxMmFkZGU1YzA5ZWQ5IiwicGF0aCI6ImNyZWRlbnRpYWxTdWJqZWN0LmNsYXNzWzFdLnR5cGUifSx7InZhbHVlIjoiMGZlZGIyZmU2ZDFjNzVjZjE5NGRmZjkzOGFiNmI5Mzg5YzM4YzVlYmNkNWEwZDUzZmE1MDVjZWQ0MDcwYTM1OSIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC5jbGFzc1sxXS5lZmZlY3RpdmVEYXRlIn0seyJ2YWx1ZSI6IjRmMmVkYjNiNjMzNWI3NjQ5ZjVmYjIzMGJkMjViMDM1YTdlNWIxNmVkNTQ5MjA4NDMzMDdkN2Q4YTQ4MjdkYWQiLCJwYXRoIjoib3BlbkF0dGVzdGF0aW9uTWV0YWRhdGEudGVtcGxhdGUubmFtZSJ9LHsidmFsdWUiOiI2NTIwY2E4NmViMTY3Mzk0MjM2MTIwZjg3YjJiYjM2MjI3OTBkMTg4NGM0NTMwODEyMTIxNmYyMmVmZTQ4NzA0IiwicGF0aCI6Im9wZW5BdHRlc3RhdGlvbk1ldGFkYXRhLnRlbXBsYXRlLnR5cGUifSx7InZhbHVlIjoiZGZjYmYyMTNmYzBmNDk0OWQ2YjA1ZWZhZjJiNzE5OTEwYjQyN2Y5YjVlYjc0ZmRiNjNjZWFiNDYxMzE1NTMyNCIsInBhdGgiOiJvcGVuQXR0ZXN0YXRpb25NZXRhZGF0YS50ZW1wbGF0ZS51cmwifSx7InZhbHVlIjoiMWIwYjYyYzY1YzIwNGUyODE3NDMzZDYzMGZlYTNkNTE1NDYyNGNkMjAwYWQ0ZjhkOGFmNjY2NGRhNGM4NmRiOSIsInBhdGgiOiJvcGVuQXR0ZXN0YXRpb25NZXRhZGF0YS5wcm9vZi50eXBlIn0seyJ2YWx1ZSI6ImFmNzBjZjE3YWQyZjk2NzM1NTEwYWJhNjhiMmFiMWM5ZDEzMzBiYWQyYjVjM2MwZmVkY2VkZDk0NTRmMWQzYjgiLCJwYXRoIjoib3BlbkF0dGVzdGF0aW9uTWV0YWRhdGEucHJvb2YubWV0aG9kIn0seyJ2YWx1ZSI6IjkzMjZiZjI5ODZmMzNkMWIxNTA4NWRmOGQwYjQ3ZTA0Zjg4NGYyMzJmNTM2MWEyYWZlNjBlNjM4MTBiYzY3ZTUiLCJwYXRoIjoib3BlbkF0dGVzdGF0aW9uTWV0YWRhdGEucHJvb2YudmFsdWUifSx7InZhbHVlIjoiNWI3MWVmYzNmNTAwYzRhMmExYzBjZDVmMjc5NDI1N2QwM2JmNjljNzUzNjJhYjQ4MmE1OTQyMDI4NGY2NTg2YyIsInBhdGgiOiJvcGVuQXR0ZXN0YXRpb25NZXRhZGF0YS5wcm9vZi5yZXZvY2F0aW9uLnR5cGUifSx7InZhbHVlIjoiYjZiMjJmNjI5Mzg5NzU4NDRjM2U4NDNjZjZmZDA0YWJkNDAyZjVkYWRlNjY5OTc2NjM0ZWU4N2UwZDU0Y2Y3OSIsInBhdGgiOiJvcGVuQXR0ZXN0YXRpb25NZXRhZGF0YS5pZGVudGl0eVByb29mLnR5cGUifSx7InZhbHVlIjoiNjA0ZjI0YTg5YzZlMmVkMTNmNDg1MWMyOTU3YjEwZDMzNGM5NjdmMDYxYTZhY2EyZjc0OTA2N2EyZDU1MGEyYyIsInBhdGgiOiJvcGVuQXR0ZXN0YXRpb25NZXRhZGF0YS5pZGVudGl0eVByb29mLmlkZW50aWZpZXIifSx7InZhbHVlIjoiMTljNDNmZjY1MjJjMThhMmI1YmJmNTI5OGNjMzUyYWIzYzdlMGM0ZmQ4ZmI5YTlkOTBhN2M3MzE4MzhhY2Q1NCIsInBhdGgiOiJhdHRhY2htZW50c1swXS5maWxlTmFtZSJ9LHsidmFsdWUiOiIxMTU3ZGVmYjQ4MGM0ZDRhNTlkODY3N2E5ZDljZjBhODViNzJmMzYyYzIzMTM2OTVjMjMxYjIyZTU1MmJmNmUyIiwicGF0aCI6ImF0dGFjaG1lbnRzWzBdLm1pbWVUeXBlIn0seyJ2YWx1ZSI6IjI2MGMxYTM3MjhkNWMxNDlhMTUzOWJkMWI0NDE3NWExODk5NDU2ZmFlZWMzOWU3ZDQ4NGU5MzVhMTZhYWE4NDUiLCJwYXRoIjoiYXR0YWNobWVudHNbMF0uZGF0YSJ9XQ==", + "privacy": { + "obfuscated": [] + }, + "key": "did:ethr:0xE712878f6E8d5d4F9e87E10DA604F9cB564C9a89#controller", + "signature": "0x5568e41b14d8999d205e7176638a9ce5a021b9f3ebb20344bcd892e2224023326559ebd6c4ab6ee7cf8873cbbbedb3f78b82780530d8493a5d0084dc37b3d6cf1c" + } +} diff --git a/libs/server-common/src/fixtures/oav3/did-revocation-store-signed-not-revoked.json b/libs/server-common/src/fixtures/oav3/did-revocation-store-signed-not-revoked.json new file mode 100644 index 00000000..f70c3e9c --- /dev/null +++ b/libs/server-common/src/fixtures/oav3/did-revocation-store-signed-not-revoked.json @@ -0,0 +1,76 @@ +{ + "version": "https://schema.openattestation.com/3.0/schema.json", + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://schemata.openattestation.com/com/openattestation/1.0/DrivingLicenceCredential.json", + "https://schemata.openattestation.com/com/openattestation/1.0/OpenAttestation.v3.json", + "https://schemata.openattestation.com/com/openattestation/1.0/CustomContext.json" + ], + "reference": "SERIAL_NUMBER_123", + "name": "Republic of Singapore Driving Licence", + "issuanceDate": "2010-01-01T19:23:24Z", + "validFrom": "2010-01-01T19:23:24Z", + "issuer": { + "id": "https://example.com", + "type": "OpenAttestationIssuer", + "name": "DEMO STORE" + }, + "type": [ + "VerifiableCredential", + "DrivingLicenceCredential", + "OpenAttestationCredential" + ], + "credentialSubject": { + "id": "did:example:SERIAL_NUMBER_123", + "class": [ + { + "type": "3", + "effectiveDate": "2010-01-01T19:23:24Z" + }, + { + "type": "3A", + "effectiveDate": "2010-01-01T19:23:24Z" + } + ] + }, + "openAttestationMetadata": { + "template": { + "name": "CUSTOM_TEMPLATE", + "type": "EMBEDDED_RENDERER", + "url": "https://localhost:3000/renderer" + }, + "proof": { + "type": "OpenAttestationProofMethod", + "method": "DID", + "value": "did:ethr:0xB26B4941941C51a4885E5B7D3A1B861E54405f90", + "revocation": { + "type": "REVOCATION_STORE", + "location": "0xc7dfB2D05ab3Da91e723F2557817165e6acEAc38" + } + }, + "identityProof": { + "type": "DID", + "identifier": "did:ethr:0xB26B4941941C51a4885E5B7D3A1B861E54405f90" + } + }, + "attachments": [ + { + "fileName": "sample.pdf", + "mimeType": "application/pdf", + "data": "BASE64_ENCODED_FILE" + } + ], + "proof": { + "type": "OpenAttestationMerkleProofSignature2018", + "proofPurpose": "assertionMethod", + "targetHash": "b9b6de4ef4f05981b26d46bda0089473c1219cea6fe18dfb82e5f7adb110fc02", + "proofs": [], + "merkleRoot": "b9b6de4ef4f05981b26d46bda0089473c1219cea6fe18dfb82e5f7adb110fc02", + "salts": "W3sidmFsdWUiOiI1NzY1MzJkMzdmYWJhYTFiNGIxNGMxMjQ4YWYzM2RjNTQ3MzVmYjk0MDhiMGY3NjJhMzY3NTdmYjA4YjA0MDNjIiwicGF0aCI6InZlcnNpb24ifSx7InZhbHVlIjoiOTVhZjU3MDYzZTg2NjYxZTEwOWI3MWM3YmI1ZDg0YTQ1ODUyZWVjYjNmNjM2NjUyZWZmNDVlYTdkMjZiMDMxZiIsInBhdGgiOiJAY29udGV4dFswXSJ9LHsidmFsdWUiOiI1MmE1N2YzZWExOTBiZjZmNTBhODRkMjVhOWE1Zjk2ODk1ZDkyMTUxZDVmNTM0OTc5NTFmN2Y2MDI0ZDU2MWNmIiwicGF0aCI6IkBjb250ZXh0WzFdIn0seyJ2YWx1ZSI6ImYyMDQyMGU4NjYyYjVjYjIyNmQ4N2Q3N2Y0OTEwMWQwZjQ0MTEzY2VkNGY2Y2VkMzlhODQzNzdlNmZkOTIyOTAiLCJwYXRoIjoiQGNvbnRleHRbMl0ifSx7InZhbHVlIjoiZDMxYTNmNGE2NTk0NjA3NjE4MDExNGY1ZWJhMTA2MzE3NTRiMWE1YWY4MDE1ZDEzOGZkNzU1MjIxN2Y3ZTk2NiIsInBhdGgiOiJAY29udGV4dFszXSJ9LHsidmFsdWUiOiIyMmE2NDJhMTQxYTRmNGMwNzdiYjRmZjI1M2U1NmNjNzIzMGU0YWJkNjgwNzFiMDdlNDM4Y2U2ZjUzZmI3ZWQ4IiwicGF0aCI6InJlZmVyZW5jZSJ9LHsidmFsdWUiOiI0MDhlNjk4ZmI2YWJkYzZlOTk4NjA2MDg4YjgzNmYyYjliNGM2NTkxZGY2ZmExODhmZGRkZjYwMDE3M2EyMzNhIiwicGF0aCI6Im5hbWUifSx7InZhbHVlIjoiZGIzY2U2MGY1ODc5N2YxN2RjNzk0YjcwZGQxZTQzNmY0M2E0NzU5YmViZTY3NDNiNDZhODRhZjc4MGQ0Njg2MiIsInBhdGgiOiJpc3N1YW5jZURhdGUifSx7InZhbHVlIjoiOGM1MWFmMDVlZTM4YmM1YmUyYzU2YzdkYTIwOTA2NWRkODc5YjUzYjBiZTI1NmI2MjZhZTJiZGYzZmNlOWJmZiIsInBhdGgiOiJ2YWxpZEZyb20ifSx7InZhbHVlIjoiMzc4ZGM5MzgwODU0N2FkNzYyZjAyN2JlY2FhZjZiOWFkYTk1MTNlMzQ0ZDk4YTNkMDMyNDgxMDQwZjVmZTNmMiIsInBhdGgiOiJpc3N1ZXIuaWQifSx7InZhbHVlIjoiOGFkNTgwYzc0YjkyZTAzNTJhMDA4OWQ5NzZhNTc0MGVkMDU1ZjViMjQ2ZmU2N2VjMGEwZjFhMDI5MmE5NzI3MSIsInBhdGgiOiJpc3N1ZXIudHlwZSJ9LHsidmFsdWUiOiI0ZWFjY2Y1NjhmYTYzOTc1ZDY1NDVmYTcwZTgwMmNjMmM2YmQxZTE0ZmIwZmEyZmQyZjA4OWY2YjJhOTc2YmI1IiwicGF0aCI6Imlzc3Vlci5uYW1lIn0seyJ2YWx1ZSI6ImU1ZDdiMTZlOTY3ODgzZGRmYjUzY2NkZjUwZjUyNjQ3NDdiZTdhZjkwNmExZjU5ZWUyNWNmZWM1YTUyNTRiZGYiLCJwYXRoIjoidHlwZVswXSJ9LHsidmFsdWUiOiJlM2Q3ZGRiOGQ2MWZlN2I1ZDFjYTFjNWVhNjExYjJjZmYwNDVjOTMxMGZmODRjNTFhNTdiNzMxMzQ0ZDYxYTFiIiwicGF0aCI6InR5cGVbMV0ifSx7InZhbHVlIjoiMTRlMjQwNDc1MjdkNjgyODQxZWViNmMwNGFhZGIxYWZhNzZjMDRhMTY4NzBhMzc5ODJjYWM5NTY3NzlkMWVmMCIsInBhdGgiOiJ0eXBlWzJdIn0seyJ2YWx1ZSI6ImI3NjZjNTcxNTE5YWU5ZGEyZDlmOGM3NmRiNTNmM2ExOWFmYjE3ODM4NjJhMmRiNzk5YjkwYmU0Y2UwZThhOTEiLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3QuaWQifSx7InZhbHVlIjoiNGIyNWRmMDRkYTJiNjI4MmQwMmU3ZGEzYTI3MWRjZWVmMjZlMDNiMmUyNDU5NDMwOWFjZTJmMDAyZGUwNGM4MyIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC5jbGFzc1swXS50eXBlIn0seyJ2YWx1ZSI6ImQwMTgyNzFmYmRhZTNmNTZlMWMwMTI5Y2E3MGM2ZjEwZGRlYTg5NDNlNWUzODQwNWE5YjY5NTdmMjUzNGNmYTQiLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3QuY2xhc3NbMF0uZWZmZWN0aXZlRGF0ZSJ9LHsidmFsdWUiOiJiOWY4ODY5ZmQ1Yzc4MzcwMGZjOTY0OGE5NzliOGE0MGNiZjk4NDFhN2EzZmUzOTMyZDljODYxMzE3YWI1ZWMyIiwicGF0aCI6ImNyZWRlbnRpYWxTdWJqZWN0LmNsYXNzWzFdLnR5cGUifSx7InZhbHVlIjoiMjQ2ODRiOWQxYTZkMmMzN2YxZmE5ZjI2ZmM0ZDc3MWMzNmI5OTk0ODUwOGI3NmRmM2U3MWE5MGY2MDk3YjFlYSIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC5jbGFzc1sxXS5lZmZlY3RpdmVEYXRlIn0seyJ2YWx1ZSI6IjQxMWI4ODY1YWJlYWFjOThhNTZlODYzM2EzNDgyOGNkNDllNmViMDdkODRlYmI4OTQ5ZWIxOGNjYzU1OTRhMDAiLCJwYXRoIjoib3BlbkF0dGVzdGF0aW9uTWV0YWRhdGEudGVtcGxhdGUubmFtZSJ9LHsidmFsdWUiOiI0ZjQyYzY5MDg5Y2MwODg0OWE1MTNjZjM0MzRkZTRjMTVlMTc4NWZmYWNhNGIyMzdmYTYwNmY5NjA2ODhkYTM2IiwicGF0aCI6Im9wZW5BdHRlc3RhdGlvbk1ldGFkYXRhLnRlbXBsYXRlLnR5cGUifSx7InZhbHVlIjoiNTVkNmEyNTA5MTE3MTlhYjllZTViYWFlYTU2ZTJlOTQ0ZjFhMzU1ZjJjZDM0YWIxYmI3MjI1NWJkY2YxZmUxZCIsInBhdGgiOiJvcGVuQXR0ZXN0YXRpb25NZXRhZGF0YS50ZW1wbGF0ZS51cmwifSx7InZhbHVlIjoiMTJjMGM4NjkwZjAxYWE4NTVjMzZiZjVjN2Q0YjU4NzA5OTJkYWQ3NDVmMDNjOGI3Mzc0YmJlOTcwNWQzMDEyOSIsInBhdGgiOiJvcGVuQXR0ZXN0YXRpb25NZXRhZGF0YS5wcm9vZi50eXBlIn0seyJ2YWx1ZSI6ImFjMDNjYWMxMjRkMjU0ZGIxZjA3ODg1ZDUwZThkYTdkZjc5NWYyMmUzZTQ2ZGNmYWU1YjA5OTljMGYyZDhmOTMiLCJwYXRoIjoib3BlbkF0dGVzdGF0aW9uTWV0YWRhdGEucHJvb2YubWV0aG9kIn0seyJ2YWx1ZSI6IjI3NzNiZDNlNzI2ZjFjZGQ5YTU5NGE0MWZhNWZjMjQ4YmI3MGRjYmIyYmMyOTk2MjE4MWNlZDZkNDBjMWU4NWEiLCJwYXRoIjoib3BlbkF0dGVzdGF0aW9uTWV0YWRhdGEucHJvb2YudmFsdWUifSx7InZhbHVlIjoiNGU4NGEzMzI1MWE4NmVkZDQ3ZTc3YjcxZThkMzYwMzAxMjM3NDJlN2RlOWVmNDM4NGFkZDFjMmJhYjY1ZmM4MiIsInBhdGgiOiJvcGVuQXR0ZXN0YXRpb25NZXRhZGF0YS5wcm9vZi5yZXZvY2F0aW9uLnR5cGUifSx7InZhbHVlIjoiNzNlOTJiNzJhNTc3MDQyNTIzZjdjZjVjMGZkNGFkZTEyZDNkMDdlZjdkOTQ5ZGY4NTY5MDU1MGY0Y2IxNWQzNCIsInBhdGgiOiJvcGVuQXR0ZXN0YXRpb25NZXRhZGF0YS5wcm9vZi5yZXZvY2F0aW9uLmxvY2F0aW9uIn0seyJ2YWx1ZSI6ImYyMjMwYmVkZTI5Yzg3ZGYyN2RmYjBiYTI2N2FjMTQ2OThiNDk5MDBiZjkzMDM3ZDJjNGI1M2QyNjNjOTEzZjUiLCJwYXRoIjoib3BlbkF0dGVzdGF0aW9uTWV0YWRhdGEuaWRlbnRpdHlQcm9vZi50eXBlIn0seyJ2YWx1ZSI6IjkyYTQyOGNiNDdhNmY5ZWVjNGFhMmE2YTkxYzg0NTlkZTdiMmEyNDgxNDEwMTg5ZmMyNWM3Mzk3Mjg0NGU3NzciLCJwYXRoIjoib3BlbkF0dGVzdGF0aW9uTWV0YWRhdGEuaWRlbnRpdHlQcm9vZi5pZGVudGlmaWVyIn0seyJ2YWx1ZSI6IjFhMTM1MWZmOTU0MTNkZDk4MzFhOTgzZjE5YWVkNTI3MjgxZTk3ODQ1OThmNWNkMzU3NmE5ODE3ODRlM2M1OGEiLCJwYXRoIjoiYXR0YWNobWVudHNbMF0uZmlsZU5hbWUifSx7InZhbHVlIjoiZjBhZTUyZjE0MDcwMjIyYTZlY2QyMDgxNWE2OTA4ZWQwYWFjNzIyOGY3M2M2MjZjNGI0YzU3NGE0ZjAyMTkxZCIsInBhdGgiOiJhdHRhY2htZW50c1swXS5taW1lVHlwZSJ9LHsidmFsdWUiOiIyYjRhM2I5NWY5NzJiMzZkMjk3ZjNiYjFmN2UzMjk5YWM4Y2EzZmI1NmY4NWNjMzI4Nzg0M2VhZjdkZjE3NWEzIiwicGF0aCI6ImF0dGFjaG1lbnRzWzBdLmRhdGEifV0=", + "privacy": { + "obfuscated": [] + }, + "key": "did:ethr:0xB26B4941941C51a4885E5B7D3A1B861E54405f90#controller", + "signature": "0x291b04727f134a0259a8bde0db05a5428b262c46d16d47c55ff77555c36baaf6687750d81515b70c18e0bbabb121e8378d4f4c9e111382345f5e9e6b9e19d6331c" + } +} diff --git a/libs/server-common/src/fixtures/oav3/did-revocation-store-signed-revoked.json b/libs/server-common/src/fixtures/oav3/did-revocation-store-signed-revoked.json new file mode 100644 index 00000000..54925b89 --- /dev/null +++ b/libs/server-common/src/fixtures/oav3/did-revocation-store-signed-revoked.json @@ -0,0 +1,76 @@ +{ + "version": "https://schema.openattestation.com/3.0/schema.json", + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://schemata.openattestation.com/com/openattestation/1.0/DrivingLicenceCredential.json", + "https://schemata.openattestation.com/com/openattestation/1.0/OpenAttestation.v3.json", + "https://schemata.openattestation.com/com/openattestation/1.0/CustomContext.json" + ], + "reference": "SERIAL_NUMBER_123", + "name": "Republic of Singapore Driving Licence", + "issuanceDate": "2010-01-01T19:23:24Z", + "validFrom": "2010-01-01T19:23:24Z", + "issuer": { + "id": "https://example.com", + "type": "OpenAttestationIssuer", + "name": "DEMO STORE" + }, + "type": [ + "VerifiableCredential", + "DrivingLicenceCredential", + "OpenAttestationCredential" + ], + "credentialSubject": { + "id": "did:example:SERIAL_NUMBER_123", + "class": [ + { + "type": "3", + "effectiveDate": "2010-01-01T19:23:24Z" + }, + { + "type": "3A", + "effectiveDate": "2010-01-01T19:23:24Z" + } + ] + }, + "openAttestationMetadata": { + "template": { + "name": "CUSTOM_TEMPLATE", + "type": "EMBEDDED_RENDERER", + "url": "https://localhost:3000/renderer" + }, + "proof": { + "type": "OpenAttestationProofMethod", + "method": "DID", + "value": "did:ethr:0xB26B4941941C51a4885E5B7D3A1B861E54405f90", + "revocation": { + "type": "REVOCATION_STORE", + "location": "0xc7dfB2D05ab3Da91e723F2557817165e6acEAc38" + } + }, + "identityProof": { + "type": "DID", + "identifier": "did:ethr:0xB26B4941941C51a4885E5B7D3A1B861E54405f90" + } + }, + "attachments": [ + { + "fileName": "sample.pdf", + "mimeType": "application/pdf", + "data": "BASE64_ENCODED_FILE" + } + ], + "proof": { + "type": "OpenAttestationMerkleProofSignature2018", + "proofPurpose": "assertionMethod", + "targetHash": "fdd40a012f38dacedad0d7fbe68726ac47f7836c6019f9824329207b9d7e6bb1", + "proofs": [], + "merkleRoot": "fdd40a012f38dacedad0d7fbe68726ac47f7836c6019f9824329207b9d7e6bb1", + "salts": "W3sidmFsdWUiOiIxMTU4NmE2ZWIzM2UxYTliNjc5MGIyMTNjZmNlNmQ3YjcyNzBkZjEwNTFhM2E1YWEzNWI4ZDc0N2Y5ZWQ0YzJkIiwicGF0aCI6InZlcnNpb24ifSx7InZhbHVlIjoiZmRmN2U4NDI5YjM2YTAxYzM0MDY1NWZmZmVjMGI2YjJhZjNlY2Q0ZmVkOWYxYjI5MmRhNTYwMmI1NDMzZTI5YyIsInBhdGgiOiJAY29udGV4dFswXSJ9LHsidmFsdWUiOiIyNGJlNmQyNzBiNTNjMmZkZWE4ZDliZjEyMDY3NzQ3NzYxODhiNTAzODdhZDdkNzlmOTY2Mzg2MDFjODVlMTgxIiwicGF0aCI6IkBjb250ZXh0WzFdIn0seyJ2YWx1ZSI6IjFiY2NlYWJiMDQ2NWQ3MjUzOWJkNzY2ZTNjNDFlZTM0YzhkNGE0MjE3ZWQ2Y2UxZWVjOTM2NWViYjYzNzljZDQiLCJwYXRoIjoiQGNvbnRleHRbMl0ifSx7InZhbHVlIjoiMWI5MjUxNWQ2YzEzMmY3NTdjMGQ4MWRkMDY0Njc4ZGE2N2ZiNTU2NGRhMmNhNjk0MGZiOWQ4N2JiN2ZmOGJjZCIsInBhdGgiOiJAY29udGV4dFszXSJ9LHsidmFsdWUiOiIzZjA1ZmUwODQwNDZmZmFlN2FjNTUwMTBjODI5ZTMyNGY3ODAxNmFkNzQzMWJkYjlhYWU0NjcyMDEyMDNhYmFkIiwicGF0aCI6InJlZmVyZW5jZSJ9LHsidmFsdWUiOiI3NDYwYzQ2YmYyYTJhOWZhNGY1NWQ3Yjg0ZjUyZTk5NDllMTJiOTBhYThhYjZmZTk0ZWFiOWNlMzk5ODQ2Y2RjIiwicGF0aCI6Im5hbWUifSx7InZhbHVlIjoiOTkxM2JhYzBjMTRkNDY1ZjUxODY2OTQ1ODFiYzBmNTY4NDQwMGU4NzA0ZTQ4NWUxZjA4YTY5NjhhYjU0NzQ1MCIsInBhdGgiOiJpc3N1YW5jZURhdGUifSx7InZhbHVlIjoiYTA4OTJlZmZiODgxN2U2OGFiOGM2NzY0YWU1NGVhYmMxMWNiOGI3YTgzYWI1MGRkOTYyMDhmNWM3NDk3ZDNlNyIsInBhdGgiOiJ2YWxpZEZyb20ifSx7InZhbHVlIjoiODhhMGU5M2ZmYjk3OThkOGIyODlmODc1MzlhMWZlMzhiNTA2MWU5NTYyN2VhY2E0YTU2M2IyZDMxZGNiNjJiMiIsInBhdGgiOiJpc3N1ZXIuaWQifSx7InZhbHVlIjoiZWRlMDA4NmRkMjU3ZGRjNGJmMjA0ZDExMjYwYmMwZTlkZWM4YTNkZmVjOTZmMjBmNjM1YTE2Mzc2M2ZhM2ZjNiIsInBhdGgiOiJpc3N1ZXIudHlwZSJ9LHsidmFsdWUiOiIyMjkxZmFlOTM2Y2VjZjM1YWEzNzBjNjBmNTE5NTNhMmE5OGY1NmFiOTZhMjA3YTBmNzg2ZjJjYTRkMDQwMmI4IiwicGF0aCI6Imlzc3Vlci5uYW1lIn0seyJ2YWx1ZSI6IjlkMjYyODY2ODYwYjIwZDlmNDIzYTM5OGM3Y2Y4YTIzZmVmYThhOGJiOGNiM2Q4ZGM5MWVkM2FiYjZkZGYxOTUiLCJwYXRoIjoidHlwZVswXSJ9LHsidmFsdWUiOiIzNmRjNDIzMDE2ZGM3MjIzM2QzNjUyYzI2ZmFiOGE2Yjk1YmUwNmVhMzMwNDBjMjBkZTI2ZDdhOTJlMjllNDVlIiwicGF0aCI6InR5cGVbMV0ifSx7InZhbHVlIjoiY2EwMTBmNTJlOWRmNTI5ZDE3ZDc2M2NiNzhhNDY2ODkzMjE2ZjBkNjE3ODZhNGRiYmNmNmVhNzMwMTg2YzdjZCIsInBhdGgiOiJ0eXBlWzJdIn0seyJ2YWx1ZSI6IjQzZDUxZGFhZWIwOWI3MWRjNWM4MDFkMjQzZDZjZjQ4ZGYwODQ1YjEzMWNlZWQ4NGNmNWQ3MTI1MTM0M2EyZDYiLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3QuaWQifSx7InZhbHVlIjoiYTM3Yjc4OWRjZWVlMjk4ZWJiNDJiZGE5OTZjNWY1ZmFhODU1MDM4YmZiZTlkODk3MTdjMDVjNjA5YmUzMmNmZSIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC5jbGFzc1swXS50eXBlIn0seyJ2YWx1ZSI6IjUyNjAwMGI2MmQyNDQzMDQ5NjVhMjY4ZDdlZDBmODNhNGE3Y2MyZGQ0ZTg3NWJlYWE0N2U1ZGQ2ZGQ1Y2ZjMDMiLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3QuY2xhc3NbMF0uZWZmZWN0aXZlRGF0ZSJ9LHsidmFsdWUiOiI1YmU0ZDUwNTc2MGYwMDc1N2NkN2MwYzk3ZWE4YTkzOTBmOGE2ZTIzNTVjNzZmMzAyYzY3YTNmNGY2MWUwYTg3IiwicGF0aCI6ImNyZWRlbnRpYWxTdWJqZWN0LmNsYXNzWzFdLnR5cGUifSx7InZhbHVlIjoiNjljNjM2ZWM0MTQwZmNiYzM0ODI0ZmRhNjMzMzA3Y2ZjOWZlNjlhYTRhOGYxYWY1NGQ2Yzg2ZDZjZTgzYzlhNiIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC5jbGFzc1sxXS5lZmZlY3RpdmVEYXRlIn0seyJ2YWx1ZSI6IjU1NWI4YjI3YzkxZmE0MzRkYTNmNWQ3Y2YwMjMzNTg2ZjZmNzQ4ODUwM2RiNTc2ZDczMGIyNjIyM2VjNDVjNWYiLCJwYXRoIjoib3BlbkF0dGVzdGF0aW9uTWV0YWRhdGEudGVtcGxhdGUubmFtZSJ9LHsidmFsdWUiOiJjOTRlNThjZjhkMTdlNTMzNThlZjYzMmVlOTY3NjkwYjFkYzhlZDI1YmEwNGZiNzM3YWMwMjU0MmMwZGJmODVhIiwicGF0aCI6Im9wZW5BdHRlc3RhdGlvbk1ldGFkYXRhLnRlbXBsYXRlLnR5cGUifSx7InZhbHVlIjoiOGVlN2YxMzM2ZjcxNzY2ZTkzOWYxNTNmM2M5OWI0Y2QxOGZjMTQxNjZkZDlmNTM2MDllMTkxOThlNWVkMmY0MCIsInBhdGgiOiJvcGVuQXR0ZXN0YXRpb25NZXRhZGF0YS50ZW1wbGF0ZS51cmwifSx7InZhbHVlIjoiYWRiOTZiNThhZjg3NTIyNTY4NmZjNmYwNzFiNTc4NTkxMzNhODllYmRiNDg3ZTk0ZWRjOWMyZTAzNTAwZGVmZiIsInBhdGgiOiJvcGVuQXR0ZXN0YXRpb25NZXRhZGF0YS5wcm9vZi50eXBlIn0seyJ2YWx1ZSI6ImU0ZWY3Mjc0MTU4N2U5ZGUwNjc3YjM4MmExNjhjNDM4YjQyYmUyMmU0OGQ1NDg5NDNhOWMwZTU0ZjMzMzcwNjQiLCJwYXRoIjoib3BlbkF0dGVzdGF0aW9uTWV0YWRhdGEucHJvb2YubWV0aG9kIn0seyJ2YWx1ZSI6IjhiYTM2OWI0N2FhZGZiMmZkYjFkNzZjMjUzM2IwYTA3NmM4OGUxOGFkNzljMTJiM2M0MDA0YWQwMWMzNzI4YjAiLCJwYXRoIjoib3BlbkF0dGVzdGF0aW9uTWV0YWRhdGEucHJvb2YudmFsdWUifSx7InZhbHVlIjoiNzdlZGRiMjg0MzUzMTE0YzdhN2JjMmQzNGQ5NjZmNDAyM2RkYmY1ZGUxMWNlZmI0NGZiNThlYzFmNDgwYTU5OSIsInBhdGgiOiJvcGVuQXR0ZXN0YXRpb25NZXRhZGF0YS5wcm9vZi5yZXZvY2F0aW9uLnR5cGUifSx7InZhbHVlIjoiMzFmZTc2MWI2YWY0YjQ2MGE1ZDY2MDBlZWNjZWRiZjZlNWY1NjEwMzBmNGEyNTM5MmI0YTY0MDg3MjE1NDQ1MSIsInBhdGgiOiJvcGVuQXR0ZXN0YXRpb25NZXRhZGF0YS5wcm9vZi5yZXZvY2F0aW9uLmxvY2F0aW9uIn0seyJ2YWx1ZSI6IjBiZTljYWMxN2MwOGE1NzA4MmJiZDEwNjk2MWU4OWU0OTg4OGEyNjYxZTAzM2MxOWNlOGU0ZTE4MjE2YzA0MTgiLCJwYXRoIjoib3BlbkF0dGVzdGF0aW9uTWV0YWRhdGEuaWRlbnRpdHlQcm9vZi50eXBlIn0seyJ2YWx1ZSI6IjY4ZGNkYzFiOGYwMjZhZjIxNzQ1OTY5ODNhOWIzNDIyZGQxNmM3YmE0OWI4MWJjMTVlNzE0N2Y5ODhmZjNiMmIiLCJwYXRoIjoib3BlbkF0dGVzdGF0aW9uTWV0YWRhdGEuaWRlbnRpdHlQcm9vZi5pZGVudGlmaWVyIn0seyJ2YWx1ZSI6IjYxMzJmMmQ2MTcwODljZWZjNTkzZjdiNzYwOGJhMmExOTI0MjM4MWFmOWJiZjAzZDFhYzExYmQ3ODA0OGJmODgiLCJwYXRoIjoiYXR0YWNobWVudHNbMF0uZmlsZU5hbWUifSx7InZhbHVlIjoiZWI4NmEzN2MyNjk3YWRlZmVkMzU2OTRlM2FhN2MzYzc4YTZmMWNhZmRkNTAzMjYzMDI4ZTJhNjc2YjdhMWVkMiIsInBhdGgiOiJhdHRhY2htZW50c1swXS5taW1lVHlwZSJ9LHsidmFsdWUiOiJkMjQxMTM2ZDNhODIwNjk0YmUzYWZjMWIwYmY2OTNjOWI0ODQ2YzcyODVkYzFmN2I3OTVkMjRiMmY0ODE2M2UxIiwicGF0aCI6ImF0dGFjaG1lbnRzWzBdLmRhdGEifV0=", + "privacy": { + "obfuscated": [] + }, + "key": "did:ethr:0xB26B4941941C51a4885E5B7D3A1B861E54405f90#controller", + "signature": "0x2ff1a3310d67226a6be6b4be1dcc1a75ba0085b08c415c55867aa637420fdd1774f0a4c765bc54e60a97fd90538636e13e22642ac4197e36cc6d840e380d653c1c" + } +} diff --git a/libs/server-common/src/fixtures/oav3/did-signed.json b/libs/server-common/src/fixtures/oav3/did-signed.json new file mode 100644 index 00000000..f90282be --- /dev/null +++ b/libs/server-common/src/fixtures/oav3/did-signed.json @@ -0,0 +1,79 @@ +{ + "version": "https://schema.openattestation.com/3.0/schema.json", + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://schemata.openattestation.com/com/openattestation/1.0/DrivingLicenceCredential.json", + "https://schemata.openattestation.com/com/openattestation/1.0/OpenAttestation.v3.json", + "https://schemata.openattestation.com/com/openattestation/1.0/CustomContext.json" + ], + "reference": "SERIAL_NUMBER_123", + "name": "Republic of Singapore Driving Licence", + "issuanceDate": "2010-01-01T19:23:24Z", + "validFrom": "2010-01-01T19:23:24Z", + "issuer": { + "id": "https://example.com", + "type": "OpenAttestationIssuer", + "name": "DEMO STORE" + }, + "type": [ + "VerifiableCredential", + "DrivingLicenceCredential", + "OpenAttestationCredential" + ], + "credentialSubject": { + "id": "did:example:SERIAL_NUMBER_123", + "class": [ + { + "type": "3", + "effectiveDate": "2010-01-01T19:23:24Z" + }, + { + "type": "3A", + "effectiveDate": "2010-01-01T19:23:24Z" + } + ] + }, + "openAttestationMetadata": { + "template": { + "name": "CUSTOM_TEMPLATE", + "type": "EMBEDDED_RENDERER", + "url": "https://localhost:3000/renderer" + }, + "proof": { + "type": "OpenAttestationProofMethod", + "method": "DID", + "value": "did:ethr:0x1245e5B64D785b25057f7438F715f4aA5D965733", + "revocation": { + "type": "NONE" + } + }, + "identityProof": { + "type": "DID", + "identifier": "did:ethr:0x1245e5B64D785b25057f7438F715f4aA5D965733" + } + }, + "attachments": [ + { + "fileName": "sample.pdf", + "mimeType": "application/pdf", + "data": "BASE64_ENCODED_FILE" + } + ], + "proof": { + "type": "OpenAttestationMerkleProofSignature2018", + "proofPurpose": "assertionMethod", + "targetHash": "82f51d6eb620e4264dff0ac2b9d99a965a88ff51e46192bb4808ea969ee67402", + "proofs": [ + "a1c633145bc0f37105fe510d335376c1919a6cf51030628877288bdee5541c22", + "c3d7c5908f25eba67baf7f607932f1924acdb7a6cf04ad5408dba251bf0a47bc", + "94c07ddcc4a2ade59e3120dce9f19f0f4ad80a58943555f4b51af4668b1c1d62" + ], + "merkleRoot": "f43045b0c57072a044e810b798e32b8c1de1d0d0c5774d55c8eed1f3fdec6438", + "salts": "W3sidmFsdWUiOiIyMTJjMDNmYmIzNmMyMjY2NTU0OGM5OGM5ZjE2ZTYwYTc2MjBjZGM0ZjczMjY3NDMxYzA0ZjYzY2U0MTYxMmVkIiwicGF0aCI6InZlcnNpb24ifSx7InZhbHVlIjoiMDRjZTAzMDliNWU3NTNmOTFiMjdmNmE2Nzg2N2VkZWY2OTY3ZmU4YzAyZmEwNTE5ZjY3Mzc3NDI1ZWI5OTE5ZiIsInBhdGgiOiJAY29udGV4dFswXSJ9LHsidmFsdWUiOiIzNWQ2Njg2YTZjYTI1NmQ3NWRmMzE2YmJlZjUxMjYwYzdiZGIxZWJkODRlZTM3OTM3OTI1MGYxYTVkNjk3ZGRlIiwicGF0aCI6IkBjb250ZXh0WzFdIn0seyJ2YWx1ZSI6IjMyZjIyZjJmMzQwYTgwNmRkMTE0NzQ3NjQ3ZTkwZjlmNjNjNmY4ZDUyMzk5YzI0OWVhN2U4YWZmMWVhNjA3MGUiLCJwYXRoIjoiQGNvbnRleHRbMl0ifSx7InZhbHVlIjoiMGE2NWFkNjQxZmRjYTkxNTdmMmQ5M2UyNmUzZTM4NmU3MTg3NTQyNWI0M2E4OTZkNmJlNWMxY2Q0MTkwNTFhOCIsInBhdGgiOiJAY29udGV4dFszXSJ9LHsidmFsdWUiOiJhZjEzZWMzODgyM2VkODIyZmNjY2Q5YWY2YWRmOWUxNWFjZWIzZjNmZmE2Njk2OGMyZjhmMjk2MzYxNDRhNzYwIiwicGF0aCI6InJlZmVyZW5jZSJ9LHsidmFsdWUiOiI4MDAxNjA5OTUzZmFiYzZjZTZjOGIyOTVmMDdjNTM2MDhhMjQ0ZWIxMmFmMzJlZjQyYjZmYTc1MmFmNDAzOTYxIiwicGF0aCI6Im5hbWUifSx7InZhbHVlIjoiMzc5NDQyY2NiZWMyMTYxNWZmNzM4MDQ2MmEzZjlmYmUyZjc1MmQ5M2IwZGQyZjRjNTM4MGJlYWZmZDVlMjA3OSIsInBhdGgiOiJpc3N1YW5jZURhdGUifSx7InZhbHVlIjoiN2M0MzYyMmNjMTk2ZDJmOWQ5NDU2NjM3NWU2NzRlODk2OTNkYjRjZDA5YjE1MjI0MjJjODIyZGYxN2EyYjI0ZCIsInBhdGgiOiJ2YWxpZEZyb20ifSx7InZhbHVlIjoiNTIwN2M1NGY1MWNhMzlmYWJmZWM3NmFlMDU3YzEzYTc3Y2Q1YTQ3YTVkYTZkYzcwMGQwYjVkNzZhZDQxOTYwNiIsInBhdGgiOiJpc3N1ZXIuaWQifSx7InZhbHVlIjoiODExZWU1MmE0MTQ0MTMzNjc4OWEwOWYzNjIwODcwYWVmNTAzNzEzYzE4YTFlNjY2YzU1MTk5ZjhiYTY5ODRlNCIsInBhdGgiOiJpc3N1ZXIudHlwZSJ9LHsidmFsdWUiOiJiOWUzZDllYjNhYzUwMThjNTEzMDFjNjNkNTIyMWMzY2M3NTZlOGFjYzcxYmY4ZmQxMjlhYmMyNDA5OGRhNzRmIiwicGF0aCI6Imlzc3Vlci5uYW1lIn0seyJ2YWx1ZSI6Ijc1M2FjYTQ3NmI4YWRhNjBjNjQ2ZmUwZjhmMTNiNjhlMzBkMDU5NGVkY2NkYTAxMDJmZTVmMmI0OGRmY2ZlZjYiLCJwYXRoIjoidHlwZVswXSJ9LHsidmFsdWUiOiJjMWY4NDM1YmI4YzQxMzkyMDFlMGY1ZjYwZDkwMDI1M2RkNmJhZDJkODljYjg4N2JlNzdkNmRjYTQ3NmQxMzMxIiwicGF0aCI6InR5cGVbMV0ifSx7InZhbHVlIjoiZWY2MTllMDAzNWRkYThlODhlNTQwZTExMjIyOTk0ZjM4YTM0NTBhYThjM2I5ZjQ1MDFmMmQ3ZmFmN2UzYTNhYiIsInBhdGgiOiJ0eXBlWzJdIn0seyJ2YWx1ZSI6ImFjODRlNjUzMjNhMWQ1MjdiNTQzMmQ3NmM5NWMxOWQ3ODM3MzExOTg1YzY0ODg1NjY1ZDU2NTEyYTE2MDI3NWIiLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3QuaWQifSx7InZhbHVlIjoiMDhiYWI3ZGY4YzA1N2JlZGUwYzE4NTA3MTcwMjU5NjkwMDg5MjM0ZDE1NGQyNzlhMThiMzExY2YwZTUyZDVlNCIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC5jbGFzc1swXS50eXBlIn0seyJ2YWx1ZSI6IjFhZDhhYmQyMzhhNmE5NzhjYzdlMmE0NzIwN2FhYjRlZjM1Njc3MDAwMTlmNDg0YzMzMTlkYTI3YzAzMjIwNTEiLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3QuY2xhc3NbMF0uZWZmZWN0aXZlRGF0ZSJ9LHsidmFsdWUiOiJiMzdjMzBjNGUzMGM1ZGI4MGZiMzYxMGFkM2U0NmZmMzNmNTc2ZTQ3MGIzZjNjNmIzMDI4ZGUzYWYwNTRjOTVkIiwicGF0aCI6ImNyZWRlbnRpYWxTdWJqZWN0LmNsYXNzWzFdLnR5cGUifSx7InZhbHVlIjoiMTVhNDE2YjhjNWI0MmM3N2E4MGMzYmZmMTRlZmY2YWQ3NzMyMTZmYjAzZjc1OTJiMzM5MGIxZThlMDY1NmJjMiIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC5jbGFzc1sxXS5lZmZlY3RpdmVEYXRlIn0seyJ2YWx1ZSI6ImQ5NzgyNmEyMjI0YTI4MDE2NDVhMmZjOTVmYWFiNzU1NjkzOGE4ZTcwZWNmNDY2ZDE5NzlmYTYzODNjZWRmN2UiLCJwYXRoIjoib3BlbkF0dGVzdGF0aW9uTWV0YWRhdGEudGVtcGxhdGUubmFtZSJ9LHsidmFsdWUiOiI5NzIxMmEwMmQ0NGVlZDA5OTA0YzBhZGY2NTkxNzg0OGQ2YmExMjQxMWIwNTI3Y2IzZjNiNDk0NzQ3Nzc5ODI3IiwicGF0aCI6Im9wZW5BdHRlc3RhdGlvbk1ldGFkYXRhLnRlbXBsYXRlLnR5cGUifSx7InZhbHVlIjoiNTRjZDQ5ZmMxMGUxZjdhN2RmOTE2ZmQ0OGQwNTAzYmY4ODU5NjNhMTZkN2U2MDFiNTFlOWMwZGQxNjkxMTEwOCIsInBhdGgiOiJvcGVuQXR0ZXN0YXRpb25NZXRhZGF0YS50ZW1wbGF0ZS51cmwifSx7InZhbHVlIjoiZDBiMzc0ZDlkZWQxNmZkNDkyZmVlYmZlMTZlOWQ0MWMwMWRhNTgxMmI5NjQ3MmNiNWEzOWQ2MTA5M2JhYjNiNCIsInBhdGgiOiJvcGVuQXR0ZXN0YXRpb25NZXRhZGF0YS5wcm9vZi50eXBlIn0seyJ2YWx1ZSI6IjdkNzRkMjUwZjRhNGU1ZDY5Y2RiM2YxNTJhNmJlNTVhYzNjMWYwYWYxZDMzY2RlNTYxMzBjODc1OTIwYWYxMDMiLCJwYXRoIjoib3BlbkF0dGVzdGF0aW9uTWV0YWRhdGEucHJvb2YubWV0aG9kIn0seyJ2YWx1ZSI6IjUxYmFhY2RjZGM5ZTk4NTg0NTkyMDU2MDhmM2Q5Y2YwYmI5YTI1NmI0ZGUxOGIxMWJlNjU0OTMyN2I5MGUxMzgiLCJwYXRoIjoib3BlbkF0dGVzdGF0aW9uTWV0YWRhdGEucHJvb2YudmFsdWUifSx7InZhbHVlIjoiNzFjZThjMjM3ZGZjNjkxYzBlZDBlMTRkYWM5YmIzOTg5OTJhNWQwNGM3MDlmM2EwNjI0NDE1MTNlZjYxNDhlYiIsInBhdGgiOiJvcGVuQXR0ZXN0YXRpb25NZXRhZGF0YS5wcm9vZi5yZXZvY2F0aW9uLnR5cGUifSx7InZhbHVlIjoiMjQwNWY4NmE5NGNkY2Y5NzExYzI0NzRlZTBmMDc5MDRiNThmZmRmYjNhNjMyMWNjYWEwMDRhZGJlZmIxMjRmZiIsInBhdGgiOiJvcGVuQXR0ZXN0YXRpb25NZXRhZGF0YS5pZGVudGl0eVByb29mLnR5cGUifSx7InZhbHVlIjoiOTdlZmE5MjdlMTVmMGFlYmIxNmEyOWY3NjM2YTVlZmRjYmNhYWU5YzdhM2MwMGFmZDczZmM3MDUzMzNhMjZmOCIsInBhdGgiOiJvcGVuQXR0ZXN0YXRpb25NZXRhZGF0YS5pZGVudGl0eVByb29mLmlkZW50aWZpZXIifSx7InZhbHVlIjoiZTUzYWQ0ODk1Mjg3YTExZjE1MzRiNDRmNDI5ZDgyOTlkZGFmOTkzMTNjZGE1MGE2ZTAyMTE0ZTc2Yjg0NGY4ZSIsInBhdGgiOiJhdHRhY2htZW50c1swXS5maWxlTmFtZSJ9LHsidmFsdWUiOiIxNDU0MGY2ZGViMDBjOGI2OTc3MmE4ZTljNzU1OGU0MTgxMGI1Y2FmYTQzNGQ1NWFjMTIxZTk3MmE1NDY0MDMzIiwicGF0aCI6ImF0dGFjaG1lbnRzWzBdLm1pbWVUeXBlIn0seyJ2YWx1ZSI6IjNiZTUyODM3ZjMwNzcyNDYzZjY1NmM4ZDQ3ZGM1ZWRiZjcyMmY4ZDQ5ZWFiZTI0NWVjZGM2ZWJhODIxNzFjYTAiLCJwYXRoIjoiYXR0YWNobWVudHNbMF0uZGF0YSJ9XQ==", + "privacy": { + "obfuscated": [] + }, + "key": "did:ethr:0x1245e5B64D785b25057f7438F715f4aA5D965733#controller", + "signature": "0x4a054aaf881da5130a3b19160a7f0ce1afef1fe093c83be438c8c40b0a04ace5142f3d48a23c8ce7c32fabe04fc69e6fd4c94cf3ceb4adb4f3da5fa937208d4e1c" + } +} diff --git a/libs/server-common/src/fixtures/oav3/did-wrapped.json b/libs/server-common/src/fixtures/oav3/did-wrapped.json new file mode 100644 index 00000000..3662ad34 --- /dev/null +++ b/libs/server-common/src/fixtures/oav3/did-wrapped.json @@ -0,0 +1,73 @@ +{ + "version": "https://schema.openattestation.com/3.0/schema.json", + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://schemata.openattestation.com/com/openattestation/1.0/DrivingLicenceCredential.json", + "https://schemata.openattestation.com/com/openattestation/1.0/OpenAttestation.v3.json", + "https://schemata.openattestation.com/com/openattestation/1.0/CustomContext.json" + ], + "reference": "SERIAL_NUMBER_123", + "name": "Republic of Singapore Driving Licence", + "issuanceDate": "2010-01-01T19:23:24Z", + "validFrom": "2010-01-01T19:23:24Z", + "issuer": { + "id": "https://example.com", + "type": "OpenAttestationIssuer", + "name": "DEMO STORE" + }, + "type": [ + "VerifiableCredential", + "DrivingLicenceCredential", + "OpenAttestationCredential" + ], + "credentialSubject": { + "id": "did:example:SERIAL_NUMBER_123", + "class": [ + { + "type": "3", + "effectiveDate": "2010-01-01T19:23:24Z" + }, + { + "type": "3A", + "effectiveDate": "2010-01-01T19:23:24Z" + } + ] + }, + "openAttestationMetadata": { + "template": { + "name": "CUSTOM_TEMPLATE", + "type": "EMBEDDED_RENDERER", + "url": "https://localhost:3000/renderer" + }, + "proof": { + "type": "OpenAttestationProofMethod", + "method": "DID", + "value": "did:ethr:0xE712878f6E8d5d4F9e87E10DA604F9cB564C9a89", + "revocation": { + "type": "NONE" + } + }, + "identityProof": { + "type": "DID", + "identifier": "did:ethr:0xE712878f6E8d5d4F9e87E10DA604F9cB564C9a89" + } + }, + "attachments": [ + { + "fileName": "sample.pdf", + "mimeType": "application/pdf", + "data": "BASE64_ENCODED_FILE" + } + ], + "proof": { + "type": "OpenAttestationMerkleProofSignature2018", + "proofPurpose": "assertionMethod", + "targetHash": "39d61d63571aa26b1cf8b84f8e26b34f3c43ea1819583c2ebcd8a382843e4bf1", + "proofs": [], + "merkleRoot": "39d61d63571aa26b1cf8b84f8e26b34f3c43ea1819583c2ebcd8a382843e4bf1", + "salts": "W3sidmFsdWUiOiIxOTg5OThhNWM0MTVhMDAwMjZlNGUwZmI3NzRmMzVmMDA4MjJmM2IyODBlN2YwOWRkYmY3MTVhM2YzNzFlZTk0IiwicGF0aCI6InZlcnNpb24ifSx7InZhbHVlIjoiZjQyY2QxZjFiYjViZGEzM2Q3NDhlZjFlZTE4ZDQ2ZThmNzRhOWFmMzA1NjcxMGI5NjNjMmIzYTVmMDQwZjA5YSIsInBhdGgiOiJAY29udGV4dFswXSJ9LHsidmFsdWUiOiJiN2I0MzIyZDc4MzEwYjQ3NjA2ZmQzYjIxNWU4YTNlZTYzYzZjNjY5MWEzNjAxNGRjMDU3NzE3ZTM5NWFiMjI3IiwicGF0aCI6IkBjb250ZXh0WzFdIn0seyJ2YWx1ZSI6IjRiZDEyNWVkYjNiMjUxMzllMTRhYjRmZjkzNTk0YjFhZjIzZDk1MTkwMDQyMjI2Yzk0MzlkMTM4OTQ2ZTQ5NGQiLCJwYXRoIjoiQGNvbnRleHRbMl0ifSx7InZhbHVlIjoiMjhmMWZiMjUwYThlNWE1ZTBhNjkzMTM1ZmVkN2NlNGM5MzY2MTYyN2I0YTRhYWRlNWFiYTgwMjZkODQ5MjdjOCIsInBhdGgiOiJAY29udGV4dFszXSJ9LHsidmFsdWUiOiJmMTBjZTcwZGZhMTk4ODljZTI2MTVjYjJiODhkYjZjYWMyMjFjY2E5MWY5YTQwMDQ2NmEzMGQxODYzMjY3ZWViIiwicGF0aCI6InJlZmVyZW5jZSJ9LHsidmFsdWUiOiJlMjhiZmY0Yjk1MDkzMTNhMTQzMjk4OWFmZDQxMjAzOWFlYzMyNTQxNzEyMzIwMzk4NDgyZDM4OGFjNDhhZWFjIiwicGF0aCI6Im5hbWUifSx7InZhbHVlIjoiZTE1YTFkODgzMjA0MTM1NGRiMTdlZGI4ZmJkNTNkMzNjYjM2Y2VkNDM1NTFkNjkxNDgxYWI3YzM1YWE4Nzc1ZSIsInBhdGgiOiJpc3N1YW5jZURhdGUifSx7InZhbHVlIjoiZGVmZTc2NjVkY2NiNmM5MWM4YjFjOGI5OWYwNWYyM2ExNzhmZTY1YzBhZjJhNTdiZjhlNzMyMmVjMjkwZGMwYyIsInBhdGgiOiJ2YWxpZEZyb20ifSx7InZhbHVlIjoiMzFhYzZkNTA1OWIxYmNiODE5ZWUwODhhZjk2MDJmZTMxMGE0YzMyNDFkYTM2NTUzYzIwNWVlNGUxNTQ3OWQzZiIsInBhdGgiOiJpc3N1ZXIuaWQifSx7InZhbHVlIjoiMTgzNWQ3ODUyYTUwMWNhODA0NGE0N2Q3MjNiZGQxNzQyNmM5ZmE3OTVmYjg5Y2ZiOTZlNWRmZTk2ODY0YjllNiIsInBhdGgiOiJpc3N1ZXIudHlwZSJ9LHsidmFsdWUiOiJhZDQxODU4OWM1MTIyNjE2N2Q5OWE0ODYzN2QzMTFkMDYxYjE0YmNiY2IyZTE4MGRhMTg4ODY5YWRiNDM0ZjNmIiwicGF0aCI6Imlzc3Vlci5uYW1lIn0seyJ2YWx1ZSI6ImVhYTU4NGRjODZmYTM0ZTVhZTIwN2MwZjNiYTI2NTFiNmZiZmI1MTlkZmY4M2U4OWIyY2JlMTJjOTJhZjJlMjMiLCJwYXRoIjoidHlwZVswXSJ9LHsidmFsdWUiOiI5ODFhYTFkNjliNGNhZjRjZDk2OTA4NDNjYmQwYzE4NjA1NjlkMWRjZDY2N2QzYTg4ZTRiMjlkYjg4NWI0MmU2IiwicGF0aCI6InR5cGVbMV0ifSx7InZhbHVlIjoiOGU5NjY4ZTkyOTYxMDJiNWU0MTlhMjcyYzhhMjE1ZTYyNzg2ZGY1ZWFlZTNiN2MzNjU2YjQ3NzExYjg4MmJlYiIsInBhdGgiOiJ0eXBlWzJdIn0seyJ2YWx1ZSI6ImMzNzcxNGUzNzhkMDg2ZWIzYjAxNDhmNTk4ZjQ3MmQ0M2M3ZjU4ZjkyNjRjN2MxM2ViMDFhMGIyYzcyNThmZjUiLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3QuaWQifSx7InZhbHVlIjoiZTJhYWU0ZmVmMDk3OTNjNjk0MWZjNjAzNzQ0ZDhmN2RmZmE4Y2I4ZTY1M2E3OThkZDI1ZWU2ZGJmMWU0MWRhYyIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC5jbGFzc1swXS50eXBlIn0seyJ2YWx1ZSI6IjE5ZTlmNjdlYTZlMmZiMDhjNzY0NjJiZGE3ZTQzNjJjZjU3MjE1MjUxZjI3YjU2MDFmYTIwMjQ4YzQyY2NlOGQiLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3QuY2xhc3NbMF0uZWZmZWN0aXZlRGF0ZSJ9LHsidmFsdWUiOiIzMDEyMTc3MTNmZTI2YjU5ZjVhMjNkNjI3MDYxMGI4MDkxM2NhY2RjNTk2YjJjNDYyODY1M2M4MDE5MGRkYzMyIiwicGF0aCI6ImNyZWRlbnRpYWxTdWJqZWN0LmNsYXNzWzFdLnR5cGUifSx7InZhbHVlIjoiMzg5ZDRmMmVkMzY1MDIxOWIzM2Y2YzdlZDg0OGQ2OTNiZGU4ZDE1MTkzYTY3MzI4NWFlM2NiNGI0ZjIwMTczYiIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC5jbGFzc1sxXS5lZmZlY3RpdmVEYXRlIn0seyJ2YWx1ZSI6ImNkZWU0NjM2YTU0MjY1ZGE4OGE1YWQwMmY2M2M0MjgwZWIzMWRkNmQ1ZDcwYzcwYzE1MTk3ODIzNTU1NmZmZGEiLCJwYXRoIjoib3BlbkF0dGVzdGF0aW9uTWV0YWRhdGEudGVtcGxhdGUubmFtZSJ9LHsidmFsdWUiOiI2YjQyNTNhMDJkMGNkMmEzYTdiMGQzNGQ5YWVkNzYyNTFlNTdmZjIyOTcxY2U4ZGM2MmNmYTljMzA5MjlkMzY2IiwicGF0aCI6Im9wZW5BdHRlc3RhdGlvbk1ldGFkYXRhLnRlbXBsYXRlLnR5cGUifSx7InZhbHVlIjoiZGFjMWRkZWRhZmE3M2VmM2UzNTA3ODQyOWM1OTIzMjFhNmRkZGQ5MzhlYzc1ZWJlZGVlZmY1NmFkOTk0YjJmOSIsInBhdGgiOiJvcGVuQXR0ZXN0YXRpb25NZXRhZGF0YS50ZW1wbGF0ZS51cmwifSx7InZhbHVlIjoiYWZlMDhiZDI1NjE4ZTQ5OWYzNjEwYTI3Y2JiYjIzYTVhYTI4NjU5MzhiNzQ4ZDZiMTNmYjhhMDk4ZjA2ZWQ2MyIsInBhdGgiOiJvcGVuQXR0ZXN0YXRpb25NZXRhZGF0YS5wcm9vZi50eXBlIn0seyJ2YWx1ZSI6ImI4YThiNDg1NTkxNTM0YzAyNTVjNzRlOTc0MmI3ZjVkMDI5NmQyZTM2MWE4N2FhMDZmZGEwOTM0NWUyZjU5NGEiLCJwYXRoIjoib3BlbkF0dGVzdGF0aW9uTWV0YWRhdGEucHJvb2YubWV0aG9kIn0seyJ2YWx1ZSI6ImFjYmQ5NzgwNTk2Y2Y4OTI2NWMxNjJhYjU4MGRmOTI4N2VhNGM0NTE4YmM4ZTNkMDcwNWFlYzAwMTZjMzMwYjIiLCJwYXRoIjoib3BlbkF0dGVzdGF0aW9uTWV0YWRhdGEucHJvb2YudmFsdWUifSx7InZhbHVlIjoiNjUzNzEyODY3NDNkMWM1NTE1ZDdkYzZmNTUxODY4NmRjNzg4OWQ1OWFiZmQxZTljMTQ2N2Y3OGM3YWUxZmVkNiIsInBhdGgiOiJvcGVuQXR0ZXN0YXRpb25NZXRhZGF0YS5wcm9vZi5yZXZvY2F0aW9uLnR5cGUifSx7InZhbHVlIjoiNzhhYWZjNjQ2ZWM2MmIxZTVjNDcyNjRkZTk2MTY0NWZkNDdhMGI4NjQ1MzcyMjYwZjVkZDgxN2I3ZWEyY2JmMSIsInBhdGgiOiJvcGVuQXR0ZXN0YXRpb25NZXRhZGF0YS5pZGVudGl0eVByb29mLnR5cGUifSx7InZhbHVlIjoiMDM2MjhhMmRiZmEzOTBkNDY2NTk5ZjNkOTY2ZDU1ZTNjOWU5ZjM4Y2QzMTU5ZGNmYTA0NTlhODM1NGM0ZDc4MiIsInBhdGgiOiJvcGVuQXR0ZXN0YXRpb25NZXRhZGF0YS5pZGVudGl0eVByb29mLmlkZW50aWZpZXIifSx7InZhbHVlIjoiNThjNzRlODVmNmY2MDA0ODQwZmNmZWIzM2JmNTMzMTliMTcxODBkMzU4ZjZkMzBhMjQwNTVhZTgxMDIxNDFkZCIsInBhdGgiOiJhdHRhY2htZW50c1swXS5maWxlTmFtZSJ9LHsidmFsdWUiOiIyZjYyYjNhNmY0MGE3YTc2MWQ4OWIzOGE1YWU4MjQ4OWJkNDA5OWQ2YzNkMjk5NzIxYWQ3MjZjOGFiNWViYmYxIiwicGF0aCI6ImF0dGFjaG1lbnRzWzBdLm1pbWVUeXBlIn0seyJ2YWx1ZSI6IjhjYjI5NmMwY2NjYWQwYzc3NzVhMGQ1MGRkODM3NTA1YTg3NmQwMDViOWFhZmJmNmZjN2FiM2JjNDZkM2IwYzEiLCJwYXRoIjoiYXR0YWNobWVudHNbMF0uZGF0YSJ9XQ==", + "privacy": { + "obfuscated": [] + } + } +} diff --git a/libs/server-common/src/fixtures/oav3/did.json b/libs/server-common/src/fixtures/oav3/did.json new file mode 100644 index 00000000..0a4c873a --- /dev/null +++ b/libs/server-common/src/fixtures/oav3/did.json @@ -0,0 +1,62 @@ +{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://schemata.openattestation.com/com/openattestation/1.0/DrivingLicenceCredential.json", + "https://schemata.openattestation.com/com/openattestation/1.0/OpenAttestation.v3.json", + "https://schemata.openattestation.com/com/openattestation/1.0/CustomContext.json", + "https://dev-dvp-context.s3.ap-southeast-2.amazonaws.com/AANZFTA-CoO.json" + ], + "reference": "SERIAL_NUMBER_123", + "name": "Republic of Singapore Driving Licence", + "issuanceDate": "2010-01-01T19:23:24Z", + "validFrom": "2010-01-01T19:23:24Z", + "issuer": { + "id": "did:ethr:0x5aaA29b606d730E96a61eD5745D109f82a87A9C8", + "type": "OpenAttestationIssuer", + "name": "DEMO STORE" + }, + "type": [ + "VerifiableCredential", + "DrivingLicenceCredential", + "OpenAttestationCredential" + ], + "credentialSubject": { + "id": "did:example:SERIAL_NUMBER_123", + "class": [ + { + "type": "3", + "effectiveDate": "2010-01-01T19:23:24Z" + }, + { + "type": "3A", + "effectiveDate": "2010-01-01T19:23:24Z" + } + ] + }, + "openAttestationMetadata": { + "template": { + "name": "CUSTOM_TEMPLATE", + "type": "EMBEDDED_RENDERER", + "url": "https://localhost:3000/renderer" + }, + "proof": { + "type": "OpenAttestationProofMethod", + "method": "DID", + "value": "did:ethr:0x5aaA29b606d730E96a61eD5745D109f82a87A9C8", + "revocation": { + "type": "NONE" + } + }, + "identityProof": { + "type": "DID", + "identifier": "did:ethr:0x5aaA29b606d730E96a61eD5745D109f82a87A9C8" + } + }, + "attachments": [ + { + "fileName": "sample.pdf", + "mimeType": "application/pdf", + "data": "BASE64_ENCODED_FILE" + } + ] +} diff --git a/libs/server-common/src/fixtures/oav3/document-v2.json b/libs/server-common/src/fixtures/oav3/document-v2.json new file mode 100644 index 00000000..a779aa48 --- /dev/null +++ b/libs/server-common/src/fixtures/oav3/document-v2.json @@ -0,0 +1,32 @@ +{ + "id": "SERIAL_NUMBER_123", + "$template": { + "name": "CUSTOM_TEMPLATE", + "type": "EMBEDDED_RENDERER", + "url": "https://localhost:3000/renderer" + }, + "issuers": [ + { + "name": "DEMO STORE", + "tokenRegistry": "0x9178F546D3FF57D7A6352bD61B80cCCD46199C2d", + "identityProof": { + "type": "DNS-TXT", + "location": "tradetrust.io" + } + } + ], + "recipient": { + "name": "Recipient Name" + }, + "unknownKey": "Some value", + "credentialSubject": { + "id": "did:example:JOHN_DOE_DID", + "licenseNumber": "S1234567a", + "birthDate": "1977-02-22", + "name": "John Doe", + "class": [ + { "type": "3", "effectiveDate": "2010-01-01T19:23:24Z" }, + { "type": "3A", "effectiveDate": "2010-01-01T19:23:24Z" } + ] + } +} diff --git a/libs/server-common/src/fixtures/oav3/invalid_did_signed_v2.json b/libs/server-common/src/fixtures/oav3/invalid_did_signed_v2.json new file mode 100644 index 00000000..6e26d954 --- /dev/null +++ b/libs/server-common/src/fixtures/oav3/invalid_did_signed_v2.json @@ -0,0 +1,63 @@ +{ + "version": "https://schema.openattestation.com/2.0/schema.json", + "data": { + "id": "e1917cfe-70fa-4187-ac6d-ccc57c0d4645:string:SGCNM21566325", + "$template": { + "name": "95b39779-f300-43b7-9010-1eb96eafc6b5:string:CERTIFICATE_OF_NON_MANIPULATION", + "type": "43e9f19d-1ebd-485d-8cbf-2726c1f7c755:string:EMBEDDED_RENDERER", + "url": "7b0602b0-5e55-42de-813a-27c4f2b54d20:string:https://demo-cnm.openattestation.com" + }, + "issuers": [ + { + "id": "6002d4ab-d1a6-447e-9f86-945ee220fedb:string:did:ethr:0xE712878f6E8d5d4F9e87E10DA604F9cB564C9a89", + "name": "f46a0b69-983b-4b5e-83f6-aebe14625810:string:DEMO STORE", + "revocation": { + "type": "bcd9fe64-1b8a-41d3-a176-5d750b4a6cef:string:NONE" + }, + "identityProof": { + "type": "c2990f33-814c-4c7f-a14c-8cef4b1aa8ad:string:DID", + "key": "4ef2653f-7fb5-409b-acc1-a76344ff2de0:string:did:ethr:0xE712878f6E8d5d4F9e87E10DA604F9cB564C9a89#controller" + } + } + ], + "recipient": { + "name": "a5bcf044-94e4-421e-a0d6-960546dc3b41:string:SG FREIGHT", + "address": { + "street": "1b8dc011-05e0-405e-a687-4b2292a9455a:string:101 ORCHARD ROAD", + "country": "39f0508c-637c-407f-9087-c0d1239c9407:string:SINGAPORE" + } + }, + "consignment": { + "description": "6819ad4b-ffb2-427c-ac5a-6b812cbefff9:string:16667 CARTONS OF RED WINE", + "quantity": { + "value": "fb57e16f-d2f4-4caa-8c37-86514695a3ac:number:5000", + "unit": "b4259694-628d-4dc0-bce1-f7d34bab8a75:string:LITRES" + }, + "countryOfOrigin": "68c4b3bf-6ecf-4b55-894b-3def8c287c6f:string:AUSTRALIA", + "outwardBillNo": "2f5b4abf-47d8-4530-8e13-0f78688637e5:string:AQSIQ170923130", + "dateOfDischarge": "5dde46da-25cb-4721-96c8-a8f7fee4789a:string:2018-01-26", + "dateOfDeparture": "6cc4d4bd-52a4-4998-9cf7-c32a86e78fb0:string:2018-01-30", + "countryOfFinalDestination": "054ea1a5-37ae-4625-b8bf-d2ac43ddfcf2:string:CHINA", + "outgoingVehicleNo": "11ac7f5b-2839-4d27-b4fb-7ac77acb36c7:string:COSCO JAPAN 074E/30-JAN" + }, + "declaration": { + "name": "1954f051-1fde-4288-8468-5020dcc883a6:string:PETER LEE", + "designation": "07ff6d5c-40e5-496d-86fd-704866dc8e06:string:SHIPPING MANAGER", + "date": "3873ff50-223f-41d0-b37c-6572328594ae:string:2018-01-28" + } + }, + "signature": { + "type": "SHA3MerkleProof", + "targetHash": "cce7bd33bd80b746b71e943e23ddc88fcb99c9011becdd1b4d8b7ab9567d2adb", + "proof": [], + "merkleRoot": "cce7bd33bd80b746b71e943e23ddc88fcb99c9011becdd1b4d8b7ab9567d2adb" + }, + "proof": [ + { + "type": "OpenAttestationSignature2018", + "proofPurpose": "assertionMethod", + "verificationMethod": "did:ethr:0xE712878f6E8d5d4F9e87E10DA604F9cB564C9a89#controller", + "signature": "0xff0227ce8400a17a2d80073a95fd895f4fed0011954c90eef389bc618087a4b36ed958775420d122e9a6764c6ffe9d3302d4f45fb065d5e962c3572d3872f31a1c" + } + ] +} diff --git a/libs/server-common/src/fixtures/oav3/store_issued_v2.json b/libs/server-common/src/fixtures/oav3/store_issued_v2.json new file mode 100644 index 00000000..8975d8ee --- /dev/null +++ b/libs/server-common/src/fixtures/oav3/store_issued_v2.json @@ -0,0 +1,36 @@ +{ + "version": "https://schema.openattestation.com/2.0/schema.json", + "data": { + "$template": { + "type": "6c0ff4ec-a233-4e57-8ef5-97bea1efe687:string:EMBEDDED_RENDERER", + "name": "9efe8ff9-dd09-4dbd-8b3b-0252dac77c41:string:COVERING_LETTER", + "url": "297d310f-fa79-4ced-bbb1-ceb7277e1dd1:string:https://generic-templates.tradetrust.io" + }, + "issuers": [ + { + "name": "c9a5bb22-66ad-413f-8954-24270388bf11:string:Demo Issuer", + "documentStore": "982db3d5-4034-4690-ac25-85d97f2202d6:string:0x8bA63EAB43342AAc3AdBB4B827b68Cf4aAE5Caca", + "identityProof": { + "type": "b05da51b-a603-4ce1-8bfe-5069243e48c0:string:DNS-TXT", + "location": "260a874c-1d34-45c6-bc9b-28bc8eb3d8e8:string:demo-tradetrust.openattestation.com" + } + } + ], + "name": "5ba17180-e10e-405c-b999-ba236249b048:string:Covering Letter", + "logo": "ad97c7e9-f43a-4826-8460-aea1e62adb27:string:https://www.aretese.com/images/govtech-animated-logo.gif", + "title": "e72d118d-cb00-4934-aac8-927e8efeedba:string:Documents Bundle", + "remarks": "f413262a-c98c-4a14-a351-37b57f7ee639:string:Some very important documents in here for some submission", + "links": { + "self": { + "href": "5dcab418-e4e9-47b9-bca7-5147410d7d06:string:https://action.openattestation.com?q=%7B%22type%22%3A%22DOCUMENT%22%2C%22payload%22%3A%7B%22uri%22%3A%22https%3A%2F%2Fapi-ropsten.tradetrust.io%2Fstorage%2F79a7d6cc-fbef-4a31-9a88-c32ff99b4214%22%2C%22key%22%3A%222d7803aad8632ef2d80a0cf57ba543b3153748f48b37ac13eac6b4201b6bbe4a%22%2C%22permittedActions%22%3A%5B%22STORE%22%5D%2C%22redirect%22%3A%22https%3A%2F%2Fdev.tradetrust.io%2F%22%7D%7D" + } + } + }, + "signature": { + "created": "2021-03-25T07:52:31.291Z", + "type": "SHA3MerkleProof", + "targetHash": "77c8e74955a8df8b802ebbb498924b12ef133894c3fbf28a6ef18bd3bf0d9531", + "proof": [], + "merkleRoot": "77c8e74955a8df8b802ebbb498924b12ef133894c3fbf28a6ef18bd3bf0d9531" + } +} diff --git a/libs/server-common/src/fixtures/oav3/unsigned_v3_2.json b/libs/server-common/src/fixtures/oav3/unsigned_v3_2.json new file mode 100644 index 00000000..4c93428f --- /dev/null +++ b/libs/server-common/src/fixtures/oav3/unsigned_v3_2.json @@ -0,0 +1,58 @@ +{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://schemata.openattestation.com/com/openattestation/1.0/DrivingLicenceCredential.json", + "https://schemata.openattestation.com/com/openattestation/1.0/OpenAttestation.v3.json", + "https://schemata.openattestation.com/com/openattestation/1.0/CustomContext.json" + ], + "reference": "SERIAL_NUMBER_123", + "name": "Republic of Singapore Driving Licence", + "issuanceDate": "2010-01-01T19:23:24Z", + "validFrom": "2010-01-01T19:23:24Z", + "issuer": { + "id": "https://example.com", + "name": "DEMO STORE", + "type": "OpenAttestationIssuer" + }, + "type": [ + "VerifiableCredential", + "DrivingLicenceCredential", + "OpenAttestationCredential" + ], + "credentialSubject": { + "id": "did:example:JOHN_DOE_DID", + "licenseNumber": "S1234567a", + "birthDate": "1977-02-22", + "name": "John Doe", + "class": [ + { "type": "3", "effectiveDate": "2010-01-01T19:23:24Z" }, + { "type": "3A", "effectiveDate": "2010-01-01T19:23:24Z" } + ] + }, + "openAttestationMetadata": { + "template": { + "name": "DRIVING_LICENSE", + "type": "EMBEDDED_RENDERER", + "url": "https://tutorial-renderer.openattestation.com" + }, + "proof": { + "type": "OpenAttestationProofMethod", + "method": "DID", + "value": "did:ethr:0xE712878f6E8d5d4F9e87E10DA604F9cB564C9a89", + "revocation": { + "type": "NONE" + } + }, + "identityProof": { + "type": "DNS-DID", + "identifier": "example.tradetrust.io" + } + }, + "attachments": [ + { + "fileName": "sample.pdf", + "mimeType": "application/pdf", + "data": "BASE64_ENCODED_FILE" + } + ] +} diff --git a/libs/server-common/src/fixtures/oav3/v2_did_dns_signed.json b/libs/server-common/src/fixtures/oav3/v2_did_dns_signed.json new file mode 100644 index 00000000..1d480b3b --- /dev/null +++ b/libs/server-common/src/fixtures/oav3/v2_did_dns_signed.json @@ -0,0 +1,65 @@ +{ + "version": "https://schema.openattestation.com/2.0/schema.json", + "data": { + "id": "9a472a0a-42db-4559-baf2-90d6fcc2b113:string:SGCNM21566325", + "$template": { + "name": "e2da3963-d070-43e0-9cce-888886cd3173:string:CERTIFICATE_OF_NON_MANIPULATION", + "type": "f6b1b012-7dcb-4725-952d-228708746a21:string:EMBEDDED_RENDERER", + "url": "f64728c6-b985-465c-a31c-d2c98d5e055a:string:https://demo-cnm.openattestation.com" + }, + "issuers": [ + { + "id": "27f71dea-839c-4484-8a72-72f974a3c093:string:did:ethr:0xE712878f6E8d5d4F9e87E10DA604F9cB564C9a89", + "name": "cc8465bc-4432-47cf-94bf-0ee0c8c49c22:string:DEMO STORE", + "revocation": { + "type": "85debc04-1698-4a1b-b6ac-7ef7e8f9d4b4:string:NONE" + }, + "identityProof": { + "type": "767bd2d0-f1e4-4471-81a0-c4056d42f592:string:DNS-DID", + "key": "5edf0191-5492-4659-891b-84e68793c9be:string:did:ethr:0xE712878f6E8d5d4F9e87E10DA604F9cB564C9a89#controller", + "location": "ad412e6a-a9b6-40e6-bb17-18b097d86833:string:example.tradetrust.io" + } + } + ], + "recipient": { + "name": "243cdac1-8d75-47ca-a4f3-b9e305f16f50:string:SG FREIGHT", + "address": { + "street": "241f4d4f-ddeb-4344-a0e5-6a766b664c38:string:101 ORCHARD ROAD", + "country": "b66e2078-1bb2-41e3-b60b-0a16b6b79639:string:SINGAPORE" + } + }, + "consignment": { + "description": "77d0a5c6-e383-485c-b0bf-1439a1f67ae4:string:16667 CARTONS OF RED WINE", + "quantity": { + "value": "cd54295b-e569-4461-8b3d-5eae4f12f86d:number:5000", + "unit": "c4f4b1b4-3f05-469c-9f79-8eea65cfb9e1:string:LITRES" + }, + "countryOfOrigin": "89377474-29b7-44eb-8a2e-d2d8f4c2ba25:string:AUSTRALIA", + "outwardBillNo": "8c8e7f1b-6ec0-429f-8536-739b149507f9:string:AQSIQ170923130", + "dateOfDischarge": "32ef239d-d98a-4712-a40a-330d39d4db16:string:2018-01-26", + "dateOfDeparture": "a068e31d-9f2b-4698-b4d0-69bf9181d1d6:string:2018-01-30", + "countryOfFinalDestination": "2ef9d520-dd2c-4076-a2af-bf1e6bd9bd61:string:CHINA", + "outgoingVehicleNo": "099c443a-c51e-4446-ae2c-0ba90d6d2510:string:COSCO JAPAN 074E/30-JAN" + }, + "declaration": { + "name": "b9915d76-bf07-427d-952d-2c77eca55cc3:string:PETER LEE", + "designation": "d04e3577-515c-4fad-aa66-47319ce3b970:string:SHIPPING MANAGER", + "date": "ffd492e3-88f6-4803-a2eb-1a6395197909:string:2018-01-28" + } + }, + "signature": { + "type": "SHA3MerkleProof", + "targetHash": "d0ebc96b62001b10348d3f9931f91b3c7aa421445f9719a984d67c22465a86c5", + "proof": [], + "merkleRoot": "d0ebc96b62001b10348d3f9931f91b3c7aa421445f9719a984d67c22465a86c5" + }, + "proof": [ + { + "created": "2021-03-25T07:52:31.291Z", + "type": "OpenAttestationSignature2018", + "proofPurpose": "assertionMethod", + "verificationMethod": "did:ethr:0xE712878f6E8d5d4F9e87E10DA604F9cB564C9a89#controller", + "signature": "0xd05bb71bdb6f78451e2d12851825421666c6c5e355f516325ce5002a0586f89f6ebbd465227bec59c745dd26918dd8dab9122dcd398256d8e487e0ecf82a53421b" + } + ] +} diff --git a/libs/server-common/src/index.ts b/libs/server-common/src/index.ts new file mode 100644 index 00000000..f0c3d098 --- /dev/null +++ b/libs/server-common/src/index.ts @@ -0,0 +1,5 @@ +export * from './context'; +export * from './db'; +export * from './error'; +export * from './utils'; +export * from './vc'; diff --git a/libs/server-common/src/utils/environment.spec.ts b/libs/server-common/src/utils/environment.spec.ts new file mode 100644 index 00000000..b10b18bb --- /dev/null +++ b/libs/server-common/src/utils/environment.spec.ts @@ -0,0 +1,34 @@ +import { checkEnv } from './environment'; + +const variables = ['CLIENT', 'SECRET']; + +describe('environment', () => { + const OLD_ENV = process.env; + describe('checkEnv', () => { + afterEach(() => { + process.env = OLD_ENV; + }); + it('should pass the check when variables are defined', () => { + process.env = { + CLIENT: 'Postman', + SECRET: 'Pat', + }; + + expect(checkEnv(variables)).toStrictEqual({ + CLIENT: 'Postman', + SECRET: 'Pat', + }); + }); + it('should throw an error specifying missing variable(s) when variables are not defined', () => { + process.env = { + CLIENT: 'Postman', + }; + + expect(() => { + checkEnv(variables); + }).toThrow( + 'Missing the following required environment variable(s): SECRET' + ); + }); + }); +}); diff --git a/libs/server-common/src/utils/environment.ts b/libs/server-common/src/utils/environment.ts new file mode 100644 index 00000000..cbb130a3 --- /dev/null +++ b/libs/server-common/src/utils/environment.ts @@ -0,0 +1,23 @@ +/** + * Checks and returns environment variables. + * If variables are not defined then an error is thrown + * specifying the missing variables. + * + * @param vars + * @returns Partial + */ +export const checkEnv = (vars: string[]): { [key: string]: string } => { + const errs: string[] = []; + const res: { [key: string]: string } = {}; + vars.forEach((varName) => { + const val = process.env[varName]; + val ? (res[varName] = val) : errs.push(varName); + }); + if (errs.length) { + const missingVars = errs.join(', '); + throw Error( + `Missing the following required environment variable(s): ${missingVars}` + ); + } + return res; +}; diff --git a/libs/server-common/src/utils/index.ts b/libs/server-common/src/utils/index.ts new file mode 100644 index 00000000..334eeff6 --- /dev/null +++ b/libs/server-common/src/utils/index.ts @@ -0,0 +1,6 @@ +export * from './environment'; +export * from './logger'; +export * from './s3-adapter'; +export * from './test-helpers'; +export * from './uuid'; +export * from './vc'; diff --git a/libs/server-common/src/utils/logger.ts b/libs/server-common/src/utils/logger.ts new file mode 100644 index 00000000..ad99eda2 --- /dev/null +++ b/libs/server-common/src/utils/logger.ts @@ -0,0 +1,109 @@ +/* eslint-disable no-console */ +import pinoExpressLogger from 'express-pino-logger'; +import pino from 'pino'; +import { RequestInvocationContext } from '../context'; + +export const logger: pino.Logger = pino({ + name: process.env['API'], + level: process.env['LOG_LEVEL'] || 'debug', + messageKey: 'message', + timestamp: pino.stdTimeFunctions.isoTime, + redact: ['authorization', 'accessToken', 'invocationContext'], +}); + +const DEBUG_USE_CONSOLE = process.env['DEBUG_USE_CONSOLE'] === 'true'; + +export const expressLogger = pinoExpressLogger(); + +interface LogFn { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (msg: string, ...args: any[]): void; +} + +export class Logger { + /** pino logger instance */ + private logger: pino.Logger; + + /** + * Constructs a new Logger. + * This has been made private to ensure that logger instances may only be constructed using the + * static factory methods provided in this class. + * + * @param invocationContext The invocation context for the logger. If this is provided, then its + * field values will be used as defaults for any log entries. Note, however, that these + * defaults may still be overridden by providing specific values for fields when + * invoking a log method. + */ + private constructor(invocationContext?: RequestInvocationContext) { + this.logger = invocationContext + ? logger.child(invocationContext as pino.Bindings) + : logger; + } + + /** + * A factory method that obtains a Logger instance without an initial context. + * + * @return The Logger instance. + */ + public static get(): Logger { + return new Logger(); + } + + /** + * A factory method that obtains a Logger instance with the specified initial context. + * + * @param invocationContext The invocation context for the logger. + * @return The Logger instance. + */ + public static from(invocationContext?: RequestInvocationContext): Logger { + return new Logger(invocationContext); + } + + info: LogFn = (...args) => { + if (DEBUG_USE_CONSOLE) { + console.info(...args); + return; + } + this.logger.info(...args); + }; + + debug: LogFn = (...args) => { + if (DEBUG_USE_CONSOLE) { + console.debug(...args); + return; + } + this.logger.debug(...args); + }; + + trace: LogFn = (...args) => { + if (DEBUG_USE_CONSOLE) { + console.trace(...args); + return; + } + this.logger.trace(...args); + }; + + error: LogFn = (...args) => { + if (DEBUG_USE_CONSOLE) { + console.error(...args); + return; + } + this.logger.error(...args); + }; + + fatal: LogFn = (...args) => { + if (DEBUG_USE_CONSOLE) { + console.error(...args); + return; + } + this.logger.fatal(...args); + }; + + warn: LogFn = (...args) => { + if (DEBUG_USE_CONSOLE) { + console.warn(...args); + return; + } + this.logger.warn(...args); + }; +} diff --git a/libs/server-common/src/utils/s3-adapter.spec.ts b/libs/server-common/src/utils/s3-adapter.spec.ts new file mode 100644 index 00000000..1a24961d --- /dev/null +++ b/libs/server-common/src/utils/s3-adapter.spec.ts @@ -0,0 +1,101 @@ +import { + GetObjectCommand, + HeadObjectCommand, + DeleteObjectCommand, + S3Client, +} from '@aws-sdk/client-s3'; +import { mockClient } from 'aws-sdk-client-mock'; + +const s3Mock = mockClient(S3Client); + +export default function toReadableStream(value: string) { + return new ReadableStream({ + start(controller) { + controller.enqueue(value); + controller.close(); + }, + }); +} + +const mockAwsProviderConfig = { + bucketName: 'test', + clientConfig: { + region: 'test', + }, +}; + +import { S3Adapter } from './s3-adapter'; + +const testDocumentId = 'testId'; +const testEncryptedDocument = { + cipherText: 'testCipherText', + iv: 'testIv', + tag: 'testTag', + type: 'testType', +}; + +describe('S3Adapter', () => { + const s3StorageClient = new S3Adapter(mockAwsProviderConfig); + + it('should return a document if it exists', async () => { + s3Mock.on(GetObjectCommand).resolves({ + Body: { + transformToString: async () => + Promise.resolve(JSON.stringify(testEncryptedDocument)), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any, + }); + + const encryptedDocument = await s3StorageClient.getDocument(testDocumentId); + + expect(encryptedDocument).toStrictEqual(testEncryptedDocument); + }); + + it('should return null if body is empty', async () => { + s3Mock.on(GetObjectCommand).resolves({}); + expect(await s3StorageClient.getDocument(testDocumentId)).toBe(null); + }); + + it('should return null if document does not exists', async () => { + s3Mock + .on(GetObjectCommand) + .rejectsOnce(new Error('The specified key does not exist.')); + expect(await s3StorageClient.getDocument(testDocumentId)).toBe(null); + }); + + it('should return an error if unexpected error is thrown', async () => { + s3Mock.on(GetObjectCommand).rejects('test'); + await expect(s3StorageClient.getDocument(testDocumentId)).rejects.toThrow(); + }); + + describe('isDocumentExists', () => { + it("should return false if document doesn't exists", async () => { + s3Mock.on(HeadObjectCommand).rejectsOnce(new Error('not found')); + + const isDocumentExists = await s3StorageClient.isDocumentExists( + testDocumentId + ); + + expect(isDocumentExists).toBe(false); + }); + it('should return true if document exists', async () => { + s3Mock.on(HeadObjectCommand).resolves({}); + + const isDocumentExists = await s3StorageClient.isDocumentExists( + testDocumentId + ); + + expect(isDocumentExists).toBe(true); + }); + }); + + describe('deleteDocument', () => { + it('should return error if error is thrown', async () => { + s3Mock.on(DeleteObjectCommand).rejectsOnce({}); + + await expect( + s3StorageClient.deleteDocument(testDocumentId) + ).rejects.toThrowError(); + }); + }); +}); diff --git a/libs/server-common/src/utils/s3-adapter.ts b/libs/server-common/src/utils/s3-adapter.ts new file mode 100644 index 00000000..d318e9f1 --- /dev/null +++ b/libs/server-common/src/utils/s3-adapter.ts @@ -0,0 +1,94 @@ +import { + DeleteObjectCommand, + DeleteObjectCommandInput, + GetObjectCommand, + HeadObjectCommand, + PutObjectCommand, + PutObjectCommandInput, + S3Client, +} from '@aws-sdk/client-s3'; +import { + EncryptedDocument, + S3Config, + StorageClient, +} from '@dvp/api-interfaces'; +import { logger } from './logger'; + +export class S3Adapter implements StorageClient { + private s3Client: S3Client; + private bucket: string; + private documentStorePath: string; + + constructor(providerConfig: S3Config) { + this.bucket = providerConfig.bucketName; + this.s3Client = new S3Client(providerConfig.clientConfig); + this.documentStorePath = 'documents/'; + } + + getDocumentStorePath() { + return this.documentStorePath; + } + + async isDocumentExists(documentId: string) { + const params = { + Bucket: this.bucket, + Key: `${this.documentStorePath}${documentId}`, + }; + try { + await this.s3Client.send(new HeadObjectCommand(params)); + return true; + } catch (err) { + logger.debug( + "[S3Adapter.isDocumentExists] document doesn't exist for %s: %s", + documentId, + err + ); + return false; + } + } + + async getDocument(documentId: string) { + const params = { + Bucket: this.bucket, + Key: `${this.documentStorePath}${documentId}`, + }; + try { + const encryptedDocument = await this.s3Client.send( + new GetObjectCommand(params) + ); + + if (encryptedDocument?.Body) { + return JSON.parse(await encryptedDocument.Body.transformToString()) as { + document: EncryptedDocument; + }; + } + return null; + } catch (err: unknown) { + if ( + err instanceof Error && + err?.message === 'The specified key does not exist.' + ) { + return null; + } + throw err; + } + } + + async uploadDocument(document: string, documentId: string) { + const params: PutObjectCommandInput = { + Bucket: this.bucket, + Key: `${this.documentStorePath}${documentId}`, + Body: document, + }; + await this.s3Client.send(new PutObjectCommand(params)); + return documentId; + } + + async deleteDocument(documentId: string) { + const params: DeleteObjectCommandInput = { + Bucket: this.bucket, + Key: `${this.documentStorePath}${documentId}`, + }; + await this.s3Client.send(new DeleteObjectCommand(params)); + } +} diff --git a/libs/server-common/src/utils/test-helpers.ts b/libs/server-common/src/utils/test-helpers.ts new file mode 100644 index 00000000..d69a91ea --- /dev/null +++ b/libs/server-common/src/utils/test-helpers.ts @@ -0,0 +1,18 @@ +import { getMockReq } from '@jest-mock/express'; +import { RequestInvocationContext } from '../context'; + +export const uuidV4Regex = new RegExp( + /^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i +); + +export const getMockInvocationContext = (method: string, path: string) => { + const mockRequest = getMockReq({ + method, + headers: { + 'Correlation-ID': 'NUMPTYHEAD1', + }, + }); + + mockRequest.route = { path }; + return new RequestInvocationContext(mockRequest); +}; diff --git a/libs/server-common/src/utils/uuid.spec.ts b/libs/server-common/src/utils/uuid.spec.ts new file mode 100644 index 00000000..29ce0f52 --- /dev/null +++ b/libs/server-common/src/utils/uuid.spec.ts @@ -0,0 +1,7 @@ +import { getUuId } from './uuid'; + +describe('uuid', () => { + it('should return uuid', () => { + expect(getUuId()).toBeDefined(); + }); +}); diff --git a/libs/server-common/src/utils/uuid.ts b/libs/server-common/src/utils/uuid.ts new file mode 100644 index 00000000..9c47828a --- /dev/null +++ b/libs/server-common/src/utils/uuid.ts @@ -0,0 +1,3 @@ +import { v4 as uuidV4 } from 'uuid'; + +export const getUuId = () => uuidV4(); diff --git a/libs/server-common/src/utils/vc.spec.ts b/libs/server-common/src/utils/vc.spec.ts new file mode 100644 index 00000000..28a60be9 --- /dev/null +++ b/libs/server-common/src/utils/vc.spec.ts @@ -0,0 +1,65 @@ +import { + VerifiableCredential, + WrappedVerifiableCredential, +} from '@dvp/api-interfaces'; +import { NON_OA_CREDENTIAL, OA_CREDENTIAL, OA_SIGNED } from '../fixtures'; +import { + isOpenAttestationType, + isVerifiableCredential, + isGenericDocument, +} from './vc'; + +describe('isOpenAttestationType', () => { + it('should return true for document that contains OpenAttestation type', () => { + const result = isOpenAttestationType(OA_CREDENTIAL as never); + expect(result).toBe(true); + }); + + it('should return true for document that does not contain OpenAttestation type', () => { + const result = isOpenAttestationType(NON_OA_CREDENTIAL as never); + expect(result).toBe(false); + }); +}); + +describe('isVerifiableCredential', () => { + describe('OpenAttestation', () => { + it('should return true for a document that is a verifiable credential', () => { + const result = isVerifiableCredential( + OA_SIGNED as WrappedVerifiableCredential + ); + expect(result).toBe(true); + }); + + it('should return false for a document that is a not a verifiable credential', () => { + const result = isVerifiableCredential( + // eslint-disable-next-line + // @ts-ignore + OA_CREDENTIAL + ); + expect(result).toBe(false); + }); + }); +}); + +describe('isGenericDocument', () => { + it('should return true if the document contains the originalDocument property (generic)', () => { + const documentWithOriginalDocumentProperty = { + ...NON_OA_CREDENTIAL, + credentialSubject: { + ...NON_OA_CREDENTIAL.credentialSubject, + originalDocument: 'test123', + }, + }; + expect( + isGenericDocument( + documentWithOriginalDocumentProperty as VerifiableCredential + ) + ).toBe(true); + }); + + it('should return false if the document does not contain the originalDocument property', () => { + expect(isGenericDocument(NON_OA_CREDENTIAL as VerifiableCredential)).toBe( + false + ); + }); +}); diff --git a/libs/server-common/src/utils/vc.ts b/libs/server-common/src/utils/vc.ts new file mode 100644 index 00000000..9cfd8060 --- /dev/null +++ b/libs/server-common/src/utils/vc.ts @@ -0,0 +1,35 @@ +import { + VerifiableCredential, + WrappedVerifiableCredential, +} from '@dvp/api-interfaces'; +import { utils, validateSchema } from '@govtechsg/open-attestation'; + +export const isOpenAttestationType = ( + credential: VerifiableCredential | WrappedVerifiableCredential +) => { + if (credential?.type?.includes('OpenAttestationCredential')) { + return true; + } else { + return false; + } +}; + +export const isVerifiableCredential = ( + document: WrappedVerifiableCredential +) => { + if (isOpenAttestationType(document)) { + return ( + validateSchema(document) && + (utils.isWrappedV2Document(document) || + utils.isWrappedV3Document(document)) + ); + } else if (document?.type?.includes('VerifiableCredential')) { + return true; + } else { + return false; + } +}; + +export const isGenericDocument = (document: VerifiableCredential) => { + return 'originalDocument' in document.credentialSubject; +}; diff --git a/libs/server-common/src/vc/index.ts b/libs/server-common/src/vc/index.ts new file mode 100644 index 00000000..84e92821 --- /dev/null +++ b/libs/server-common/src/vc/index.ts @@ -0,0 +1,2 @@ +export * as openAttestation from './openAttestation'; +export * as transmute from './transmute'; diff --git a/libs/server-common/src/vc/openAttestation/index.ts b/libs/server-common/src/vc/openAttestation/index.ts new file mode 100644 index 00000000..25244c53 --- /dev/null +++ b/libs/server-common/src/vc/openAttestation/index.ts @@ -0,0 +1,2 @@ +export * from './issue'; +export * from './verify'; diff --git a/libs/server-common/src/vc/openAttestation/issue.ts b/libs/server-common/src/vc/openAttestation/issue.ts new file mode 100644 index 00000000..d6ce4ca7 --- /dev/null +++ b/libs/server-common/src/vc/openAttestation/issue.ts @@ -0,0 +1,31 @@ +import { + IssuerFunction, + OAVerifiableCredential, + VerifiableCredential, +} from '@dvp/api-interfaces'; +import { + signDocument, + SUPPORTED_SIGNING_ALGORITHM, + __unsafe__use__it__at__your__own__risks__wrapDocument as wrapDocumentV3, +} from '@govtechsg/open-attestation'; + +export const issueCredential: IssuerFunction = async ( + credential, + verificationMethod, + signingKey +): Promise => { + const wrappedDocument = await wrapDocumentV3( + credential as OAVerifiableCredential + ); + + const signedDocument = await signDocument( + wrappedDocument, + SUPPORTED_SIGNING_ALGORITHM.Secp256k1VerificationKey2018, + { + public: verificationMethod, // this will become the verificationMethod in the signed document. + private: signingKey, + } + ); + + return signedDocument as VerifiableCredential; +}; diff --git a/libs/server-common/src/vc/openAttestation/verify.spec.ts b/libs/server-common/src/vc/openAttestation/verify.spec.ts new file mode 100644 index 00000000..71f40f5e --- /dev/null +++ b/libs/server-common/src/vc/openAttestation/verify.spec.ts @@ -0,0 +1,29 @@ +import { DocumentsToVerify } from '@govtechsg/oa-verify'; +import invalid_OA_V3 from '../../fixtures/oav3/did-invalid-signed.json'; +import signed_OA_V3 from '../../fixtures/oav3/did-signed.json'; +import unsigned_OA_V3 from '../../fixtures/oav3/did.json'; +import { verifyCredential } from './verify'; + +describe('Test OA verify', () => { + jest.setTimeout(25000); + it('should verify a valid OA credential', async () => { + const verificationResult = await verifyCredential( + signed_OA_V3 as DocumentsToVerify + ); + expect(verificationResult).toHaveProperty('errors'); + expect(verificationResult.errors).toHaveLength(0); + }); + it('should fail DOCUMENT_INTEGRITY when signature is invalid', async () => { + const verificationResult = await verifyCredential( + invalid_OA_V3 as DocumentsToVerify + ); + expect(verificationResult).toHaveProperty('errors'); + expect(verificationResult.errors).toContain('proof'); + }); + it("should fail DOCUMENT_STATUS when it's unsigned and unwrapped", async () => { + const verificationResult = await verifyCredential(unsigned_OA_V3 as never); + expect(verificationResult).toHaveProperty('errors'); + expect(verificationResult.errors).toContain('status'); + expect(verificationResult.errors).toContain('proof'); + }); +}); diff --git a/libs/server-common/src/vc/openAttestation/verify.ts b/libs/server-common/src/vc/openAttestation/verify.ts new file mode 100644 index 00000000..d1b26197 --- /dev/null +++ b/libs/server-common/src/vc/openAttestation/verify.ts @@ -0,0 +1,66 @@ +import { VerificationResult } from '@dvp/api-interfaces'; +import { + DocumentsToVerify, + isValid, + openAttestationDidIdentityProof, + openAttestationDidSignedDocumentStatus, + openAttestationDnsDidIdentityProof, + openAttestationDnsTxtIdentityProof, + openAttestationHash, + utils, + verificationBuilder, + VerificationFragmentType, +} from '@govtechsg/oa-verify'; + +//TODO: production strategy +const ethProvider = utils.generateProvider({ + network: 'goerli', +}); + +const oaVerifiersToRun = [ + openAttestationHash, + openAttestationDidSignedDocumentStatus, + openAttestationDnsTxtIdentityProof, + openAttestationDnsDidIdentityProof, + openAttestationDidIdentityProof, +]; + +const builtVerifier = verificationBuilder(oaVerifiersToRun, { + provider: ethProvider, +}); + +export const verifyCredential = async ( + verifiableCredential: DocumentsToVerify +): Promise => { + //Which checks to do should be read from options, and credential contents + //(but currently is hard-coded) + const checks: VerificationFragmentType[] = [ + 'DOCUMENT_INTEGRITY', + 'DOCUMENT_STATUS', + 'ISSUER_IDENTITY', + ]; + + const translateOaCheckNames = ( + names: VerificationFragmentType[] + ): string[] => { + const translationMap = { + DOCUMENT_INTEGRITY: 'proof', + DOCUMENT_STATUS: 'status', + ISSUER_IDENTITY: 'identity', + }; + return names.map((checkName) => + checkName in translationMap ? translationMap[checkName] : checkName + ); + }; + + const fragments = await builtVerifier(verifiableCredential); + const failedOAChecks = checks.filter((checkName) => { + return !isValid(fragments, [checkName]); + }); + + return { + checks: translateOaCheckNames(checks), + errors: translateOaCheckNames(failedOAChecks), + warnings: [], + }; +}; diff --git a/libs/server-common/src/vc/transmute/documentLoader.spec.ts b/libs/server-common/src/vc/transmute/documentLoader.spec.ts new file mode 100644 index 00000000..b5e83ba3 --- /dev/null +++ b/libs/server-common/src/vc/transmute/documentLoader.spec.ts @@ -0,0 +1,68 @@ +import { constants, contexts } from '@transmute/did-context'; +import { DidDocument } from '@transmute/jsonld-document-loader'; + +import { documentLoader } from './documentLoader'; +import { resolvers } from './resolvers'; + +describe('documentLoader', () => { + it('should return an error for unsupported did', async () => { + await expect(documentLoader('fake')).rejects.toThrow( + `Unsupported iri: fake` + ); + }); + it('should fetch did document from static contexts if exists', async () => { + const res = await documentLoader(constants.DID_CONTEXT_TRANSMUTE_V1_URL); + expect(res).toEqual({ + document: contexts.get(constants.DID_CONTEXT_TRANSMUTE_V1_URL), + }); + }); + + it('should fetch did document for did key', async () => { + const didKey = 'did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd'; + + const didDocument: DidDocument = { + id: didKey, + }; + + const mockResolve = jest + .spyOn(resolvers, 'resolve') + .mockResolvedValue(didDocument); + const res = await documentLoader(didKey); + expect(res).toEqual({ + document: didDocument, + }); + expect(mockResolve).toBeCalledWith(didKey); + }); + + it('should fetch did document for did web', async () => { + const didWeb = 'did:web:example.com'; + + const didDocument: DidDocument = { + id: didWeb, + }; + const mockResolve = jest + .spyOn(resolvers, 'resolve') + .mockResolvedValue(didDocument); + const res = await documentLoader(didWeb); + expect(res).toEqual({ + document: didDocument, + }); + expect(mockResolve).toBeCalledWith(didWeb); + }); + + it('should fetch did document from an external source', async () => { + const did = 'http:example.com'; + + const didDocument = { + id: did, + }; + const mockHttp = jest + .spyOn(resolvers, 'http') + .mockResolvedValue(didDocument as DidDocument); + const res = await documentLoader(did); + expect(res).toEqual({ + document: didDocument, + }); + expect(mockHttp).toBeCalledWith(did); + }); +}); diff --git a/libs/server-common/src/vc/transmute/documentLoader.ts b/libs/server-common/src/vc/transmute/documentLoader.ts new file mode 100644 index 00000000..67d424b5 --- /dev/null +++ b/libs/server-common/src/vc/transmute/documentLoader.ts @@ -0,0 +1,23 @@ +import { contexts } from '@transmute/did-context'; +import { resolvers } from './resolvers'; + +export const documentLoader = async (iri: string) => { + if (iri) { + if (contexts.get(iri)) { + return { document: contexts.get(iri) }; + } + + if (iri.startsWith('did:')) { + const didDocument = await resolvers.resolve(iri); + return { document: didDocument }; + } + + if (iri.startsWith('http')) { + const document = await resolvers.http(iri); + return { document }; + } + } + + const message = 'Unsupported iri: ' + iri; + throw new Error(message); +}; diff --git a/libs/server-common/src/vc/transmute/generators.spec.ts b/libs/server-common/src/vc/transmute/generators.spec.ts new file mode 100644 index 00000000..e5520802 --- /dev/null +++ b/libs/server-common/src/vc/transmute/generators.spec.ts @@ -0,0 +1,15 @@ +import crypto from 'crypto'; +import { generators } from './generators'; +describe('generators', () => { + it('should generate did key', async () => { + const keys = await generators.didKey('ed25519'); + expect(keys).toBeDefined(); + }); + + it('should generate did key using private key passed', async () => { + const privateKey = crypto.randomBytes(32); + const keys = await generators.didKey('ed25519', privateKey); + + expect(keys).toBeDefined(); + }); +}); diff --git a/libs/server-common/src/vc/transmute/generators.ts b/libs/server-common/src/vc/transmute/generators.ts new file mode 100644 index 00000000..e036f357 --- /dev/null +++ b/libs/server-common/src/vc/transmute/generators.ts @@ -0,0 +1,19 @@ +import { DidGeneration } from '@dvp/api-interfaces'; +import * as did from '@transmute/did-key.js'; + +import crypto from 'crypto'; + +export const generators = { + didKey: async (type: string, seed?: Buffer): Promise => { + return did.key.generate({ + type, + accept: 'application/did+ld+json', + secureRandom: () => { + if (seed) { + return seed; + } + return crypto.randomBytes(32); + }, + }); + }, +}; diff --git a/libs/server-common/src/vc/transmute/index.ts b/libs/server-common/src/vc/transmute/index.ts new file mode 100644 index 00000000..25244c53 --- /dev/null +++ b/libs/server-common/src/vc/transmute/index.ts @@ -0,0 +1,2 @@ +export * from './issue'; +export * from './verify'; diff --git a/libs/server-common/src/vc/transmute/issue.spec.ts b/libs/server-common/src/vc/transmute/issue.spec.ts new file mode 100644 index 00000000..09a5cec7 --- /dev/null +++ b/libs/server-common/src/vc/transmute/issue.spec.ts @@ -0,0 +1,77 @@ +import { issueCredential } from './issue'; + +describe('issueCredential', () => { + jest.setTimeout(20000); + + it('should issue a verifiable credential', async () => { + const mnemonic = + 'coast lesson mountain spy inform deposit two trophy album endless party crumble base grape artefact'; + const credential = { + '@context': [ + 'https://www.w3.org/2018/credentials/v1', + 'https://w3id.org/vc-revocation-list-2020/v1', + ], + id: 'urn:uuid:77d1699d-c031-4659-b00a-735e661b53ec', + type: ['VerifiableCredential'], + issuer: 'did:key:z6MktiSzqF9kqwdU8VkdBKx56EYzXfpgnNPUAGznpicNiWfn', + issuanceDate: '2010-01-01T19:23:24Z', + credentialStatus: { + id: 'https://api.did.actor/revocation-lists/1.json#0', + type: 'RevocationList2020Status', + revocationListIndex: 0, + revocationListCredential: + 'https://api.did.actor/revocation-lists/1.json', + }, + credentialSubject: { + id: 'did:example:123', + }, + }; + const issuedCredential = await issueCredential({ + credential, + mnemonic, + }); + expect(issuedCredential.proof).toEqual( + expect.objectContaining({ + type: 'Ed25519Signature2018', + proofPurpose: 'assertionMethod', + }) + ); + + expect(issuedCredential['credentialSubject']).toEqual( + expect.objectContaining(credential.credentialSubject) + ); + expect(issuedCredential.proof).toHaveProperty('verificationMethod'); + expect(issuedCredential.proof).toHaveProperty('created'); + expect(issuedCredential.proof).toHaveProperty('jws'); + }); + + it('should throw an error for missing fields', async () => { + const mnemonic = + 'coast lesson mountain spy inform deposit two trophy album endless party crumble base grape artefact'; + const credential = { + '@context': [ + 'https://www.w3.org/2018/credentials/v1', + 'https://w3id.org/vc-revocation-list-2020/v1', + ], + id: 'urn:uuid:77d1699d-c031-4659-b00a-735e661b53ec', + type: ['test'], + credentialStatus: { + id: 'https://api.did.actor/revocation-lists/1.json#0', + type: 'RevocationList2020Status', + revocationListIndex: 0, + revocationListCredential: + 'https://api.did.actor/revocation-lists/1.json', + }, + credentialSubject: { + id: 'did:example:123', + }, + }; + + await expect(() => + issueCredential({ + credential: credential as never, + mnemonic, + }) + ).rejects.toThrow('credential is not valid JSON-LD'); + }); +}); diff --git a/libs/server-common/src/vc/transmute/issue.ts b/libs/server-common/src/vc/transmute/issue.ts new file mode 100644 index 00000000..d1cfa9fa --- /dev/null +++ b/libs/server-common/src/vc/transmute/issue.ts @@ -0,0 +1,119 @@ +import { + Ed25519Signature2018, + Ed25519VerificationKey2018, +} from '@transmute/ed25519-signature-2018'; + +import { JsonWebKey, JsonWebSignature } from '@transmute/json-web-signature'; +import { EcdsaSecp256k1VerificationKey2019 } from '@transmute/secp256k1-key-pair'; + +import { DidKey, VerifiableCredential } from '@dvp/api-interfaces'; +import { verifiable } from '@transmute/vc.js'; +import { BadRequestError } from '../../error'; +import { documentLoader } from './documentLoader'; +import { getKeysForMnemonic } from './keys'; + +export interface IssueCredentialOptions { + /** + * A JSON-LD Verifiable Credential without a proof. + */ + credential: VerifiableCredential; + + /** + * Mnemonic code for generating deterministic keys + */ + mnemonic: string; + proofType?: string; + hdpath?: string; + keyType?: string; + + format?: 'vc' | 'vc-jwt'; +} + +export const getSuite = async ( + key: DidKey, + proofType = 'Ed25519Signature2018' +) => { + if (proofType === 'Ed25519Signature2018') { + return new Ed25519Signature2018({ + key: await Ed25519VerificationKey2018.from(key), + }); + } + + if (proofType === 'JsonWebSignature2020') { + return new JsonWebSignature({ + key: await JsonWebKey.from(key as EcdsaSecp256k1VerificationKey2019), + }); + } + throw new BadRequestError(new Error(`Unsupported proof type: ${proofType}`)); +}; + +export const getCredentialSuite = async ({ + credential, + mnemonic, + hdpath, + proofType = 'Ed25519Signature2018', + keyType = 'ed25519', +}: IssueCredentialOptions) => { + const keys = await getKeysForMnemonic(keyType, mnemonic, hdpath); + if (typeof credential.issuer !== 'string' && credential?.issuer?.id) { + credential.issuer.id = keys[0].controller; + } else { + credential.issuer = keys[0].controller; + } + + // we are exploiting the known structure of did:key here... + const suite = await getSuite(keys[0], proofType); + return suite; +}; + +export const issueCredential = async ({ + credential, + mnemonic, + hdpath, + proofType, + keyType = 'ed25519', + format = 'vc', +}: IssueCredentialOptions) => { + const suite = await getCredentialSuite({ + credential, + mnemonic, + keyType, + hdpath, + proofType, + }); + + if ( + proofType === 'JsonWebSignature2020' && + format === 'vc' && + !credential['@context'].includes( + 'https://w3id.org/security/suites/jws-2020/v1' + ) + ) { + credential['@context'].push('https://w3id.org/security/suites/jws-2020/v1'); + } + + if (!credential.issuer) { + credential.issuer = suite.key.controller; + } + + if (format === 'vc-jwt') { + credential['@context'].push('https://www.w3.org/2018/credentials/v1'); + } + try { + const { items } = await verifiable.credential.create({ + credential, + suite, + documentLoader, + format: [format], + }); + return items[0]; + } catch (err) { + if ( + err instanceof Error && + err?.message?.includes('credential is not valid JSON-LD') + ) { + throw new BadRequestError(err); + } + throw err; + } +}; diff --git a/libs/server-common/src/vc/transmute/keys.spec.ts b/libs/server-common/src/vc/transmute/keys.spec.ts new file mode 100644 index 00000000..6d0ef12c --- /dev/null +++ b/libs/server-common/src/vc/transmute/keys.spec.ts @@ -0,0 +1,10 @@ +import { getKeysForMnemonic } from './keys'; + +describe('getKeysForMnemonic', () => { + it('should generate keys for the mnemonic code', async () => { + const mnemonic = + 'coast lesson mountain spy inform deposit two trophy album endless party crumble base grape artefact'; + const keys = await getKeysForMnemonic('ed25519', mnemonic); + expect(keys).toBeDefined(); + }); +}); diff --git a/libs/server-common/src/vc/transmute/keys.ts b/libs/server-common/src/vc/transmute/keys.ts new file mode 100644 index 00000000..e837a292 --- /dev/null +++ b/libs/server-common/src/vc/transmute/keys.ts @@ -0,0 +1,18 @@ +import { DidKey } from '@dvp/api-interfaces'; +import * as bip39 from 'bip39'; +import hdkey from 'hdkey'; +import { generators } from './generators'; + +export const DID_KEY_BIP44_COIN_TYPE = '0'; + +export const getKeysForMnemonic = async ( + keyType: string, + mnemonic: string, + hdpath = `m/44'/${DID_KEY_BIP44_COIN_TYPE}'/0'/0/0` +): Promise => { + const seed = await bip39.mnemonicToSeed(mnemonic); + const root = hdkey.fromMasterSeed(seed); + const addrNode = root.derive(hdpath); + const { keys } = await generators.didKey(keyType, addrNode.privateKey); + return keys; +}; diff --git a/libs/server-common/src/vc/transmute/resolver.spec.ts b/libs/server-common/src/vc/transmute/resolver.spec.ts new file mode 100644 index 00000000..44d49512 --- /dev/null +++ b/libs/server-common/src/vc/transmute/resolver.spec.ts @@ -0,0 +1,53 @@ +import * as didKey from '@transmute/did-key.js'; +import * as didWeb from '@transmute/did-web'; +import axios from 'axios'; +import { resolvers } from './resolvers'; + +jest.mock('axios'); +jest.mock('@transmute/did-key.js'); +jest.mock('@transmute/did-web'); + +describe('resolvers', () => { + describe('http', () => { + it('should make http get request for a given url', async () => { + const url = 'http://fetch.url'; + const getRequest = axios.get as jest.Mock; + getRequest.mockImplementation(() => + Promise.resolve({ + data: { + id: 'test', + }, + }) + ); + const res = await resolvers.http(url); + expect(res['id']).toBe('test'); + }); + }); + + describe('resolve', () => { + it('should call transmute did key resolve for did:key', async () => { + const resolve = didKey.resolve as jest.Mock; + const didDocument = { + id: 'test', + }; + resolve.mockImplementation(() => + Promise.resolve({ + didDocument, + }) + ); + const res = await resolvers.resolve( + 'did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd' + ); + expect(res).toEqual(didDocument); + }); + it('should call transmute did web resolve for did:web', async () => { + const resolve = didWeb.resolve as jest.Mock; + const didDocument = { + id: 'test', + }; + resolve.mockImplementation(() => Promise.resolve(didDocument)); + const res = await resolvers.resolve('did:web:http://example.com'); + expect(res).toEqual(didDocument); + }); + }); +}); diff --git a/libs/server-common/src/vc/transmute/resolvers.ts b/libs/server-common/src/vc/transmute/resolvers.ts new file mode 100644 index 00000000..d7bff692 --- /dev/null +++ b/libs/server-common/src/vc/transmute/resolvers.ts @@ -0,0 +1,28 @@ +import * as didKey from '@transmute/did-key.js'; +import * as didWeb from '@transmute/did-web'; +import type { DidDocument } from '@transmute/jsonld-document-loader'; + +import axios from 'axios'; + +export const resolvers = { + http: async (url: string) => { + const resp = await axios.get(url, { + headers: { + accept: 'application/json', + }, + }); + return resp.data; + }, + resolve: async (did: string) => { + if (did.startsWith('did:key')) { + const { didDocument } = await didKey.resolve(did.split('#')[0], { + accept: 'application/did+json', + }); + return didDocument; + } + if (did.startsWith('did:web')) { + return didWeb.resolve(did); + } + throw new Error('Unsupported did method'); + }, +}; diff --git a/libs/server-common/src/vc/transmute/verify.spec.ts b/libs/server-common/src/vc/transmute/verify.spec.ts new file mode 100644 index 00000000..27207b7e --- /dev/null +++ b/libs/server-common/src/vc/transmute/verify.spec.ts @@ -0,0 +1,17 @@ +import invalidVC from '../../fixtures/genericvc/degree_invalid.json'; +import signedValidVC from '../../fixtures/genericvc/degree_signed.json'; +import { verifyCredential } from './verify'; + +describe('verify', () => { + jest.setTimeout(20000); + it('should verify a valid credential', async () => { + const verificationResult = await verifyCredential(signedValidVC as never); + expect(verificationResult).toHaveProperty('errors'); + expect(verificationResult.errors).toHaveLength(0); + }); + it('should fail DOCUMENT_INTEGRITY when signature is invalid', async () => { + const verificationResult = await verifyCredential(invalidVC as never); + expect(verificationResult).toHaveProperty('errors'); + expect(verificationResult.errors).toContain('proof'); + }); +}); diff --git a/libs/server-common/src/vc/transmute/verify.ts b/libs/server-common/src/vc/transmute/verify.ts new file mode 100644 index 00000000..0c7dfdeb --- /dev/null +++ b/libs/server-common/src/vc/transmute/verify.ts @@ -0,0 +1,91 @@ +import { VerifiableCredential, VerificationResult } from '@dvp/api-interfaces'; +import { Ed25519Signature2018 } from '@transmute/ed25519-signature-2018'; +import { ApplicationError } from '../../error'; + +import { JsonWebSignature } from '@transmute/json-web-signature'; + +import { checkStatus } from '@transmute/vc-status-rl-2020'; +import { verifiable } from '@transmute/vc.js'; + +import { documentLoader } from './documentLoader'; + +export const verifyCredential = async ( + verifiableCredential: VerifiableCredential +): Promise => { + const checks = verifiableCredential.credentialStatus + ? ['proof', 'status', 'identity'] + : ['proof', 'identity']; + + try { + const result = await verifiable.credential.verify({ + credential: verifiableCredential, + suite: [new Ed25519Signature2018(), new JsonWebSignature()], + checkStatus, + documentLoader, + }); + + if (result.verified) { + return { + checks, + errors: [], + warnings: [], + }; + } + const proofCheckFailed = !!( + result['error'] && + result['error'].find( + (e: any) => + e?.proofResult?.verified === false || e?.proofResult === false + ) + ); + const statusCheckFailed = !!( + result['error'] && + result['error'].find( + (e: any) => + e?.statusResult?.verified === false || e?.statusResult === false + ) + ); + + // Credential not active + const inactive = !!( + result['verifications'] && + result['verifications'].find( + (verification: any) => + verification.status === 'bad' && verification.title === 'Activation' + ) + ); + + // Credential Expired + const expired = + result['verifications'] && + result['verifications'].find( + (verification: any) => + verification.status === 'bad' && verification.title === 'Expired' + ); + + let errors = ['identity']; + if (statusCheckFailed) { + // Revocation + errors = ['status']; + } else if (proofCheckFailed) { + // Signature + errors = ['proof']; + } + + if (inactive) { + errors.push('inactive'); + } + + if (expired) { + errors.push('expired'); + } + + return { + checks, + errors, + warnings: [], + }; + } catch (err) { + throw new ApplicationError((err as Error).message); + } +}; diff --git a/libs/server-common/tsconfig.json b/libs/server-common/tsconfig.json new file mode 100644 index 00000000..69cbfd58 --- /dev/null +++ b/libs/server-common/tsconfig.json @@ -0,0 +1,24 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "resolveJsonModule": true, + "esModuleInterop": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/libs/server-common/tsconfig.lib.json b/libs/server-common/tsconfig.lib.json new file mode 100644 index 00000000..5a36e5e2 --- /dev/null +++ b/libs/server-common/tsconfig.lib.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "declaration": true, + "types": ["node"], + "composite": true, + "esModuleInterop": true + }, + "include": ["**/*.ts"], + "exclude": ["jest.config.ts", "**/*.spec.ts", "**/*.test.ts"] +} diff --git a/libs/server-common/tsconfig.spec.json b/libs/server-common/tsconfig.spec.json new file mode 100644 index 00000000..546f1287 --- /dev/null +++ b/libs/server-common/tsconfig.spec.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": ["jest.config.ts", "**/*.test.ts", "**/*.spec.ts", "**/*.d.ts"] +} diff --git a/libs/vc-ui/.babelrc b/libs/vc-ui/.babelrc new file mode 100644 index 00000000..e513ea41 --- /dev/null +++ b/libs/vc-ui/.babelrc @@ -0,0 +1,12 @@ +{ + "presets": [ + [ + "@nrwl/react/babel", + { + "runtime": "automatic", + "useBuiltIns": "usage" + } + ] + ], + "plugins": [["styled-components", { "pure": true, "ssr": true }]] +} diff --git a/libs/vc-ui/.storybook/main.js b/libs/vc-ui/.storybook/main.js new file mode 100644 index 00000000..4ca6b950 --- /dev/null +++ b/libs/vc-ui/.storybook/main.js @@ -0,0 +1,37 @@ +const rootMain = require('../../../.storybook/main'); + +module.exports = { + ...rootMain, + + core: { ...rootMain.core, builder: 'webpack5' }, + + staticDirs: ['../../../public'], + + stories: [ + ...rootMain.stories, + '../src/lib/**/*.stories.mdx', + '../src/lib/**/*.stories.@(js|jsx|ts|tsx)', + ], + addons: [...rootMain.addons, '@nrwl/react/plugins/storybook'], + webpackFinal: async (config, { configType }) => { + // apply any global webpack configs that might have been specified in .storybook/main.js + if (rootMain.webpackFinal) { + config = await rootMain.webpackFinal(config, { configType }); + } + + // add your own webpack tweaks if needed + + return { + ...config, + resolve: { + ...config.resolve, + fallback: { + ...config.fallback, + crypto: require.resolve('crypto-browserify/'), + stream: require.resolve('stream-browserify'), + buffer: require.resolve('buffer'), + }, + }, + }; + }, +}; diff --git a/libs/vc-ui/.storybook/preview.js b/libs/vc-ui/.storybook/preview.js new file mode 100644 index 00000000..e69de29b diff --git a/libs/vc-ui/.storybook/tsconfig.json b/libs/vc-ui/.storybook/tsconfig.json new file mode 100644 index 00000000..601ae8ff --- /dev/null +++ b/libs/vc-ui/.storybook/tsconfig.json @@ -0,0 +1,27 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "emitDecoratorMetadata": true, + "outDir": "" + }, + "files": [ + "../../../node_modules/@nrwl/react/typings/styled-jsx.d.ts", + "../../../node_modules/@nrwl/react/typings/cssmodule.d.ts", + "../../../node_modules/@nrwl/react/typings/image.d.ts" + ], + + "exclude": [ + "../**/*.spec.ts", + "../**/*.spec.js", + "../**/*.spec.tsx", + "../**/*.spec.jsx" + ], + "include": [ + "../src/**/*.stories.ts", + "../src/**/*.stories.js", + "../src/**/*.stories.jsx", + "../src/**/*.stories.tsx", + "../src/**/*.stories.mdx", + "*.js" + ] +} diff --git a/libs/vc-ui/README.md b/libs/vc-ui/README.md new file mode 100644 index 00000000..c75341d3 --- /dev/null +++ b/libs/vc-ui/README.md @@ -0,0 +1,7 @@ +# vc-ui + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test vc-ui` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/vc-ui/jest.config.ts b/libs/vc-ui/jest.config.ts new file mode 100644 index 00000000..fcfa4ac4 --- /dev/null +++ b/libs/vc-ui/jest.config.ts @@ -0,0 +1,15 @@ +/* eslint-disable */ +export default { + displayName: 'vc-ui', + preset: '../../jest.preset.js', + setupFiles: ['./jest.setup.ts'], + transform: { + '^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nrwl/react/plugins/jest', + '^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nrwl/react/babel'] }], + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], + coverageDirectory: '../../coverage/libs/vc-ui', + moduleNameMapper: { + '^react-pdf': 'react-pdf/dist/umd/entry.jest', + }, +}; diff --git a/libs/vc-ui/jest.setup.ts b/libs/vc-ui/jest.setup.ts new file mode 100644 index 00000000..6fc79565 --- /dev/null +++ b/libs/vc-ui/jest.setup.ts @@ -0,0 +1,7 @@ +import 'jest-canvas-mock'; + +const mockClipboard = { + writeText: jest.fn(), +}; + +(global.navigator.clipboard as any) = mockClipboard; diff --git a/libs/vc-ui/project.json b/libs/vc-ui/project.json new file mode 100644 index 00000000..ff34806a --- /dev/null +++ b/libs/vc-ui/project.json @@ -0,0 +1,55 @@ +{ + "name": "vc-ui", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/vc-ui/src", + "projectType": "library", + "tags": [], + "targets": { + "lint": { + "executor": "@nrwl/linter:eslint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["libs/vc-ui/**/*.{ts,tsx,js,jsx}"] + } + }, + "test": { + "executor": "@nrwl/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/vc-ui/jest.config.ts", + "passWithNoTests": true + } + }, + "storybook": { + "executor": "@nrwl/storybook:storybook", + "options": { + "uiFramework": "@storybook/react", + "port": 4400, + "config": { + "configFolder": "libs/vc-ui/.storybook" + } + }, + "configurations": { + "ci": { + "quiet": true + } + } + }, + "build-storybook": { + "executor": "@nrwl/storybook:build", + "outputs": ["{options.outputPath}"], + "options": { + "uiFramework": "@storybook/react", + "outputPath": "dist/storybook/vc-ui", + "config": { + "configFolder": "libs/vc-ui/.storybook" + } + }, + "configurations": { + "ci": { + "quiet": true + } + } + } + } +} diff --git a/libs/vc-ui/src/assets/OpenBox.svg b/libs/vc-ui/src/assets/OpenBox.svg new file mode 100644 index 00000000..6b345485 --- /dev/null +++ b/libs/vc-ui/src/assets/OpenBox.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/libs/vc-ui/src/assets/dropbox.svg b/libs/vc-ui/src/assets/dropbox.svg new file mode 100644 index 00000000..c5421620 --- /dev/null +++ b/libs/vc-ui/src/assets/dropbox.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libs/vc-ui/src/index.ts b/libs/vc-ui/src/index.ts new file mode 100644 index 00000000..e460a630 --- /dev/null +++ b/libs/vc-ui/src/index.ts @@ -0,0 +1,2 @@ +export * from './lib'; +export * from './utils'; diff --git a/libs/vc-ui/src/lib/Button/Button.stories.tsx b/libs/vc-ui/src/lib/Button/Button.stories.tsx new file mode 100644 index 00000000..05fae50c --- /dev/null +++ b/libs/vc-ui/src/lib/Button/Button.stories.tsx @@ -0,0 +1,16 @@ +import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import { Button } from './Button'; + +const Story: ComponentMeta = { + component: Button, + title: 'Button', +}; +export default Story; + +const Template: ComponentStory = (args) => + + +`; diff --git a/libs/vc-ui/src/lib/Button/index.ts b/libs/vc-ui/src/lib/Button/index.ts new file mode 100644 index 00000000..8b166a86 --- /dev/null +++ b/libs/vc-ui/src/lib/Button/index.ts @@ -0,0 +1 @@ +export * from './Button'; diff --git a/libs/vc-ui/src/lib/Card/Card.test.tsx b/libs/vc-ui/src/lib/Card/Card.test.tsx new file mode 100644 index 00000000..3e95974c --- /dev/null +++ b/libs/vc-ui/src/lib/Card/Card.test.tsx @@ -0,0 +1,54 @@ +import { Switch } from '@mui/material'; +import { fireEvent, render } from '@testing-library/react'; +import { Card } from './Card'; + +const mockHandleAction = jest.fn(); + +describe('Card', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('should render correctly', () => { + const { baseElement } = render( + + ); + + expect(baseElement).toMatchSnapshot(); + }); + + it('should call correct function when item is clicked', () => { + const { getByTestId } = render( + + ); + + fireEvent.click(getByTestId('button:Remove')); + + expect(mockHandleAction).toHaveBeenCalled(); + }); + + it('should show header action if present', () => { + const { getByTestId } = render( + + } + /> + ); + + fireEvent.click(getByTestId('switch')); + + expect(mockHandleAction).toHaveBeenCalled(); + }); +}); diff --git a/libs/vc-ui/src/lib/Card/Card.tsx b/libs/vc-ui/src/lib/Card/Card.tsx new file mode 100644 index 00000000..072657da --- /dev/null +++ b/libs/vc-ui/src/lib/Card/Card.tsx @@ -0,0 +1,60 @@ +import { + Card as MUICard, + CardActions, + CardContent, + CardProps, + Stack, +} from '@mui/material'; +import { Button, Text } from '..'; + +interface ICard extends CardProps { + name: string; + text: string; + actionLabel?: string; + handleAction?: () => void; + headerAction?: React.ReactNode; +} + +export const Card = ({ + name, + text, + handleAction, + actionLabel, + headerAction, + ...rest +}: ICard) => { + return ( + + + + + {name} + + {headerAction} + + + {text} + + {actionLabel && handleAction && ( + + + + + + + +
+
+
+
+
+
+
+
+
+ ID +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+
+
+
+
+ First Name +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+
+
+
+
+ Last Name +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+ 006 +
+
+
+
+ Postman +
+
+
+
+ Pat +
+
+
+
+
+
+ 007 +
+
+
+
+ Wreck-It +
+
+
+
+ Ralph +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ Rows per page: +

+
+ + + +
+

+ 1–2 of 2 +

+
+ + +
+
+
+
+
+
+
+
+ +`; diff --git a/libs/vc-ui/src/lib/DataTable/__snapshots__/MoreInfo.test.tsx.snap b/libs/vc-ui/src/lib/DataTable/__snapshots__/MoreInfo.test.tsx.snap new file mode 100644 index 00000000..76f12300 --- /dev/null +++ b/libs/vc-ui/src/lib/DataTable/__snapshots__/MoreInfo.test.tsx.snap @@ -0,0 +1,34 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MoreInfo should render correctly 1`] = ` + +
+
+ +
+
+ +`; diff --git a/libs/vc-ui/src/lib/DataTable/index.ts b/libs/vc-ui/src/lib/DataTable/index.ts new file mode 100644 index 00000000..abe824f7 --- /dev/null +++ b/libs/vc-ui/src/lib/DataTable/index.ts @@ -0,0 +1,2 @@ +export * from './DataTable'; +export * from './MoreInfo'; diff --git a/libs/vc-ui/src/lib/FileUpload/FileUpload.spec.tsx b/libs/vc-ui/src/lib/FileUpload/FileUpload.spec.tsx new file mode 100644 index 00000000..9baf2f57 --- /dev/null +++ b/libs/vc-ui/src/lib/FileUpload/FileUpload.spec.tsx @@ -0,0 +1,39 @@ +import { FileUpload } from './FileUpload'; +import { render, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { FileData, fileToBase64 } from '../../utils'; + +const mockCallback = jest.fn(); +const mockFileUploadProps = { + buttonText: 'testButton', + multiple: false, + acceptedFiles: '.pdf', + required: true, + onChange: mockCallback, +}; +const mockFile = new File(['(⌐□_□)'], 'mockFile.pdf', { + type: 'application/pdf', +}); + +describe('FileUpload', () => { + beforeEach(() => jest.clearAllMocks()); + it('should render the component', () => { + const { getByTestId } = render(); + + expect(getByTestId('file-upload-button')).toBeTruthy(); + }); + + it('should upload a file and call the callback function', async () => { + const { getByTestId } = render(); + + const fileData = await fileToBase64(mockFile); + const fileUpload = getByTestId('file-upload-input') as HTMLInputElement; + + await userEvent.upload(fileUpload, mockFile); + + await waitFor(() => { + expect(mockCallback).toBeCalledWith((fileData as FileData).dataURL); + expect(fileUpload?.files).toHaveLength(1); + }); + }); +}); diff --git a/libs/vc-ui/src/lib/FileUpload/FileUpload.stories.tsx b/libs/vc-ui/src/lib/FileUpload/FileUpload.stories.tsx new file mode 100644 index 00000000..677a3371 --- /dev/null +++ b/libs/vc-ui/src/lib/FileUpload/FileUpload.stories.tsx @@ -0,0 +1,23 @@ +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import { FileUpload } from './FileUpload'; + +const Story: ComponentMeta = { + component: FileUpload, + title: 'FileUpload', +}; +export default Story; + +const Template: ComponentStory = (args) => ( + +); + +export const Primary = Template.bind({}); + +Primary.args = { + buttonText: 'Upload Document', + multiple: false, + acceptedFiles: '.pdf', + onChange: (value: string | string[]) => { + return; + }, +}; diff --git a/libs/vc-ui/src/lib/FileUpload/FileUpload.tsx b/libs/vc-ui/src/lib/FileUpload/FileUpload.tsx new file mode 100644 index 00000000..9deea05d --- /dev/null +++ b/libs/vc-ui/src/lib/FileUpload/FileUpload.tsx @@ -0,0 +1,73 @@ +import { Box, InputLabel } from '@mui/material'; +import { ChangeEvent, FunctionComponent, useRef } from 'react'; +import { FileData, fileToBase64 } from '../../utils/fileToBase64'; +import { Button } from '../Button'; + +interface FileUploadProps { + buttonText: string; + multiple: boolean; + acceptedFiles?: string; + required?: boolean; + onChange(value: string | string[]): void; +} + +export const FileUpload: FunctionComponent = ({ + buttonText, + multiple, + acceptedFiles, + required, + onChange, +}) => { + const inputRef = useRef(null); + + function processFiles(files: FileList): Promise { + return Promise.all([].map.call(files, fileToBase64)) as Promise; + } + + const _onChange = async (event: ChangeEvent) => { + if (event.target.files) { + const filesInfo = await processFiles(event.target.files); + if (multiple) { + return onChange(filesInfo.map((fileInfo) => fileInfo.dataURL)); + } else { + return onChange(filesInfo[0].dataURL); + } + } + }; + + const handleClick = () => { + if (inputRef.current) { + inputRef?.current?.click(); + } + }; + + return ( + + + + + +
+ + +

+

+ + +

+
+ +
+ + + +
+

+

+

+
+
+
+ +
+ + + +
+

+

+

+
+ + + +`; + +exports[`MaterialLayoutRenderer should match row snapshot 1`] = ` + +
+
+
+
+ +
+ + + +
+

+

+

+
+
+
+ +
+ + + +
+

+

+

+
+
+
+ +
+ + + +
+

+

+

+
+
+
+ +`; diff --git a/libs/vc-ui/src/lib/GenericJsonForm/Renders/Layouts/index.ts b/libs/vc-ui/src/lib/GenericJsonForm/Renders/Layouts/index.ts new file mode 100644 index 00000000..847c5701 --- /dev/null +++ b/libs/vc-ui/src/lib/GenericJsonForm/Renders/Layouts/index.ts @@ -0,0 +1,6 @@ +export { + default as MaterialHorizontalLayoutRenderer, + materialHorizontalLayoutTester, + } from './MaterialHorizontalLayout'; +export * from "./GroupLayout"; + diff --git a/libs/vc-ui/src/lib/GenericJsonForm/Renders/index.ts b/libs/vc-ui/src/lib/GenericJsonForm/Renders/index.ts new file mode 100644 index 00000000..0ed27f02 --- /dev/null +++ b/libs/vc-ui/src/lib/GenericJsonForm/Renders/index.ts @@ -0,0 +1 @@ +export * from './JsonFormsRenders'; diff --git a/libs/vc-ui/src/lib/GenericJsonForm/index.ts b/libs/vc-ui/src/lib/GenericJsonForm/index.ts new file mode 100644 index 00000000..46f2da13 --- /dev/null +++ b/libs/vc-ui/src/lib/GenericJsonForm/index.ts @@ -0,0 +1 @@ +export * from './GenericJsonForm'; diff --git a/libs/vc-ui/src/lib/GenericJsonForm/testUtils/JsonFormsTestUtils.tsx b/libs/vc-ui/src/lib/GenericJsonForm/testUtils/JsonFormsTestUtils.tsx new file mode 100644 index 00000000..2001b822 --- /dev/null +++ b/libs/vc-ui/src/lib/GenericJsonForm/testUtils/JsonFormsTestUtils.tsx @@ -0,0 +1,141 @@ +import { createAjv, ControlElement } from '@jsonforms/core'; +import { JsonFormsStateProvider } from '@jsonforms/react'; +import React from 'react'; +import { Renderers } from '../Renders'; +import { render } from '@testing-library/react'; +import { ThemeProvider } from '@mui/material'; +import { jsonFormTheme } from '../../../theme'; + +window.matchMedia = jest.fn().mockImplementation((query) => { + return { + matches: true, + media: query, + onchange: null, + addListener: jest.fn(), + removeListener: jest.fn(), + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn(), + }; +}); + +export const testUISchema: ControlElement = { + type: 'Control', + scope: '#/properties/sample', +}; + +export const testRootSchema = { + type: 'object', + properties: { + sample: { + type: 'string', + title: 'sample', + }, + }, +}; +export const testRootSchemaRequired = { + type: 'object', + properties: { + sample: { + type: 'string', + title: 'sample', + }, + }, + required: ['sample'], +}; +export const testSchemaRestrictedLength = { + type: 'object', + properties: { + sample: { + type: 'string', + title: 'sample', + maxLength: 5, + }, + }, +}; + +export const samplePropsInputFields = { + uischema: testUISchema, + schema: { + type: 'Control', + format: 'date-time', + }, + path: 'sample', + data: '', + enabled: true, + id: '#/properties/sample', + errors: '', + label: 'sample', + visible: true, + required: false, + isValid: true, + config: { + restrict: false, + trim: false, + showUnfocusedDescription: false, + hideRequiredAsterisk: false, + }, + rootSchema: testRootSchema, +}; + +export const samplePropsInputFieldsRequired = { + ...samplePropsInputFields, + required: true, + errors: 'required', +}; + +export const samplePropsInputFieldsRestrictedLength = { + uischema: testUISchema, + schema: { + type: 'string', + title: 'sample', + maxLength: 5, + }, + path: 'sample', + data: '', + enabled: true, + id: '#/properties/sample', + errors: '', + label: 'sample', + visible: true, + required: false, + isValid: true, + config: { + restrict: true, + trim: true, + showUnfocusedDescription: false, + hideRequiredAsterisk: false, + }, + rootSchema: testSchemaRestrictedLength, +}; + +export const initCore = (required = false, data?: any) => { + if (required) + return { + schema: testRootSchemaRequired, + uiSchema: testUISchema, + data, + ajv: createAjv(), + }; + return { + schema: testRootSchema, + uiSchema: testUISchema, + data, + ajv: createAjv(), + }; +}; + +export const jsonFormsTestHarness = ( + data: any, + child: React.ReactNode, + required = false +) => { + const core = initCore(required, data); + return render( + + + {child} + + + ); +}; diff --git a/libs/vc-ui/src/lib/GenericJsonForm/testUtils/index.ts b/libs/vc-ui/src/lib/GenericJsonForm/testUtils/index.ts new file mode 100644 index 00000000..8b8bdbb9 --- /dev/null +++ b/libs/vc-ui/src/lib/GenericJsonForm/testUtils/index.ts @@ -0,0 +1 @@ +export * from './JsonFormsTestUtils'; diff --git a/libs/vc-ui/src/lib/GenericJsonForm/util.test.ts b/libs/vc-ui/src/lib/GenericJsonForm/util.test.ts new file mode 100644 index 00000000..21e496c6 --- /dev/null +++ b/libs/vc-ui/src/lib/GenericJsonForm/util.test.ts @@ -0,0 +1,41 @@ +import { Translator, UISchemaElement } from '@jsonforms/core'; +import { ErrorObject } from 'ajv'; +import { JsonFormsErrorMapper } from './util'; + +const error: ErrorObject = { + keyword: 'required', + instancePath: '#/path', + schemaPath: '#/path', + params: {}, +}; +const nonRequiredError: ErrorObject = { + keyword: 'minLength', + instancePath: '#/path', + schemaPath: '#/path', + message: 'must be of length', + params: {}, +}; + +const translatorObj: Translator = () => ''; + +const uischema: UISchemaElement & { label?: string } = { + type: '', + label: 'boppo', +}; + +describe('genericJsonForm Utils', () => { + it('should show adjusted error message', () => { + const response = JsonFormsErrorMapper(error, translatorObj, uischema); + + expect(response).toEqual('boppo is a required field'); + }); + it("should return default message if error message isn't overridden", () => { + const response = JsonFormsErrorMapper( + nonRequiredError, + translatorObj, + uischema + ); + + expect(response).toEqual('must be of length'); + }); +}); diff --git a/libs/vc-ui/src/lib/GenericJsonForm/util.ts b/libs/vc-ui/src/lib/GenericJsonForm/util.ts new file mode 100644 index 00000000..a3ae8842 --- /dev/null +++ b/libs/vc-ui/src/lib/GenericJsonForm/util.ts @@ -0,0 +1,17 @@ +import { Translator, UISchemaElement } from "@jsonforms/core"; +import { ErrorObject } from "ajv"; + + export const JsonFormsErrorMapper = ( + error: ErrorObject, + _translate: Translator, + uischema?: UISchemaElement & { label?: string } + ): string => { + const fieldLabel: string = uischema?.label + ? uischema.label + : (error.params['missingProperty'] as string); + switch (error.keyword) { + case 'required': + return `${fieldLabel} is a required field`; + } + return error.message ? error.message : ''; + }; diff --git a/libs/vc-ui/src/lib/PdfRenderer/PdfRenderer.spec.tsx b/libs/vc-ui/src/lib/PdfRenderer/PdfRenderer.spec.tsx new file mode 100644 index 00000000..e771e836 --- /dev/null +++ b/libs/vc-ui/src/lib/PdfRenderer/PdfRenderer.spec.tsx @@ -0,0 +1,130 @@ +/* eslint-disable @typescript-eslint/await-thenable */ +/* eslint-disable @typescript-eslint/require-await */ +import { + render, + fireEvent, + act, + waitFor, + screen, +} from '@testing-library/react'; +import { PdfRenderer } from './PdfRenderer'; +import { AANZFTA_COO_PARTIAL } from '../fixtures'; + +const urlMock = 'www.example.com'; +const originalDocument: string = + AANZFTA_COO_PARTIAL?.credentialSubject?.['originalDocument']; + +describe('PdfRenderer', () => { + beforeEach(() => jest.clearAllMocks()); + + it('should render successfully', async () => { + await act(async () => { + render(); + }); + + expect(screen.getByTestId('pdf-container')).toBeTruthy(); + }); + + it('should show all pages of the document', async () => { + await act(async () => { + render(); + }); + await waitFor(() => { + expect(screen.getByTestId('page_1')).toBeTruthy(); + expect(screen.getByTestId('page_2')).toBeTruthy(); + }); + }); + + it('should show a divider between the pages of the document', async () => { + await act(async () => { + render(); + }); + await waitFor(() => { + expect(screen.queryByTestId('page_1_divider')).toBeTruthy(); + }); + }); + + it('should not show a divider on the last page', async () => { + await act(async () => { + render(); + }); + await waitFor(() => { + expect(screen.queryByTestId('page_2_divider')).toBeFalsy(); + }); + }); + + it('should show a QrCode if a document loaded', async () => { + await act(async () => { + render(); + }); + await waitFor(() => { + expect(screen.getByTestId('qr-code-element')).toBeTruthy(); + }); + }); + + it('should have a resize element if QrCode is displayed', async () => { + await act(async () => { + render(); + }); + await waitFor(() => { + expect(screen.queryByTestId('qrcode-resize')).toBeTruthy(); + }); + }); + + // TODO: Come up with a solution to provide jsdom with the required properties. + it.skip('should resize the QrCode ', async () => { + await act(async () => { + render(); + }); + + await waitFor(() => { + expect(screen.getByTestId('qr-code-element')).toBeTruthy(); + expect(screen.getByTestId('qrcode-resize')).toBeTruthy(); + }); + + const qrCodeElement = screen.getByTestId('qr-code-element'); + const qrCodeElementOriginalWidth = qrCodeElement.clientWidth; + const resizeElement = screen.getByTestId('qrcode-resize'); + const changeInPosition = 100; + + act(() => { + fireEvent.mouseOver(resizeElement); + fireEvent.mouseDown(resizeElement); + fireEvent.mouseMove(resizeElement, { clientX: changeInPosition }); + fireEvent.mouseUp(resizeElement); + }); + + await waitFor(() => { + expect(screen.getByTestId('qr-code-element').clientWidth).toBe( + qrCodeElementOriginalWidth + changeInPosition + ); + }); + }); + + // TODO: Come up with a solution to provide jsdom with the required properties. + it.skip('should move the QrCode ', async () => { + await act(async () => { + render(); + }); + + await waitFor(() => { + expect(screen.getByTestId('qr-code-element')).toBeTruthy(); + }); + + const qrCodeElement = screen.getByTestId('qr-code-element'); + const qrCodeElementOriginalPosition = qrCodeElement.getBoundingClientRect(); + const changeInPosition = 100; + + act(() => { + fireEvent.mouseDown(qrCodeElement); + fireEvent.mouseMove(qrCodeElement, { clientX: changeInPosition }); + fireEvent.mouseUp(qrCodeElement); + }); + + await waitFor(() => { + expect(qrCodeElement.getBoundingClientRect().left).toBe( + qrCodeElementOriginalPosition.left + changeInPosition + ); + }); + }); +}); diff --git a/libs/vc-ui/src/lib/PdfRenderer/PdfRenderer.stories.tsx b/libs/vc-ui/src/lib/PdfRenderer/PdfRenderer.stories.tsx new file mode 100644 index 00000000..bbe9184d --- /dev/null +++ b/libs/vc-ui/src/lib/PdfRenderer/PdfRenderer.stories.tsx @@ -0,0 +1,19 @@ +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import { PdfRenderer } from './PdfRenderer'; +import { AANZFTA_COO_PARTIAL } from '../fixtures'; + +const Story: ComponentMeta = { + component: PdfRenderer, + title: 'PdfRenderer', +}; +export default Story; + +const Template: ComponentStory = (args) => ( + +); + +export const Primary = Template.bind({}); + +Primary.args = { + pdfDocument: AANZFTA_COO_PARTIAL.credentialSubject['originalDocument'], +}; diff --git a/libs/vc-ui/src/lib/PdfRenderer/PdfRenderer.tsx b/libs/vc-ui/src/lib/PdfRenderer/PdfRenderer.tsx new file mode 100644 index 00000000..d249e31d --- /dev/null +++ b/libs/vc-ui/src/lib/PdfRenderer/PdfRenderer.tsx @@ -0,0 +1,319 @@ +/* eslint-disable @typescript-eslint/restrict-plus-operands */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +import { Box, Divider, Paper } from '@mui/material'; +import { + FunctionComponent, + useCallback, + useEffect, + useRef, + useState, +} from 'react'; +import { throttle } from 'lodash'; +import { Document, Page } from 'react-pdf/dist/esm/entry.webpack'; + +import { QrCode } from '../QrCode'; + +interface PdfRendererProps { + pdfDocument: string; + qrUrl?: string; +} + +export interface QrCodeData { + x: number; + y: number; + size: number; +} + +export const PdfRenderer: FunctionComponent = ({ + pdfDocument, + qrUrl, +}) => { + const [documentLoaded, setDocumentLoaded] = useState(false); + const isDragging = useRef(false); + const dragHeadRef = useRef(null); + const containerRef = useRef(null); + + const [position, setPosition] = useState({ + x: 1, + y: 1, + size: 80, + }); + const [PDFContainerWidth, setPDFContainerWidth] = useState< + number | undefined + >(); + const [PDFContainerHeight, setPDFContainerHeight] = useState< + number | undefined + >(); + + const [numPages, setNumPages] = useState(0); + + function onDocumentLoadSuccess({ numPages }: { numPages: number }) { + setDocumentLoaded(true); + setNumPages(numPages); + } + + const defaultQrRatio = 0.11; + const defaultQrPaddingRatio = 0.05; + const defaultQrBorderRatio = 0.005; + + const calculateQrSize = (pdfWidth: number) => pdfWidth * defaultQrRatio; + const calculateQrPadding = (qrSize: number) => + qrSize * defaultQrPaddingRatio * 2; + const calculateQrBorder = (qrSize: number) => + qrSize * defaultQrBorderRatio * 2; + + const calculateTotalQrSize = (pdfWidth: number, minSize: number) => { + const qrSize = calculateQrSize(pdfWidth); + const totalSize = + qrSize + calculateQrPadding(qrSize) + calculateQrBorder(qrSize); + return totalSize >= minSize ? totalSize : minSize; + }; + + const _setPDFContainerWidth = () => { + const width = containerRef?.current?.offsetWidth; + + if (width) { + const totalQrSize = calculateTotalQrSize(width, 80); + setPosition((previous_position) => ({ + ...previous_position, + size: totalQrSize, + })); + } + setPDFContainerWidth(width); + }; + + const _setPDFContainerHeight = () => { + const height = containerRef?.current?.offsetHeight; + setPDFContainerHeight(height); + }; + + const throttledSetPDFContainerWidth = throttle(_setPDFContainerWidth, 500); + const throttledSetPDFContainerHeight = throttle(_setPDFContainerHeight, 500); + + const enforceBoundary = ( + boundaryMin: number, + boundaryMax: number, + elementPosition: number + ) => { + return Math.max(boundaryMin, Math.min(boundaryMax, elementPosition)); + }; + + const onMouseDown = useCallback((e: MouseEvent) => { + if (dragHeadRef.current && dragHeadRef.current.contains(e.target as Node)) { + isDragging.current = true; + } + + e.stopPropagation(); + }, []); + + const onMouseUp = useCallback(() => { + if (isDragging.current) { + isDragging.current = false; + } + }, [PDFContainerHeight, PDFContainerWidth]); + + const onMouseMove = useCallback( + (e: MouseEvent) => { + if (isDragging.current) { + if (containerRef?.current) { + const { clientWidth, clientHeight } = containerRef.current; + + const qrSize = position.size; + const qrPadding = calculateQrPadding(position.size); + const qrBorder = calculateQrBorder(position.size); + const totalQrSize = qrSize + qrPadding + qrBorder; + + const containerBoundaryX = clientWidth - totalQrSize; + const containerBoundaryY = clientHeight - totalQrSize; + + setPosition((prev_position) => { + const elementPositionX = prev_position.x + e.movementX; + const elementPositionY = prev_position.y + e.movementY; + + const boundedQrX = enforceBoundary( + 0, + containerBoundaryX, + elementPositionX + ); + const boundedQrY = enforceBoundary( + 0, + containerBoundaryY, + elementPositionY + ); + + return { ...prev_position, x: boundedQrX, y: boundedQrY }; + }); + } + } + e.stopPropagation(); + }, + [position] + ); + + const onQrCodeResize = ( + newQrWidth: number, + elementMovementX: number, + elementMovementY: number + ) => { + if (containerRef?.current) { + const { clientWidth, clientHeight } = containerRef.current; + + const qrPadding = calculateQrPadding(newQrWidth); + const qrBorder = calculateQrBorder(newQrWidth); + const totalQrSize = newQrWidth + qrPadding + qrBorder; + + const minQrSize = clientWidth * 0.11; + + const containerBoundaryX = clientWidth - totalQrSize; + const containerBoundaryY = clientHeight - totalQrSize; + + if (totalQrSize > minQrSize && totalQrSize < clientWidth) { + setPosition((prev_position: QrCodeData) => { + const elementPositionX = prev_position.x - elementMovementX; + const elementPositionY = prev_position.y - elementMovementY; + + const boundedQrX = enforceBoundary( + 0, + containerBoundaryX, + elementPositionX + ); + const boundedQrY = enforceBoundary( + 0, + containerBoundaryY, + elementPositionY + ); + + return { + x: boundedQrX, + y: boundedQrY, + size: newQrWidth, + }; + }); + } + } + }; + + useEffect(() => { + setPDFContainerWidth(containerRef?.current?.offsetWidth); + setPDFContainerHeight(containerRef?.current?.offsetHeight); + + document.addEventListener('mouseup', onMouseUp); + document.addEventListener('mousedown', onMouseDown); + document.addEventListener('mousemove', onMouseMove); + window.addEventListener('resize', throttledSetPDFContainerWidth); + window.addEventListener('resize', throttledSetPDFContainerHeight); + + return () => { + document.removeEventListener('mouseup', onMouseUp); + document.removeEventListener('mousedown', onMouseDown); + document.removeEventListener('mousemove', onMouseMove); + window.removeEventListener('resize', throttledSetPDFContainerWidth); + window.removeEventListener('resize', throttledSetPDFContainerHeight); + }; + }, [ + onMouseMove, + onMouseDown, + onMouseUp, + throttledSetPDFContainerWidth, + throttledSetPDFContainerHeight, + ]); + + return ( + { + e.preventDefault(); + e.stopPropagation(); + }} + data-testid="pdf-container" + ref={containerRef} + sx={{ + position: 'relative', + width: '100%', + WebkitTouchCallout: 'none', + WebkitUserSelect: 'none', + KhtmlUserSelect: 'none', + MozUserSelect: 'none', + MsUserSelect: 'none', + UserSelect: 'none', + }} + > + + + {Array.from(new Array(numPages), (el, index) => ( + + { + if ( + containerRef?.current?.offsetWidth && + containerRef?.current?.offsetHeight + ) { + const totalQrSize = calculateTotalQrSize( + containerRef?.current?.offsetWidth, + 80 + ); + setPosition((previous_position) => ({ + ...previous_position, + size: totalQrSize, + })); + } + }} + /> + {numPages !== index + 1 && ( + + )} + + ))} + + + {documentLoaded && qrUrl && ( + + + + )} + + ); +}; diff --git a/libs/vc-ui/src/lib/PdfRenderer/index.ts b/libs/vc-ui/src/lib/PdfRenderer/index.ts new file mode 100644 index 00000000..d4882c70 --- /dev/null +++ b/libs/vc-ui/src/lib/PdfRenderer/index.ts @@ -0,0 +1 @@ +export * from './PdfRenderer'; diff --git a/libs/vc-ui/src/lib/PdfViewer/PdfViewer.spec.tsx b/libs/vc-ui/src/lib/PdfViewer/PdfViewer.spec.tsx new file mode 100644 index 00000000..bc2ea26a --- /dev/null +++ b/libs/vc-ui/src/lib/PdfViewer/PdfViewer.spec.tsx @@ -0,0 +1,29 @@ +/* eslint-disable @typescript-eslint/require-await */ +import { render, act, waitFor, screen } from '@testing-library/react'; +import { PdfViewer } from './PdfViewer'; +import { AANZFTA_COO_PARTIAL } from '../fixtures'; + +describe('PdfViewer', () => { + it('should display VcUtility component and PdfRenderer', async () => { + await act(async () => { + render(); + }); + + await waitFor(() => { + expect(screen.getByTestId('vc-utility')).toBeTruthy(); + expect(screen.getByTestId('pdf-container')).toBeTruthy(); + }); + }); + + it('should display a QrCode in the pdf container', async () => { + await act(async () => { + render(); + }); + + await waitFor(() => { + expect(screen.getByTestId('vc-utility')).toBeTruthy(); + expect(screen.getByTestId('pdf-container')).toBeTruthy(); + expect(screen.getByTestId('qr-code-element')).toBeTruthy(); + }); + }); +}); diff --git a/libs/vc-ui/src/lib/PdfViewer/PdfViewer.stories.tsx b/libs/vc-ui/src/lib/PdfViewer/PdfViewer.stories.tsx new file mode 100644 index 00000000..0eaecac4 --- /dev/null +++ b/libs/vc-ui/src/lib/PdfViewer/PdfViewer.stories.tsx @@ -0,0 +1,19 @@ +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import { PdfViewer } from './PdfViewer'; +import { AANZFTA_COO_PARTIAL } from '../fixtures'; + +const Story: ComponentMeta = { + component: PdfViewer, + title: 'PdfViewer', +}; +export default Story; + +const Template: ComponentStory = (args) => ( + +); + +export const Primary = Template.bind({}); + +Primary.args = { + document: AANZFTA_COO_PARTIAL, +}; diff --git a/libs/vc-ui/src/lib/PdfViewer/PdfViewer.tsx b/libs/vc-ui/src/lib/PdfViewer/PdfViewer.tsx new file mode 100644 index 00000000..b287d4c2 --- /dev/null +++ b/libs/vc-ui/src/lib/PdfViewer/PdfViewer.tsx @@ -0,0 +1,62 @@ +import { useRef } from 'react'; +import { Box } from '@mui/material'; +import { PdfRenderer } from '../PdfRenderer/PdfRenderer'; +import { VerifiableCredential } from '@dvp/api-interfaces'; +import { VcUtility } from '../VcUtility'; + +export interface IPDFViewer { + document: VerifiableCredential; +} + +export const PdfViewer = ({ document }: IPDFViewer) => { + const pdfContainer = useRef(null); + + const onPrint = () => { + if (pdfContainer.current) { + window.print(); + } + }; + + const selfLink = document?.credentialSubject?.links?.self?.href + ? document?.credentialSubject['links']['self']['href'] + : ''; + return document?.credentialSubject['originalDocument'] ? ( + <> + + + + + + + + + + ) : null; +}; diff --git a/libs/vc-ui/src/lib/PdfViewer/index.ts b/libs/vc-ui/src/lib/PdfViewer/index.ts new file mode 100644 index 00000000..5fc0566f --- /dev/null +++ b/libs/vc-ui/src/lib/PdfViewer/index.ts @@ -0,0 +1 @@ +export * from './PdfViewer'; diff --git a/libs/vc-ui/src/lib/QrCode/QRCode.spec.tsx b/libs/vc-ui/src/lib/QrCode/QRCode.spec.tsx new file mode 100644 index 00000000..f4c0c90d --- /dev/null +++ b/libs/vc-ui/src/lib/QrCode/QRCode.spec.tsx @@ -0,0 +1,19 @@ +import { getByTestId, render } from '@testing-library/react'; +import { QrCode } from './QrCode'; + +describe('QRCode', () => { + it('should render successfully', () => { + const { baseElement } = render(); + expect(baseElement).toBeTruthy(); + }); + + it('should use the size specified', () => { + const { baseElement } = render( + + ); + const qrcodeElement = getByTestId(baseElement, 'qrcode'); + + expect(qrcodeElement.getAttribute('width')).toBe('300'); + expect(qrcodeElement.getAttribute('height')).toBe('300'); + }); +}); diff --git a/libs/vc-ui/src/lib/QrCode/QrCode.stories.tsx b/libs/vc-ui/src/lib/QrCode/QrCode.stories.tsx new file mode 100644 index 00000000..77b364fb --- /dev/null +++ b/libs/vc-ui/src/lib/QrCode/QrCode.stories.tsx @@ -0,0 +1,17 @@ +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import { QrCode } from './QrCode'; + +const Story: ComponentMeta = { + component: QrCode, + title: 'QRCode', +}; +export default Story; + +const Template: ComponentStory = (args) => ; + +export const Primary = Template.bind({}); + +Primary.args = { + url: 'test-url', + qrCodeOptions: { width: 200 }, +}; diff --git a/libs/vc-ui/src/lib/QrCode/QrCode.tsx b/libs/vc-ui/src/lib/QrCode/QrCode.tsx new file mode 100644 index 00000000..4b47f5c4 --- /dev/null +++ b/libs/vc-ui/src/lib/QrCode/QrCode.tsx @@ -0,0 +1,94 @@ +import * as QRCode from 'qrcode'; +import { MouseEventHandler, useEffect, useRef } from 'react'; +import { throttle } from 'lodash'; +import { Box } from '@mui/material'; + +interface IQrCode { + url: string; + qrCodeOptions?: QRCode.QRCodeRenderersOptions; + onResize?: ( + newQrWidth: number, + elementMovementX: number, + elementMovementY: number + ) => void; + isResizable?: boolean; +} + +export const QrCode = ({ + url, + qrCodeOptions = { width: 200 }, + onResize, + isResizable = false, +}: IQrCode) => { + const canvasRef = useRef(null); + const containerRef = useRef(null); + + let xPosition = 0; + let qrWidth = 0; + + const mouseDownHandler: MouseEventHandler = function (e) { + if (containerRef.current) { + xPosition = e.clientX; + qrWidth = containerRef.current.clientWidth; + + document.addEventListener('mousemove', mouseMoveHandler); + document.addEventListener('mouseup', mouseUpHandler); + } + }; + + const mouseMoveHandler = function (e: MouseEvent) { + const movementX = e.movementX; + const movementY = e.movementY; + const resizeChangeInPosition = e.clientX - xPosition; + const newQrCodeWidth = qrWidth + resizeChangeInPosition; + + if (onResize && newQrCodeWidth >= 80) { + throttle(() => { + onResize(newQrCodeWidth, movementX, movementY); + }, 300)(); + } + e.stopPropagation(); + }; + + const mouseUpHandler = function () { + document.removeEventListener('mousemove', mouseMoveHandler); + document.removeEventListener('mouseup', mouseUpHandler); + }; + + useEffect(() => { + if (canvasRef.current) { + QRCode.toCanvas(canvasRef.current, url, { + ...qrCodeOptions, + }).catch(() => { + throw new Error('Error generating the QRCode'); + }); + } + }, [url, qrCodeOptions]); + + return ( + + + {isResizable && ( + + )} + + ); +}; diff --git a/libs/vc-ui/src/lib/QrCode/index.ts b/libs/vc-ui/src/lib/QrCode/index.ts new file mode 100644 index 00000000..693e3ed3 --- /dev/null +++ b/libs/vc-ui/src/lib/QrCode/index.ts @@ -0,0 +1 @@ +export * from './QrCode'; diff --git a/libs/vc-ui/src/lib/RendererViewer/RendereViewer.spec.tsx b/libs/vc-ui/src/lib/RendererViewer/RendereViewer.spec.tsx new file mode 100644 index 00000000..bef907ae --- /dev/null +++ b/libs/vc-ui/src/lib/RendererViewer/RendereViewer.spec.tsx @@ -0,0 +1,104 @@ +import { WrappedVerifiableCredential } from '@dvp/api-interfaces'; +import { act, render, waitFor } from '@testing-library/react'; +import { CHAFTA_COO } from '../fixtures'; +import { + reducer, + RendererViewer, + VCDocumentActionType, + _getRendererURl, +} from './RendererViewer'; + +describe('RendererViewer', () => { + it('should render successfully', () => { + const { baseElement } = render(); + expect(baseElement).toBeTruthy(); + }); + + it('should show VcUtility component', () => { + const { getByTestId } = render(); + expect(getByTestId('vc-utility')).toBeTruthy(); + }); + + it('should show the tabs', async () => { + const { getByTestId, getByRole } = render( + + ); + + await waitFor(() => { + expect(getByRole('tab', { selected: true }).textContent).toBe('Render'); + expect(getByRole('tab', { name: 'Json view' })).toBeTruthy(); + expect(getByTestId('tab-panel-0')).toBeTruthy(); + }); + }); + + it('should switch to the second tab and display the contents', async () => { + const { getByRole, getByTestId } = render( + + ); + + act(() => { + getByRole('tab', { name: 'Json view' }).click(); + }); + + await waitFor(() => { + expect(getByRole('tab', { selected: true }).textContent).toBe('Json'); + expect(JSON.parse(getByTestId('tab-panel-1')?.textContent)).toMatchObject( + CHAFTA_COO.credentialSubject + ); + }); + }); +}); + +describe('_getRendererURl', () => { + it('should return renderer url if url is specified in document', () => { + if (CHAFTA_COO.openAttestationMetadata.template) { + CHAFTA_COO.openAttestationMetadata.template.url = 'http://soopadoopa.com'; + } + + const res = _getRendererURl(CHAFTA_COO); + expect(res).toStrictEqual('http://soopadoopa.com'); + }); + + it('should return default renderer url if url is not specified in document', () => { + if (CHAFTA_COO.openAttestationMetadata.template) { + CHAFTA_COO.openAttestationMetadata.template.url = ''; + } + + const res = _getRendererURl(CHAFTA_COO); + expect(res).toStrictEqual('https://generic-templates.tradetrust.io'); + }); + + describe('reducer', () => { + it('should return the initial state', () => { + const action = { + type: 'TEST' as VCDocumentActionType, + payload: [], + }; + + expect(reducer({ document: CHAFTA_COO }, action)).toStrictEqual({ + document: CHAFTA_COO, + }); + }); + + it('should handle OBFUSCATE', () => { + const action = { + type: VCDocumentActionType.Obfuscate, + payload: 'credentialSubject.links', + }; + + expect(CHAFTA_COO.credentialSubject['links']).toBeDefined(); + expect( + (CHAFTA_COO as WrappedVerifiableCredential).proof.privacy.obfuscated + .length + ).toStrictEqual(0); + + const res = reducer({ document: CHAFTA_COO }, action); + + expect(res.document?.credentialSubject['links']).not.toBeDefined(); + expect( + (res.document as WrappedVerifiableCredential).proof.privacy.obfuscated + .length + ).toStrictEqual(1); + }); + }); +}); diff --git a/libs/vc-ui/src/lib/RendererViewer/RendereViewer.stories.tsx b/libs/vc-ui/src/lib/RendererViewer/RendereViewer.stories.tsx new file mode 100644 index 00000000..ab890c17 --- /dev/null +++ b/libs/vc-ui/src/lib/RendererViewer/RendereViewer.stories.tsx @@ -0,0 +1,17 @@ +import type { ComponentStory, ComponentMeta } from '@storybook/react'; +import { RendererViewer } from './RendererViewer'; +import { CHAFTA_COO } from '../fixtures'; + +const Story: ComponentMeta = { + component: RendererViewer, + title: 'RendererViewer', +}; +export default Story; + +const Template: ComponentStory = (args) => ( + +); + +export const Primary = Template.bind({}); + +Primary.args = { document: CHAFTA_COO }; diff --git a/libs/vc-ui/src/lib/RendererViewer/RendererViewer.tsx b/libs/vc-ui/src/lib/RendererViewer/RendererViewer.tsx new file mode 100644 index 00000000..976a0203 --- /dev/null +++ b/libs/vc-ui/src/lib/RendererViewer/RendererViewer.tsx @@ -0,0 +1,200 @@ +import { + OAVerifiableCredential, + VerifiableCredential, + WrappedVerifiableCredential, +} from '@dvp/api-interfaces'; + +import { + FrameActions, + FrameConnector, + print, + renderDocument, +} from '@govtechsg/decentralized-renderer-react-components'; +import { obfuscateDocument } from '@govtechsg/open-attestation'; +import { TabContext, TabList, TabPanel } from '@mui/lab'; +import { Box, Tab } from '@mui/material'; +import { useMemo, useReducer, useRef, useState } from 'react'; +import { DEFAULT_RENDERER } from '../constants'; +import { Text } from '../Text'; +import { VcUtility } from '../VcUtility'; + +export type VCDocumentState = { + document?: OAVerifiableCredential | VerifiableCredential; +}; + +export enum VCDocumentActionType { + 'Obfuscate' = 'obfuscate', +} + +export type VCDocumentAction = { + type: VCDocumentActionType.Obfuscate; + payload: string[] | string; +}; + +// We're implementing a reducer to separate state management. +// This enables the document to be updated and shared across the renderer, viewer and utility +export const reducer = ( + state: VCDocumentState, + action: VCDocumentAction +): VCDocumentState => { + switch (action.type) { + case VCDocumentActionType.Obfuscate: { + let res = state.document as WrappedVerifiableCredential; + if (res) { + res = obfuscateDocument(res, action.payload); + // Redact self url as well if present + if (res.credentialSubject.links) { + res = obfuscateDocument(res, 'credentialSubject.links'); + } + } + return { document: res }; + } + default: + return state; + } +}; + +export interface IRendererViewer { + document: OAVerifiableCredential | VerifiableCredential; +} + +export const _getRendererURl = ( + document: OAVerifiableCredential | VerifiableCredential +) => { + const rendererURl = (document as OAVerifiableCredential) + ?.openAttestationMetadata?.template?.url; + if (rendererURl) { + // interpolation is done to make ts happy + return `${rendererURl}`; + } + return DEFAULT_RENDERER; +}; + +export const RendererViewer = ({ document }: IRendererViewer) => { + const source = _getRendererURl(document); + + // TODO: Ascertain why the renderer url lives in SVIP Document + const hasRenderer = useMemo( + () => !!(document as OAVerifiableCredential)?.openAttestationMetadata, + [document] + ); + + const [height, setHeight] = useState(250); + const SCROLLBAR_WIDTH = 20; // giving scrollbar a default width as there are no perfect ways to get it + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const toFrame = useRef(); + const [tabIndex, setTabIndex] = useState(hasRenderer ? '0' : '1'); + + const [state, dispatchAction] = useReducer(reducer, { + document, + }); + + const onConnected = + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (frame: any) => { + toFrame.current = frame; + if (toFrame.current && state.document) { + toFrame.current( + renderDocument({ document: state.document as OAVerifiableCredential }) + ); + } + }; + + const dispatch = (action: FrameActions): void => { + if (action.type === 'UPDATE_HEIGHT') { + setHeight(action.payload + SCROLLBAR_WIDTH); + } + + if (action.type === 'OBFUSCATE') { + dispatchAction({ + type: VCDocumentActionType.Obfuscate, + payload: action.payload, + }); + } + }; + + const onPrint = () => { + if (toFrame.current) { + toFrame.current(print()); + } + }; + + const handleChange = (event: React.SyntheticEvent, newValue: string) => { + setTabIndex(newValue); + }; + + return ( + <> + {state.document && ( + + )} + + + + {hasRenderer && ( + { + setTabIndex('0'); + }} + /> + )} + { + setTabIndex('1'); + }} + /> + + + {hasRenderer && ( + + + + )} + + + {state.document?.credentialSubject ? ( + + ) : ( + Credential subject is empty or does not exist. + )} + + + + + ); +}; diff --git a/libs/vc-ui/src/lib/RendererViewer/index.ts b/libs/vc-ui/src/lib/RendererViewer/index.ts new file mode 100644 index 00000000..21a858a7 --- /dev/null +++ b/libs/vc-ui/src/lib/RendererViewer/index.ts @@ -0,0 +1 @@ +export * from './RendererViewer'; diff --git a/libs/vc-ui/src/lib/Text/Text.stories.tsx b/libs/vc-ui/src/lib/Text/Text.stories.tsx new file mode 100644 index 00000000..03e7ed85 --- /dev/null +++ b/libs/vc-ui/src/lib/Text/Text.stories.tsx @@ -0,0 +1,14 @@ +import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import { Text } from './Text'; + +const Story: ComponentMeta = { + component: Text, + title: 'Text', +}; +export default Story; + +export const Default: ComponentStory = () => Default; + +export const Header5: ComponentStory = () => ( + Header5 +); diff --git a/libs/vc-ui/src/lib/Text/Text.test.tsx b/libs/vc-ui/src/lib/Text/Text.test.tsx new file mode 100644 index 00000000..8c87fa56 --- /dev/null +++ b/libs/vc-ui/src/lib/Text/Text.test.tsx @@ -0,0 +1,11 @@ +import '@testing-library/jest-dom'; +import { render } from '@testing-library/react'; +import { Text } from './Text'; + +describe('Text', () => { + it('should render correctly', () => { + const { baseElement } = render(Soopa Doopa); + + expect(baseElement).toMatchSnapshot(); + }); +}); diff --git a/libs/vc-ui/src/lib/Text/Text.tsx b/libs/vc-ui/src/lib/Text/Text.tsx new file mode 100644 index 00000000..c52f1482 --- /dev/null +++ b/libs/vc-ui/src/lib/Text/Text.tsx @@ -0,0 +1,12 @@ +import { Typography, TypographyProps } from '@mui/material'; +import React from 'react'; + +export const Text = React.forwardRef( + ({ children, variant = 'body2', ...rest }, ref) => { + return ( + + {children} + + ); + } +); diff --git a/libs/vc-ui/src/lib/Text/__snapshots__/Text.test.tsx.snap b/libs/vc-ui/src/lib/Text/__snapshots__/Text.test.tsx.snap new file mode 100644 index 00000000..cc0f8cda --- /dev/null +++ b/libs/vc-ui/src/lib/Text/__snapshots__/Text.test.tsx.snap @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Text should render correctly 1`] = ` + +
+

+ Soopa Doopa +

+
+ +`; diff --git a/libs/vc-ui/src/lib/Text/index.ts b/libs/vc-ui/src/lib/Text/index.ts new file mode 100644 index 00000000..b0c76af0 --- /dev/null +++ b/libs/vc-ui/src/lib/Text/index.ts @@ -0,0 +1 @@ +export * from './Text'; diff --git a/libs/vc-ui/src/lib/VCOptions/VCOptions.spec.tsx b/libs/vc-ui/src/lib/VCOptions/VCOptions.spec.tsx new file mode 100644 index 00000000..aee5484c --- /dev/null +++ b/libs/vc-ui/src/lib/VCOptions/VCOptions.spec.tsx @@ -0,0 +1,189 @@ +import { fireEvent, within } from '@testing-library/react'; +import { render } from '../../utils'; +import { VCOptions } from './VCOptions'; + +// eslint-disable-next-line @typescript-eslint/no-unsafe-return +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useNavigate: () => mockNavigate, +})); + +const mockNavigate = jest.fn(); +const handleSubmitMock = jest.fn(); + +const formMock = { + schema: {}, + uiSchema: {}, +}; + +const formsMock = [ + { + id: '001', + name: 'testForm', + displayName: 'testForm', + fullForm: formMock, + partialForm: formMock, + }, +]; + +describe('VCOptions', () => { + beforeEach(() => jest.clearAllMocks()); + + it('should render correctly', () => { + const { baseElement } = render( + + ); + + expect(baseElement).toMatchSnapshot(); + }); + + describe('document type', () => { + it('should display document types', () => { + const { getAllByRole } = render( + + ); + + fireEvent.mouseDown(getAllByRole('listbox')[0].children[0]); + const listbox = within(getAllByRole('listbox')[0]); + + expect(listbox.getByText(formsMock[0].name)).toBeTruthy(); + }); + + it('should update the value of the listbox', () => { + const { getAllByRole } = render( + + ); + + fireEvent.mouseDown(getAllByRole('listbox')[0].children[0]); + const listbox = within(getAllByRole('listbox')[0]); + fireEvent.click(listbox.getByText(formsMock[0].name)); + + expect(getAllByRole('listbox')[0].querySelector('input')?.value).toBe( + formsMock[0].id + ); + }); + }); + + describe('form type', () => { + it('should display form types', () => { + const { getAllByRole } = render( + + ); + + fireEvent.mouseDown(getAllByRole('listbox')[1].children[0]); + const listbox = within(getAllByRole('listbox')[0]); + + expect(listbox.getByText('Partial Form')).toBeTruthy(); + expect(listbox.getByText('Full Form')).toBeTruthy(); + }); + + it('should update the value of the listbox', () => { + const { getAllByRole } = render( + + ); + + fireEvent.mouseDown(getAllByRole('listbox')[1].children[0]); + const listbox = within(getAllByRole('listbox')[0]); + fireEvent.click(listbox.getByText('Partial Form')); + + expect(getAllByRole('listbox')[1].querySelector('input')?.value).toBe( + 'partial' + ); + + fireEvent.click(listbox.getByText('Full Form')); + + expect(getAllByRole('listbox')[1].querySelector('input')?.value).toBe( + 'full' + ); + }); + }); + + describe('credential type', () => { + it('should display credential types', () => { + const { getAllByRole } = render( + + ); + + fireEvent.mouseDown(getAllByRole('listbox')[2].children[0]); + const listbox = within(getAllByRole('listbox')[0]); + + expect(listbox.getByText('Open Attestation')).toBeTruthy(); + expect( + listbox.getByText('Silicon Valley Innovation Program') + ).toBeTruthy(); + }); + + it('should update the value of the listbox', () => { + const { getAllByRole } = render( + + ); + + fireEvent.mouseDown(getAllByRole('listbox')[2].children[0]); + const listbox = within(getAllByRole('listbox')[0]); + fireEvent.click(listbox.getByText('Open Attestation')); + + expect(getAllByRole('listbox')[2].querySelector('input')?.value).toBe( + 'oa' + ); + + fireEvent.click(listbox.getByText('Silicon Valley Innovation Program')); + + expect(getAllByRole('listbox')[2].querySelector('input')?.value).toBe( + 'svip' + ); + }); + }); + + it('should display both buttons', () => { + const { getByText } = render( + + ); + + expect(getByText('Cancel')).toBeTruthy(); + expect(getByText('Next')).toBeTruthy(); + }); + + it('should disable the next button if options are not selected', () => { + const { getByText } = render( + + ); + + expect(getByText('Next').getAttribute('disabled')); + }); + + it('should call the callback function when the user clicks next', () => { + const { getByText, getAllByRole } = render( + + ); + + fireEvent.mouseDown(getAllByRole('listbox')[0].children[0]); + const documentListbox = within(getAllByRole('listbox')[0]); + fireEvent.click(documentListbox.getByText(formsMock[0].name)); + + fireEvent.mouseDown(getAllByRole('listbox')[1].children[0]); + const formListBox = within(getAllByRole('listbox')[0]); + fireEvent.click(formListBox.getByText('Full Form')); + + fireEvent.mouseDown(getAllByRole('listbox')[2].children[0]); + const credentialListBox = within(getAllByRole('listbox')[0]); + fireEvent.click(credentialListBox.getByText('Open Attestation')); + + fireEvent.click(getByText('Next')); + + expect(handleSubmitMock).toBeCalledWith({ + ...formsMock[0], + formType: 'full', + credentialType: 'oa', + }); + }); + + it('should go to home when cancel is clicked', () => { + const { getByText } = render( + + ); + + fireEvent.click(getByText('Cancel')); + + expect(mockNavigate).toBeCalledWith('/'); + }); +}); diff --git a/libs/vc-ui/src/lib/VCOptions/VCOptions.stories.tsx b/libs/vc-ui/src/lib/VCOptions/VCOptions.stories.tsx new file mode 100644 index 00000000..53f875f9 --- /dev/null +++ b/libs/vc-ui/src/lib/VCOptions/VCOptions.stories.tsx @@ -0,0 +1,30 @@ +import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import { Route } from 'react-router-dom'; +import { RouterWrapper } from '../../utils'; +import { FormOption, VCOptions } from './VCOptions'; + +const Story: ComponentMeta = { + component: VCOptions, + title: 'VCOptions', +}; +export default Story; + +const Template: ComponentStory = (args) => ( + + } /> + +); + +export const Primary = Template.bind({}); + +const form = { + schema: {}, + uiSchema: {}, +}; + +Primary.args = { + forms: [{ id: '001', name: 'sampleForm', fullForm: form, partialForm: form }], + onFormSelected: (value: FormOption) => { + return; + }, +}; diff --git a/libs/vc-ui/src/lib/VCOptions/VCOptions.tsx b/libs/vc-ui/src/lib/VCOptions/VCOptions.tsx new file mode 100644 index 00000000..0fff08e9 --- /dev/null +++ b/libs/vc-ui/src/lib/VCOptions/VCOptions.tsx @@ -0,0 +1,203 @@ +import { + Box, + FormControl, + InputLabel, + MenuItem, + Select, + SelectChangeEvent, +} from '@mui/material'; +import { FunctionComponent, useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { Button } from '../Button'; +import { Text } from '../Text'; + +export interface Form { + schema: Record; + uiSchema: Record; +} + +export interface FormOption { + id: string; + name: string; + displayName: string; + fullForm?: Form; + partialForm?: Form; + credentialType?: string; + formType?: string; +} + +interface VCOptionsProps { + forms: FormOption[]; + onFormSelected: (value: FormOption) => void; +} + +export const VCOptions: FunctionComponent = ({ + forms, + onFormSelected, +}) => { + const [availableForms, setAvailableForms] = useState([]); + const [selectedDocument, setSelectedDocument] = useState(''); + const [selectedDocumentName, setSelectedDocumentName] = useState(''); + const [selectedForm, setSelectedForm] = useState(''); + const [selectedCredential, setSelectedCredential] = useState(''); + + const navigate = useNavigate(); + + const goToHome = () => { + navigate('/'); + }; + + useEffect(() => { + setAvailableForms(forms); + }, [forms]); + + const handleSelectedDocument = (event: SelectChangeEvent) => { + setSelectedDocument(event.target.value); + setSelectedDocumentName( + availableForms.filter((value) => value.id === event.target.value)[0].name + ); + }; + + const handleSubmit = () => { + const form = availableForms.filter( + (form) => form.id === selectedDocument + )[0]; + + const formWithTypes = { + ...form, + credentialType: selectedCredential, + formType: selectedForm, + }; + + onFormSelected(formWithTypes); + }; + + const isComplete = selectedDocument && selectedForm && selectedCredential; + + return ( + + + Verifiable Credential Options + + + + Document Type + + + + + + + + Form Type + + + + + + + + Credential Type + + + + + + + + + + + +`; diff --git a/libs/vc-ui/src/lib/VCOptions/index.ts b/libs/vc-ui/src/lib/VCOptions/index.ts new file mode 100644 index 00000000..f3f0e00a --- /dev/null +++ b/libs/vc-ui/src/lib/VCOptions/index.ts @@ -0,0 +1 @@ +export * from './VCOptions'; diff --git a/libs/vc-ui/src/lib/VcUtility/VcUtility.spec.tsx b/libs/vc-ui/src/lib/VcUtility/VcUtility.spec.tsx new file mode 100644 index 00000000..9c68ba55 --- /dev/null +++ b/libs/vc-ui/src/lib/VcUtility/VcUtility.spec.tsx @@ -0,0 +1,153 @@ +import { act, render } from '@testing-library/react'; +import * as utils from '../../utils'; +import { AANZFTA_COO } from '../fixtures/documents'; +import { VcUtility } from './VcUtility'; + +const mockOnPrint = jest.fn(); +const mockCopyToClipboard = jest + .spyOn(utils, 'copyToClipboard') + .mockImplementation(() => Promise.resolve()); + +const vcSelfLink = AANZFTA_COO.credentialSubject.links?.self?.href ?? ''; + +describe('VcUtility', () => { + it('should render successfully', () => { + const { baseElement } = render( + + ); + expect(baseElement).toBeTruthy(); + }); + + it('should display qrcode, print and download button if self link exists', () => { + const { getByTestId } = render( + + ); + + expect(getByTestId('uri-dropdown-button')).toBeTruthy(); + expect(getByTestId('print-button')).toBeTruthy(); + expect(getByTestId('download-button')).toBeTruthy(); + }); + + it('should only display print and download button if self link does not exists', () => { + const { queryByTestId, getByTestId } = render( + + ); + expect(queryByTestId('uri-dropdown-button')).toBeNull(); + expect(getByTestId('print-button')).toBeTruthy(); + expect(getByTestId('download-button')).toBeTruthy(); + }); + + it('should only display print button if isPrintable', () => { + const { queryByTestId, getByTestId } = render( + + ); + expect(queryByTestId('print-button')).toBeNull(); + expect(getByTestId('uri-dropdown-button')).toBeTruthy(); + expect(getByTestId('download-button')).toBeTruthy(); + }); + + it('should display uri and qrcode when dropdown button is clicked', () => { + const { getByTestId, getByDisplayValue } = render( + + ); + act(() => { + getByTestId('uri-dropdown-button').click(); + }); + + expect(getByDisplayValue(vcSelfLink)).toBeTruthy(); + expect(getByTestId('copy-uri-button')).toBeTruthy(); + expect(getByTestId('qrcode')).toBeTruthy(); + }); + + it('should call the copyToClipboard function when copy uri button is clicked', () => { + const { getByTestId } = render( + + ); + act(() => { + getByTestId('uri-dropdown-button').click(); + getByTestId('copy-uri-button').click(); + }); + + expect(mockCopyToClipboard).toBeCalledTimes(1); + expect(mockCopyToClipboard).toHaveBeenCalledWith(vcSelfLink); + }); + + it('should call the print function when print button is clicked', () => { + const { getByTestId } = render( + + ); + act(() => { + getByTestId('print-button').click(); + }); + + expect(mockOnPrint).toBeCalledTimes(1); + }); + + it('should provide a link to download the vc', () => { + const modifiedVc = { + ...AANZFTA_COO, + credentialSubject: { + ...AANZFTA_COO.credentialSubject, + name: 'testName', + }, + }; + const { getByTestId } = render( + + ); + const downloadLink = getByTestId('download-link'); + expect(downloadLink.getAttribute('href')).toBe( + `data:text/json;,${encodeURIComponent( + JSON.stringify(modifiedVc, null, 2) + )}` + ); + expect(downloadLink.getAttribute('download')).toBe('testName.json'); + }); + + it('should use the default file name if vc subject does not contain a name property', () => { + const { getByTestId } = render( + + ); + expect(getByTestId('download-link').getAttribute('download')).toBe( + 'untitled.json' + ); + }); +}); diff --git a/libs/vc-ui/src/lib/VcUtility/VcUtility.stories.tsx b/libs/vc-ui/src/lib/VcUtility/VcUtility.stories.tsx new file mode 100644 index 00000000..4a645bc6 --- /dev/null +++ b/libs/vc-ui/src/lib/VcUtility/VcUtility.stories.tsx @@ -0,0 +1,21 @@ +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import { AANZFTA_COO } from '../fixtures'; +import { VcUtility } from './VcUtility'; + +const Story: ComponentMeta = { + component: VcUtility, + title: 'VcUtility', +}; +export default Story; + +const Template: ComponentStory = (args) => ( + +); + +export const Primary = Template.bind({}); + +Primary.args = { + document: AANZFTA_COO, + onPrint: () => ({}), + isPrintable: true, +}; diff --git a/libs/vc-ui/src/lib/VcUtility/VcUtility.tsx b/libs/vc-ui/src/lib/VcUtility/VcUtility.tsx new file mode 100644 index 00000000..b434d35f --- /dev/null +++ b/libs/vc-ui/src/lib/VcUtility/VcUtility.tsx @@ -0,0 +1,154 @@ +import { VerifiableCredential } from '@dvp/api-interfaces'; +import ContentCopyIcon from '@mui/icons-material/ContentCopy'; +import { + Box, + IconButton, + InputAdornment, + Link, + Paper, + Stack, + TextField, +} from '@mui/material'; +import { useState } from 'react'; +import { copyToClipboard } from '../../utils'; +import { Button } from '../Button'; +import { QrCode } from '../QrCode'; + +export interface IVcUtility { + document: VerifiableCredential; + onPrint: () => void; + isPrintable: boolean; +} + +export const VcUtility = ({ document, onPrint, isPrintable }: IVcUtility) => { + const [isQrCodePopoverOpen, setIsQrCodePopoverOpen] = useState(false); + const [isUriCopiedToClipboard, setIsUriCopiedToClipboard] = useState(false); + + const verifiableCredential = document; + const { name, links } = verifiableCredential.credentialSubject ?? {}; + + const fileName = (name as string) ?? 'untitled'; + const qrcodeUrl = links?.self?.href ?? ''; + + return ( + + {qrcodeUrl && ( + +