diff --git a/.env.demo b/.env.demo index c9cd8c829..edd708ebe 100644 --- a/.env.demo +++ b/.env.demo @@ -102,6 +102,9 @@ UTILITIES_NKEY_SEED= CLOUD_WALLET_NKEY_SEED= GEOLOCATION_NKEY_SEED= NOTIFICATION_NKEY_SEED= +X509_NKEY_SEED= +OIDC4VC_ISSUANCE_NKEY_SEED= +OIDC4VC_VERIFICATION_NKEY_SEED= KEYCLOAK_DOMAIN=http://localhost:8080/ KEYCLOAK_ADMIN_URL=http://localhost:8080 @@ -141,6 +144,8 @@ ELK_PASSWORD=xxxxxx # ELK user password ORGANIZATION=credebl CONTEXT=platform APP=api +# Default is true too, if nothing is passed +HIDE_EXPERIMENTAL_OIDC_CONTROLLERS=true #Schema-file-server APP_PORT=4000 diff --git a/.env.sample b/.env.sample index cb422b084..2300a7237 100644 --- a/.env.sample +++ b/.env.sample @@ -67,7 +67,8 @@ POSTGRES_PORT=5432 POSTGRES_USER='postgres' POSTGRES_PASSWORD='xxxxx' POSTGRES_DATABASE= // Please provide your DB name - +# Default is true too, set to 'false', to view OIDC controllers in openAPI docs +HIDE_EXPERIMENTAL_OIDC_CONTROLLERS=true SENDGRID_API_KEY=xxxxxxxxxxxxxx // Please provide your sendgrid API key FRONT_END_URL=http://localhost:3000 @@ -109,18 +110,21 @@ AGENT_PROTOCOL=http GEO_LOCATION_MASTER_DATA_IMPORT_SCRIPT=/prisma/scripts/geo_location_data_import.sh UPDATE_CLIENT_CREDENTIAL_SCRIPT=/prisma/scripts/update_client_credential_data.sh -USER_NKEY_SEED= xxxxxxxxxxxxx // Please provide Nkeys secret for user service -API_GATEWAY_NKEY_SEED= xxxxxxxxxxxxx // Please provide Nkeys secret for api-gateway -ORGANIZATION_NKEY_SEED= xxxxxxxxxxxxx // Please provide Nkeys secret for organization service -AGENT_PROVISIONING_NKEY_SEED= xxxxxxxxxxxxx // Please provide Nkeys secret for agent provisioning service -AGENT_SERVICE_NKEY_SEED= xxxxxxxxxxxxx // Please provide Nkeys secret for agent service -VERIFICATION_NKEY_SEED= xxxxxxxxxxxxx // Please provide Nkeys secret for verification service -ISSUANCE_NKEY_SEED= xxxxxxxxxxxxx // Please provide Nkeys secret for issuance service -CONNECTION_NKEY_SEED= xxxxxxxxxxxxx // Please provide Nkeys secret for connection service -CREDENTAILDEFINITION_NKEY_SEED= xxxxxxxxxxxxx // Please provide Nkeys secret for credential-definition service -SCHEMA_NKEY_SEED= xxxxxxxxxxxxx // Please provide Nkeys secret for schema service -UTILITIES_NKEY_SEED= xxxxxxxxxxxxx // Please provide Nkeys secret for utilities service -GEOLOCATION_NKEY_SEED= xxxxxxxxxxx // Please provide Nkeys secret for geo-location service +USER_NKEY_SEED=xxxxxxxxxxxxx // Please provide Nkeys secret for user service +API_GATEWAY_NKEY_SEED=xxxxxxxxxxxxx // Please provide Nkeys secret for api-gateway +ORGANIZATION_NKEY_SEED=xxxxxxxxxxxxx // Please provide Nkeys secret for organization service +AGENT_PROVISIONING_NKEY_SEED=xxxxxxxxxxxxx // Please provide Nkeys secret for agent provisioning service +AGENT_SERVICE_NKEY_SEED=xxxxxxxxxxxxx // Please provide Nkeys secret for agent service +VERIFICATION_NKEY_SEED=xxxxxxxxxxxxx // Please provide Nkeys secret for verification service +ISSUANCE_NKEY_SEED=xxxxxxxxxxxxx // Please provide Nkeys secret for issuance service +CONNECTION_NKEY_SEED=xxxxxxxxxxxxx // Please provide Nkeys secret for connection service +CREDENTAILDEFINITION_NKEY_SEED=xxxxxxxxxxxxx // Please provide Nkeys secret for credential-definition service +SCHEMA_NKEY_SEED=xxxxxxxxxxxxx // Please provide Nkeys secret for schema service +UTILITIES_NKEY_SEED=xxxxxxxxxxxxx // Please provide Nkeys secret for utilities service +GEOLOCATION_NKEY_SEED=xxxxxxxxxxx // Please provide Nkeys secret for geo-location service +X509_NKEY_SEED=xxxxxxxxxxx // Please provide Nkeys secret for x509 service +OIDC4VC_ISSUANCE_NKEY_SEED=xxxxxxxxxxx // Please provide Nkeys secret for x509 service +OIDC4VC_VERIFICATION_NKEY_SEED=xxxxxxxxxxx // Please provide Nkeys secret for x509 service AFJ_AGENT_TOKEN_PATH=/apps/agent-provisioning/AFJ/token/ diff --git a/.github/workflows/continuous-delivery.yml b/.github/workflows/continuous-delivery.yml index 8bc6c7bb0..f6ba08bb7 100644 --- a/.github/workflows/continuous-delivery.yml +++ b/.github/workflows/continuous-delivery.yml @@ -31,6 +31,9 @@ jobs: - webhook - organization - seed + - x509 + - oid4vc-issuance + - oid4vc-verification permissions: contents: read diff --git a/Dockerfiles/Dockerfile.oid4vc-issuance b/Dockerfiles/Dockerfile.oid4vc-issuance new file mode 100644 index 000000000..c3ac588a2 --- /dev/null +++ b/Dockerfiles/Dockerfile.oid4vc-issuance @@ -0,0 +1,43 @@ +# Stage 1: Build the application +FROM node:18-alpine as build +# Install OpenSSL +RUN apk add --no-cache openssl +RUN npm install -g pnpm +# Set the working directory +WORKDIR /app + +# Copy package.json and package-lock.json +COPY package.json ./ +COPY pnpm-workspace.yaml ./ + +ENV PUPPETEER_SKIP_DOWNLOAD=true + +# Install dependencies while ignoring scripts (including Puppeteer's installation) +RUN pnpm i --ignore-scripts + +# Copy the rest of the application code +COPY . . +RUN cd libs/prisma-service && npx prisma generate + +# Build the oid4vc-issuance service +RUN npm run build oid4vc-issuance + + +# Stage 2: Create the final image +FROM node:18-alpine +# Install OpenSSL +RUN apk add --no-cache openssl + +# Set the working directory +WORKDIR /app + +# Copy the compiled code from the build stage +COPY --from=build /app/dist/apps/oid4vc-issuance/ ./dist/apps/oid4vc-issuance/ + +# Copy the libs folder from the build stage +COPY --from=build /app/libs/ ./libs/ + +COPY --from=build /app/node_modules ./node_modules + +# Set the command to run the microservice +CMD ["sh", "-c", "cd libs/prisma-service && npx prisma migrate deploy && npx prisma generate && cd ../.. && node dist/apps/oid4vc-issuance/main.js"] \ No newline at end of file diff --git a/Dockerfiles/Dockerfile.oid4vc-verification b/Dockerfiles/Dockerfile.oid4vc-verification new file mode 100644 index 000000000..3ae0eea10 --- /dev/null +++ b/Dockerfiles/Dockerfile.oid4vc-verification @@ -0,0 +1,44 @@ +# Stage 1: Build the application +FROM node:18-alpine as build +# Install OpenSSL +RUN apk add --no-cache openssl +RUN npm install -g pnpm +# Set the working directory +WORKDIR /app + +# Copy package.json and package-lock.json +COPY package.json ./ +COPY pnpm-workspace.yaml ./ + +ENV PUPPETEER_SKIP_DOWNLOAD=true + +# Install dependencies while ignoring scripts (including Puppeteer's installation) +RUN pnpm i --ignore-scripts + +# Copy the rest of the application code +COPY . . + +RUN cd libs/prisma-service && npx prisma generate + +# Build the oid4vc-verification service +RUN npm run build oid4vc-verification + + +# Stage 2: Create the final image +FROM node:18-alpine +# Install OpenSSL +RUN apk add --no-cache openssl + +# Set the working directory +WORKDIR /app + +# Copy the compiled code from the build stage +COPY --from=build /app/dist/apps/oid4vc-verification/ ./dist/apps/oid4vc-verification/ + +# Copy the libs folder from the build stage +COPY --from=build /app/libs/ ./libs/ + +COPY --from=build /app/node_modules ./node_modules + +# Set the command to run the microservice +CMD ["sh", "-c", "cd libs/prisma-service && npx prisma migrate deploy && npx prisma generate && cd ../.. && node dist/apps/oid4vc-verification/main.js"] \ No newline at end of file diff --git a/Dockerfiles/Dockerfile.x509 b/Dockerfiles/Dockerfile.x509 new file mode 100644 index 000000000..08207f431 --- /dev/null +++ b/Dockerfiles/Dockerfile.x509 @@ -0,0 +1,43 @@ +# Stage 1: Build the application +FROM node:18-alpine as build +# Install OpenSSL +RUN apk add --no-cache openssl +RUN npm install -g pnpm +# Set the working directory +WORKDIR /app + +COPY package.json ./ +COPY pnpm-workspace.yaml ./ + +ENV PUPPETEER_SKIP_DOWNLOAD=true + +# Install dependencies while ignoring scripts (including Puppeteer's installation) +RUN pnpm i --ignore-scripts + +# Copy the rest of the application code +COPY . . + +RUN cd libs/prisma-service && npx prisma generate + +# Build the x509 service +RUN npm run build x509 + + +# Stage 2: Create the final image +FROM node:18-alpine +# Install OpenSSL +RUN apk add --no-cache openssl + +# Set the working directory +WORKDIR /app + +# Copy the compiled code from the build stage +COPY --from=build /app/dist/apps/x509/ ./dist/apps/x509/ + +# Copy the libs folder from the build stage +COPY --from=build /app/libs/ ./libs/ + +COPY --from=build /app/node_modules ./node_modules + +# Set the command to run the microservice +CMD ["sh", "-c", "cd libs/prisma-service && npx prisma migrate deploy && npx prisma generate && cd ../.. && node dist/apps/x509/main.js"] \ No newline at end of file diff --git a/apps/agent-service/src/agent-service.controller.ts b/apps/agent-service/src/agent-service.controller.ts index d368781cd..bfe047476 100644 --- a/apps/agent-service/src/agent-service.controller.ts +++ b/apps/agent-service/src/agent-service.controller.ts @@ -1,4 +1,4 @@ -import { Controller } from '@nestjs/common'; +import { Controller, Logger } from '@nestjs/common'; import { MessagePattern } from '@nestjs/microservices'; import { AgentServiceService } from './agent-service.service'; import { @@ -27,9 +27,16 @@ import { user } from '@prisma/client'; import { InvitationMessage } from '@credebl/common/interfaces/agent-service.interface'; import { AgentSpinUpStatus } from '@credebl/enum/enum'; import { SignDataDto } from '../../api-gateway/src/agent-service/dto/agent-service.dto'; +import { + IX509ImportCertificateOptionsDto, + x509CertificateDecodeDto, + X509CreateCertificateOptions +} from '@credebl/common/interfaces/x509.interface'; +import { CreateVerifier, UpdateVerifier } from '@credebl/common/interfaces/oid4vp-verification'; @Controller() export class AgentServiceController { + private readonly logger = new Logger('AgentServiceController'); constructor(private readonly agentServiceService: AgentServiceService) {} /** @@ -323,4 +330,140 @@ export class AgentServiceController { async agentdetailsByOrgId(payload: { orgId: string }): Promise { return this.agentServiceService.getAgentDetails(payload.orgId); } + + @MessagePattern({ cmd: 'agent-create-oid4vc-issuer' }) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async oidcIssuerCreate(payload: { issuerCreation; url: string; orgId: string }): Promise { + return this.agentServiceService.oidcIssuerCreate(payload.issuerCreation, payload.url, payload.orgId); + } + @MessagePattern({ cmd: 'delete-oid4vc-issuer' }) + async oidcDeleteIssuer(payload: { url: string; orgId: string }): Promise { + return this.agentServiceService.deleteOidcIssuer(payload.url, payload.orgId); + } + @MessagePattern({ cmd: 'agent-create-oid4vc-template' }) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async oidcIssuerTemplate(payload: { templatePayload; url: string; orgId: string }): Promise { + return this.agentServiceService.oidcIssuerTemplate(payload.templatePayload, payload.url, payload.orgId); + } + //TODO: change message for oid4vc + @MessagePattern({ cmd: 'oid4vc-get-issuer-by-id' }) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async oidcGetIssuerById(payload: { url: string; orgId: string }): Promise { + return this.agentServiceService.oidcGetIssuerById(payload.url, payload.orgId); + } + + @MessagePattern({ cmd: 'oid4vc-get-issuers-agent-service' }) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async oidcGetIssuers(payload: { url: string; orgId: string }): Promise { + return this.agentServiceService.oidcGetIssuers(payload.url, payload.orgId); + } + + @MessagePattern({ cmd: 'agent-service-oid4vc-create-credential-offer' }) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async oidcCreateCredentialOffer(payload: { credentialPayload; url: string; orgId: string }): Promise { + return this.agentServiceService.oidcCreateCredentialOffer(payload.credentialPayload, payload.url, payload.orgId); + } + + @MessagePattern({ cmd: 'agent-service-oid4vc-update-credential-offer' }) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async oidcUpdateCredentialOffer(payload: { issuanceMetadata; url: string; orgId: string }): Promise { + return this.agentServiceService.oidcUpdateCredentialOffer(payload.issuanceMetadata, payload.url, payload.orgId); + } + + @MessagePattern({ cmd: 'agent-service-oid4vc-get-credential-offer-by-id' }) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async oidcGetCredentialOfferById(payload: { url: string; orgId: string }): Promise { + return this.agentServiceService.oidcGetCredentialOfferById(payload.url, payload.orgId); + } + + @MessagePattern({ cmd: 'agent-service-oid4vc-get-all-credential-offers' }) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async oidcGetAllCredentialOffers(payload: { url: string; orgId: string }): Promise { + return this.agentServiceService.oidcGetAllCredentialOffers(payload.url, payload.orgId); + } + + @MessagePattern({ cmd: 'agent-service-oid4vc-delete-credential-offer' }) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async oidcDeleteCredentialOffer(payload: { url: string; orgId: string }): Promise { + return this.agentServiceService.oidcDeleteCredentialOffer(payload.url, payload.orgId); + } + + @MessagePattern({ cmd: 'agent-create-x509-certificate' }) + async createX509Certificate(payload: { + options: X509CreateCertificateOptions; + url: string; + orgId: string; + }): Promise { + return this.agentServiceService.createX509Certificate(payload.options, payload.url, payload.orgId); + } + + @MessagePattern({ cmd: 'agent-decode-x509-certificate' }) + async decodeX509Certificate(payload: { + options: x509CertificateDecodeDto; + url: string; + orgId: string; + }): Promise { + return this.agentServiceService.decodeX509Certificate(payload.options, payload.url, payload.orgId); + } + + @MessagePattern({ cmd: 'agent-import-x509-certificate' }) + async importX509Certificate(payload: { + options: IX509ImportCertificateOptionsDto; + url: string; + orgId: string; + }): Promise { + return this.agentServiceService.importX509Certificate(payload.options, payload.url, payload.orgId); + } + + @MessagePattern({ cmd: 'agent-create-oid4vp-verifier' }) + async createOid4vpVerifier(payload: { + verifierDetails: CreateVerifier; + url: string; + orgId: string; + }): Promise { + this.logger.log( + `[createOid4vpVerifier] Received 'agent-create-oid4vp-verifier' request for orgId=${payload?.orgId || 'N/A'}` + ); + return this.agentServiceService.createOid4vpVerifier(payload.verifierDetails, payload.url, payload.orgId); + } + + @MessagePattern({ cmd: 'agent-delete-oid4vp-verifier' }) + async deleteOid4vpVerifier(payload: { url: string; orgId: string }): Promise { + this.logger.log( + `[deleteOid4vpVerifier] Received 'agent-delete-oid4vp-verifier' request for orgId=${payload?.orgId || 'N/A'}` + ); + return this.agentServiceService.deleteOid4vpVerifier(payload.url, payload.orgId); + } + + @MessagePattern({ cmd: 'agent-update-oid4vp-verifier' }) + async updateOid4vpVerifier(payload: { + verifierDetails: UpdateVerifier; + url: string; + orgId: string; + }): Promise { + this.logger.log( + `[updateOid4vpVerifier] Received 'agent-update-oid4vp-verifier' request for orgId=${payload?.orgId || 'N/A'}` + ); + return this.agentServiceService.updateOid4vpVerifier(payload.verifierDetails, payload.url, payload.orgId); + } + + @MessagePattern({ cmd: 'agent-get-oid4vp-verifier-session' }) + async getOid4vpVerifierSession(payload: { url: string; orgId: string }): Promise { + this.logger.log( + `[getOid4vpVerifierSession] Received 'agent-get-oid4vp-verifier-session' request for orgId=${payload?.orgId || 'N/A'}` + ); + return this.agentServiceService.getOid4vpVerifierSession(payload.url, payload.orgId); + } + + @MessagePattern({ cmd: 'agent-create-oid4vp-verification-session' }) + async oid4vpCreateVerificationSession(payload: { + sessionRequest: object; + url: string; + orgId: string; + }): Promise { + this.logger.log( + `[oid4vpCreateVerificationSession] Received 'agent-create-oid4vp-verification-session' request for orgId=${payload?.orgId || 'N/A'}` + ); + return this.agentServiceService.createOid4vpVerificationSession(payload.sessionRequest, payload.url, payload.orgId); + } } diff --git a/apps/agent-service/src/agent-service.module.ts b/apps/agent-service/src/agent-service.module.ts index d8fe46193..85aa5ad31 100644 --- a/apps/agent-service/src/agent-service.module.ts +++ b/apps/agent-service/src/agent-service.module.ts @@ -1,4 +1,4 @@ -import { CommonModule } from '@credebl/common'; +import { CommonModule, NatsInterceptor } from '@credebl/common'; import { PrismaService } from '@credebl/prisma-service'; import { Logger, Module } from '@nestjs/common'; import { ClientsModule, Transport } from '@nestjs/microservices'; @@ -17,13 +17,15 @@ import { ConfigModule as PlatformConfig } from '@credebl/config/config.module'; import { GlobalConfigModule } from '@credebl/config/global-config.module'; import { ContextInterceptorModule } from '@credebl/context/contextInterceptorModule'; import { NATSClient } from '@credebl/common/NATSClient'; - +import { APP_INTERCEPTOR } from '@nestjs/core'; @Module({ imports: [ ConfigModule.forRoot(), GlobalConfigModule, - LoggerModule, PlatformConfig, ContextInterceptorModule, + LoggerModule, + PlatformConfig, + ContextInterceptorModule, ClientsModule.register([ { name: 'NATS_CLIENT', @@ -47,7 +49,11 @@ import { NATSClient } from '@credebl/common/NATSClient'; provide: MICRO_SERVICE_NAME, useValue: 'Agent-service' }, - NATSClient + NATSClient, + { + provide: APP_INTERCEPTOR, + useClass: NatsInterceptor + } ], exports: [AgentServiceService, AgentServiceRepository, AgentServiceModule] }) diff --git a/apps/agent-service/src/agent-service.service.ts b/apps/agent-service/src/agent-service.service.ts index fb41a1801..d14d43fe9 100644 --- a/apps/agent-service/src/agent-service.service.ts +++ b/apps/agent-service/src/agent-service.service.ts @@ -47,7 +47,6 @@ import { IDidCreate, IWallet, ITenantRecord, - LedgerListResponse, ICreateConnectionInvitation, IStoreAgent, AgentHealthData, @@ -55,7 +54,9 @@ import { IAgentConfigure, OrgDid, IBasicMessage, - WalletDetails + WalletDetails, + ILedger, + IStoreOrgAgent } from './interface/agent-service.interface'; import { AgentSpinUpStatus, AgentType, DidMethod, Ledgers, OrgAgentType, PromiseResult } from '@credebl/enum/enum'; import { AgentServiceRepository } from './repositories/agent-service.repository'; @@ -63,7 +64,6 @@ import { Prisma, RecordType, ledgers, org_agents, organisation, platform_config, import { CommonConstants } from '@credebl/common/common.constant'; import { CommonService } from '@credebl/common'; import { GetSchemaAgentRedirection } from 'apps/ledger/src/schema/schema.interface'; -import { ConnectionService } from 'apps/connection/src/connection.service'; import { ResponseMessages } from '@credebl/common/response-messages'; import { Socket, io } from 'socket.io-client'; import { WebSocketGateway } from '@nestjs/websockets'; @@ -80,17 +80,21 @@ import { NATSClient } from '@credebl/common/NATSClient'; import { SignDataDto } from '../../api-gateway/src/agent-service/dto/agent-service.dto'; import { IVerificationMethod } from 'apps/organization/interfaces/organization.interface'; import { getAgentUrl } from '@credebl/common/common.utils'; +import { + IX509ImportCertificateOptionsDto, + x509CertificateDecodeDto, + X509CreateCertificateOptions +} from '@credebl/common/interfaces/x509.interface'; +import { CreateVerifier, UpdateVerifier } from '@credebl/common/interfaces/oid4vp-verification'; @Injectable() @WebSocketGateway() export class AgentServiceService { - private readonly logger = new Logger('WalletService'); + private readonly logger = new Logger('AgentServiceService'); constructor( private readonly agentServiceRepository: AgentServiceRepository, private readonly prisma: PrismaService, private readonly commonService: CommonService, - // TODO: Remove duplicate, unused variable - private readonly connectionService: ConnectionService, @Inject('NATS_CLIENT') private readonly agentServiceProxy: ClientProxy, // TODO: Remove duplicate, unused variable @Inject(CACHE_MANAGER) private cacheService: Cache, @@ -494,8 +498,15 @@ export class AgentServiceService { if (agentSpinupDto.method !== DidMethod.KEY && agentSpinupDto.method !== DidMethod.WEB) { const { network } = agentSpinupDto; const ledger = await ledgerName(network); - const ledgerList = (await this._getALlLedgerDetails()) as unknown as LedgerListResponse; - const isLedgerExist = ledgerList.response.find((existingLedgers) => existingLedgers.name === ledger); + const ledgerList = await this._getALlLedgerDetails(); + if (!ledgerList) { + throw new BadRequestException(ResponseMessages.agent.error.invalidLedger, { + cause: new Error(), + description: ResponseMessages.errorMessages.notFound + }); + } + + const isLedgerExist = ledgerList.find((existingLedgers) => existingLedgers.name === ledger); if (!isLedgerExist) { throw new BadRequestException(ResponseMessages.agent.error.invalidLedger, { cause: new Error(), @@ -508,15 +519,14 @@ export class AgentServiceService { /** * Invoke wallet create and provision with agent */ - const walletProvision = await this._walletProvision(walletProvisionPayload); - if (!walletProvision?.response) { + const agentDetails = await this._walletProvision(walletProvisionPayload); + if (!agentDetails) { this.logger.error(`Agent not able to spin-up`); throw new BadRequestException(ResponseMessages.agent.error.notAbleToSpinup, { cause: new Error(), description: ResponseMessages.errorMessages.badRequest }); } - const agentDetails = walletProvision.response; const agentEndPoint = `${process.env.API_GATEWAY_PROTOCOL}://${agentDetails.agentEndPoint}`; /** * Socket connection @@ -690,48 +700,43 @@ export class AgentServiceService { } } - async _createConnectionInvitation( - orgId: string, - user: IUserRequestInterface, - label: string - ): Promise<{ - response; - }> { + async _createConnectionInvitation(orgId: string, user: IUserRequestInterface, label: string): Promise { try { const pattern = { cmd: 'create-connection-invitation' }; const payload = { createOutOfBandConnectionInvitation: { orgId, user, label } }; - return await this.natsCall(pattern, payload); + const result = await this.natsClient.send(this.agentServiceProxy, pattern, payload); + return result; } catch (error) { - this.logger.error(`error in create-connection in wallet provision : ${JSON.stringify(error)}`); + this.logger.error(`[natsCall] - error in create-connection in wallet provision : ${JSON.stringify(error)}`); + throw new RpcException(error?.response ? error.response : error); } } - async _getALlLedgerDetails(): Promise<{ - response; - }> { + async _getALlLedgerDetails(): Promise { try { const pattern = { cmd: 'get-all-ledgers' }; const payload = {}; - return await this.natsCall(pattern, payload); + const result = await this.natsClient.send(this.agentServiceProxy, pattern, payload); + return result; } catch (error) { - this.logger.error(`error in while fetching all the ledger details : ${JSON.stringify(error)}`); + this.logger.error(`[natsCall] - error in while fetching all the ledger details : ${JSON.stringify(error)}`); + throw new RpcException(error?.response ? error.response : error); } } - async _walletProvision(payload: IWalletProvision): Promise<{ - response; - }> { + async _walletProvision(payload: IWalletProvision): Promise> { try { const pattern = { cmd: 'wallet-provisioning' }; - return await this.natsCall(pattern, payload); + const result = await this.natsClient.send>(this.agentServiceProxy, pattern, payload); + return result; } catch (error) { - this.logger.error(`error in wallet provision : ${JSON.stringify(error)}`); + this.logger.error(`[natsCall] - error in wallet provision : ${JSON.stringify(error)}`); throw error; } } @@ -792,8 +797,14 @@ export class AgentServiceService { ledger = Ledgers.Not_Applicable; } - const ledgerList = (await this._getALlLedgerDetails()) as unknown as LedgerListResponse; - const isLedgerExist = ledgerList.response.find((existingLedgers) => existingLedgers.name === ledger); + const ledgerList = await this._getALlLedgerDetails(); + if (!ledgerList) { + throw new BadRequestException(ResponseMessages.agent.error.invalidLedger, { + cause: new Error(), + description: ResponseMessages.errorMessages.notFound + }); + } + const isLedgerExist = ledgerList.find((existingLedgers) => existingLedgers.name === ledger); if (!isLedgerExist) { throw new BadRequestException(ResponseMessages.agent.error.invalidLedger, { cause: new Error(), @@ -1412,6 +1423,138 @@ export class AgentServiceService { } } + async oidcIssuerCreate(issueData, url: string, orgId: string): Promise { + try { + const getApiKey = await this.getOrgAgentApiKey(orgId); + const data = await this.commonService + .httpPost(url, issueData, { headers: { authorization: getApiKey } }) + .then(async (response) => response); + return data; + } catch (error) { + this.logger.error(`Error in oidcIssuerCreate in agent service : ${JSON.stringify(error)}`); + throw error; + } + } + + async deleteOidcIssuer(url: string, orgId: string): Promise { + try { + const getApiKey = await this.getOrgAgentApiKey(orgId); + const response = await this.commonService.httpDelete(url, { + headers: { authorization: getApiKey } + }); + if (response?.status === 204) { + return 'Data deleted successfully'; + } + } catch (error) { + this.logger.error(`Error in deleteOidcIssuer in agent service : ${JSON.stringify(error)}`); + throw error; + } + } + + async oidcGetIssuerById(url: string, orgId: string): Promise { + try { + const getApiKey = await this.getOrgAgentApiKey(orgId); + const data = await this.commonService + .httpGet(url, { headers: { authorization: getApiKey } }) + .then(async (response) => response); + return data; + } catch (error) { + this.logger.error(`Error in oidcGetIssuerById in agent service : ${JSON.stringify(error)}`); + throw error; + } + } + + async oidcGetIssuers(url: string, orgId: string): Promise { + try { + const getApiKey = await this.getOrgAgentApiKey(orgId); + const data = await this.commonService + .httpGet(url, { headers: { authorization: getApiKey } }) + .then(async (response) => response); + return data; + } catch (error) { + this.logger.error(`Error in oidcGetIssuers in agent service : ${JSON.stringify(error)}`); + throw error; + } + } + + async oidcCreateCredentialOffer(credentialPayload, url: string, orgId: string): Promise { + try { + const getApiKey = await this.getOrgAgentApiKey(orgId); + const data = await this.commonService + .httpPost(url, credentialPayload, { headers: { authorization: getApiKey } }) + .then(async (response) => response); + return data; + } catch (error) { + this.logger.error(`Error in oidcCreateCredentialOffer in agent service : ${JSON.stringify(error)}`); + throw error; + } + } + + async oidcUpdateCredentialOffer(issuanceMetadata, url: string, orgId: string): Promise { + try { + const getApiKey = await this.getOrgAgentApiKey(orgId); + const data = await this.commonService + .httpPut(url, issuanceMetadata, { headers: { authorization: getApiKey } }) + .then(async (response) => response); + return data; + } catch (error) { + this.logger.error(`Error in _oidcUpdateCredentialOffer in agent service : ${JSON.stringify(error)}`); + throw error; + } + } + + async oidcGetCredentialOfferById(url: string, orgId: string): Promise { + try { + const getApiKey = await this.getOrgAgentApiKey(orgId); + const data = await this.commonService + .httpGet(url, { headers: { authorization: getApiKey } }) + .then(async (response) => response); + return data; + } catch (error) { + this.logger.error(`Error in _oidcGetCredentialOfferById in agent service : ${JSON.stringify(error)}`); + throw error; + } + } + + async oidcGetAllCredentialOffers(url: string, orgId: string): Promise { + try { + const getApiKey = await this.getOrgAgentApiKey(orgId); + const data = await this.commonService + .httpGet(url, { headers: { authorization: getApiKey } }) + .then(async (response) => response); + return data; + } catch (error) { + this.logger.error(`Error in _oidcGetAllCredentialOffers in agent service : ${JSON.stringify(error)}`); + throw error; + } + } + + async oidcDeleteCredentialOffer(url: string, orgId: string): Promise { + try { + const getApiKey = await this.getOrgAgentApiKey(orgId); + const data = await this.commonService + .httpDelete(`${url}`, { headers: { authorization: getApiKey } }) + .then(async (response) => response); + return data; + } catch (error) { + this.logger.error(`Error in _oidcDeleteCredentialOffer in agent service : ${JSON.stringify(error)}`); + throw error; + } + } + + async oidcIssuerTemplate(templatePayload, url: string, orgId: string): Promise { + try { + const getApiKey = await this.getOrgAgentApiKey(orgId); + const data = await this.commonService + .httpPut(url, templatePayload, { headers: { authorization: getApiKey } }) + .then(async (response) => response); + return data; + } catch (error) { + this.logger.error(`Error in oidcIssuerTemplate in agent service : ${JSON.stringify(error)}`); + throw error; + } + } + async getIssueCredentials(url: string, apiKey: string): Promise { try { const data = await this.commonService @@ -2056,38 +2199,142 @@ export class AgentServiceService { } } - async natsCall( - pattern: object, - payload: object - ): Promise<{ - response: string; - }> { + private async tokenEncryption(token: string): Promise { try { - return from(this.natsClient.send(this.agentServiceProxy, pattern, payload)) - .pipe(map((response) => ({ response }))) - .toPromise() - .catch((error) => { - this.logger.error(`catch: ${JSON.stringify(error)}`); - throw new HttpException( - { - status: error.statusCode, - error: error.message - }, - error.error - ); - }); + const secret = process.env.CRYPTO_PRIVATE_KEY; + if (!secret) { + this.logger.error('CRYPTO_PRIVATE_KEY is not configured'); + throw new InternalServerErrorException('Encryption key is not configured'); + } + const encryptedToken = CryptoJS.AES.encrypt(JSON.stringify(token), secret).toString(); + + return encryptedToken; } catch (error) { - this.logger.error(`[natsCall] - error in nats call : ${JSON.stringify(error)}`); throw error; } } - private async tokenEncryption(token: string): Promise { + async createX509Certificate(options: X509CreateCertificateOptions, url: string, orgId: string): Promise { try { - const encryptedToken = CryptoJS.AES.encrypt(JSON.stringify(token), process.env.CRYPTO_PRIVATE_KEY).toString(); + this.logger.log('Start creating X509 certificate'); + this.logger.debug('Creating X509 certificate with options', options); + const getApiKey = await this.getOrgAgentApiKey(orgId); + const x509Certificate = await this.commonService + .httpPost(url, options, { headers: { authorization: getApiKey } }) + .then(async (response) => response); + return x509Certificate; + } catch (error) { + this.logger.error(`Error in creating x509 certificate in agent service : ${JSON.stringify(error)}`); + throw error; + } + } - return encryptedToken; + async decodeX509Certificate(options: x509CertificateDecodeDto, url: string, orgId: string): Promise { + try { + this.logger.log('Start decoding X509 certificate'); + this.logger.debug('Decoding X509 certificate with options', options); + const getApiKey = await this.getOrgAgentApiKey(orgId); + const x509Certificate = await this.commonService + .httpPost(url, options, { headers: { authorization: getApiKey } }) + .then(async (response) => response); + return x509Certificate; + } catch (error) { + this.logger.error(`Error in decoding x509 certificate in agent service : ${JSON.stringify(error)}`); + throw error; + } + } + + async importX509Certificate(options: IX509ImportCertificateOptionsDto, url: string, orgId: string): Promise { + try { + this.logger.log('Start importing X509 certificate'); + this.logger.debug(`Importing X509 certificate with options`, options.certificate); + const getApiKey = await this.getOrgAgentApiKey(orgId); + const x509Certificate = await this.commonService + .httpPost(url, options, { headers: { authorization: getApiKey } }) + .then(async (response) => response); + return x509Certificate; } catch (error) { + this.logger.error(`Error in creating x509 certificate in agent service : ${JSON.stringify(error)}`); + throw error; + } + } + + async createOid4vpVerifier(verifierDetails: CreateVerifier, url: string, orgId: string): Promise { + this.logger.log(`[createOid4vpVerifier] Creating OID4VP verifier for orgId=${orgId || 'N/A'}`); + try { + const getApiKey = await this.getOrgAgentApiKey(orgId); + const createVerifier = await this.commonService + .httpPost(url, verifierDetails, { headers: { authorization: getApiKey } }) + .then(async (response) => response); + return createVerifier; + } catch (error) { + this.logger.error( + `[createOid4vpVerifier] Error in creating oid4vp verifier in agent service : ${JSON.stringify(error)}` + ); + throw error; + } + } + + async deleteOid4vpVerifier(url: string, orgId: string): Promise { + this.logger.log(`[deleteOid4vpVerifier] Deleting OID4VP verifier for orgId=${orgId || 'N/A'}`); + try { + const getApiKey = await this.getOrgAgentApiKey(orgId); + const deleteVerifier = await this.commonService.httpDelete(url, { headers: { authorization: getApiKey } }); + return deleteVerifier.data ?? deleteVerifier; + } catch (error) { + this.logger.error( + `[deleteOid4vpVerifier] Error in deleting oid4vp verifier in agent service : ${JSON.stringify(error)}` + ); + throw error; + } + } + + async updateOid4vpVerifier(verifierDetails: UpdateVerifier, url: string, orgId: string): Promise { + this.logger.log(`[updateOid4vpVerifier] Updating OID4VP verifier for orgId=${orgId || 'N/A'}`); + try { + const getApiKey = await this.getOrgAgentApiKey(orgId); + const updateVerifier = await this.commonService + .httpPut(url, verifierDetails, { headers: { authorization: getApiKey } }) + .then(async (response) => response); + return updateVerifier; + } catch (error) { + this.logger.error( + `[updateOid4vpVerifier] Error in updating oid4vp verifier in agent service : ${JSON.stringify(error)}` + ); + throw error; + } + } + + async getOid4vpVerifierSession(url: string, orgId: string): Promise { + this.logger.log(`[getOid4vpVerifierSession] Fetching OID4VP verifier session for orgId=${orgId || 'N/A'}`); + try { + const agentToken = await this.getOrgAgentApiKey(orgId); + const updateVerifier = await this.commonService + .httpGet(url, { headers: { authorization: agentToken } }) + .then(async (response) => response); + return updateVerifier; + } catch (error) { + this.logger.error( + `[getOid4vpVerifierSession] Error in getting oid4vp verifier session in agent service : ${JSON.stringify(error)}` + ); + throw error; + } + } + + async createOid4vpVerificationSession(sessionRequest: object, url: string, orgId: string): Promise { + this.logger.log( + `[createOid4vpVerificationSession] Creating OID4VP verification session for orgId=${orgId || 'N/A'}` + ); + try { + const getApiKey = await this.getOrgAgentApiKey(orgId); + const createSession = await this.commonService + .httpPost(url, sessionRequest, { headers: { authorization: getApiKey } }) + .then(async (response) => response); + return createSession; + } catch (error) { + this.logger.error( + `[createOid4vpVerificationSession] Error in creating oid4vp verification session in agent service : ${JSON.stringify(error)}` + ); throw error; } } diff --git a/apps/agent-service/src/interface/agent-service.interface.ts b/apps/agent-service/src/interface/agent-service.interface.ts index 28b711b93..8c4a60197 100644 --- a/apps/agent-service/src/interface/agent-service.interface.ts +++ b/apps/agent-service/src/interface/agent-service.interface.ts @@ -213,6 +213,7 @@ export interface IStoreOrgAgent { id?: string; clientSocketId?: string; agentEndPoint?: string; + agentToken?: string; apiKey?: string; seed?: string; did?: string; @@ -556,7 +557,7 @@ export interface IQuestionPayload { export interface IBasicMessage { content: string; } -interface Ledger { +export interface ILedger { id: string; createDateTime: string; lastChangedDateTime: string; @@ -570,10 +571,6 @@ interface Ledger { networkUrl: string | null; } -export interface LedgerListResponse { - response: Ledger[]; -} - export interface ICreateConnectionInvitation { label?: string; alias?: string; diff --git a/apps/agent-service/test/app.e2e-spec.ts b/apps/agent-service/test/app.e2e-spec.ts index 58f95a822..0a642e6bd 100644 --- a/apps/agent-service/test/app.e2e-spec.ts +++ b/apps/agent-service/test/app.e2e-spec.ts @@ -15,10 +15,5 @@ describe('AgentServiceController (e2e)', () => { await app.init(); }); - it('/ (GET)', () => { - return request(app.getHttpServer()) - .get('/') - .expect(200) - .expect('Hello World!'); - }); + it('/ (GET)', () => request(app.getHttpServer()).get('/').expect(200).expect('Hello World!')); }); diff --git a/apps/api-gateway/src/agent-service/dto/create-schema.dto.ts b/apps/api-gateway/src/agent-service/dto/create-schema.dto.ts index f34d802ae..76144b811 100644 --- a/apps/api-gateway/src/agent-service/dto/create-schema.dto.ts +++ b/apps/api-gateway/src/agent-service/dto/create-schema.dto.ts @@ -1,27 +1,31 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsString, IsNotEmpty, IsArray } from 'class-validator'; +import { IsString, IsNotEmpty, IsArray, ArrayNotEmpty } from 'class-validator'; export class CreateTenantSchemaDto { - @ApiProperty() - @IsString({ message: 'tenantId must be a string' }) @IsNotEmpty({ message: 'please provide valid tenantId' }) - tenantId: string; - - @ApiProperty() - @IsString({ message: 'schema version must be a string' }) @IsNotEmpty({ message: 'please provide valid schema version' }) - schemaVersion: string; + @ApiProperty() + @IsString({ message: 'tenantId must be a string' }) + @IsNotEmpty({ message: 'please provide valid tenantId' }) + tenantId: string; - @ApiProperty() - @IsString({ message: 'schema name must be a string' }) @IsNotEmpty({ message: 'please provide valid schema name' }) - schemaName: string; + @ApiProperty() + @IsString({ message: 'schema version must be a string' }) + @IsNotEmpty({ message: 'please provide valid schema version' }) + schemaVersion: string; - @ApiProperty() - @IsArray({ message: 'attributes must be an array' }) - @IsString({ each: true }) - @IsNotEmpty({ message: 'please provide valid attributes' }) - attributes: string[]; + @ApiProperty() + @IsString({ message: 'schema name must be a string' }) + @IsNotEmpty({ message: 'please provide valid schema name' }) + schemaName: string; - @ApiProperty() - - @IsNotEmpty({ message: 'please provide orgId' }) - orgId: string; -} \ No newline at end of file + @ApiProperty() + @IsArray({ message: 'attributes must be an array' }) + @ArrayNotEmpty({ message: 'please provide at least one attribute' }) + @IsString({ each: true, message: 'each attribute must be a string' }) + @IsNotEmpty({ each: true, message: 'attribute must not be empty' }) + attributes: string[]; + + @ApiProperty() + @IsString({ message: 'orgId must be a string' }) + @IsNotEmpty({ message: 'please provide orgId' }) + orgId: string; +} diff --git a/apps/api-gateway/src/app.module.ts b/apps/api-gateway/src/app.module.ts index b1af54f4d..3cf5b780a 100644 --- a/apps/api-gateway/src/app.module.ts +++ b/apps/api-gateway/src/app.module.ts @@ -6,7 +6,7 @@ import { AppService } from './app.service'; import { AuthzMiddleware } from './authz/authz.middleware'; import { AuthzModule } from './authz/authz.module'; import { ClientsModule, Transport } from '@nestjs/microservices'; -import { ConfigModule } from '@nestjs/config'; +import { ConditionalModule, ConfigModule } from '@nestjs/config'; import { CredentialDefinitionModule } from './credential-definition/credential-definition.module'; import { FidoModule } from './fido/fido.module'; import { IssuanceModule } from './issuance/issuance.module'; @@ -30,6 +30,10 @@ import { ContextModule } from '@credebl/context/contextModule'; import { LoggerModule } from '@credebl/logger/logger.module'; import { GlobalConfigModule } from '@credebl/config/global-config.module'; import { ConfigModule as PlatformConfig } from '@credebl/config/config.module'; +import { Oid4vcIssuanceModule } from './oid4vc-issuance/oid4vc-issuance.module'; +import { X509Module } from './x509/x509.module'; +import { Oid4vpModule } from './oid4vc-verification/oid4vc-verification.module'; +import { shouldLoadOidcModules } from '@credebl/common/common.utils'; @Module({ imports: [ @@ -62,7 +66,10 @@ import { ConfigModule as PlatformConfig } from '@credebl/config/config.module'; GlobalConfigModule, CacheModule.register(), GeoLocationModule, - CloudWalletModule + CloudWalletModule, + ConditionalModule.registerWhen(Oid4vcIssuanceModule, shouldLoadOidcModules), + ConditionalModule.registerWhen(Oid4vpModule, shouldLoadOidcModules), + ConditionalModule.registerWhen(X509Module, shouldLoadOidcModules) ], controllers: [AppController], providers: [ diff --git a/apps/api-gateway/src/cloud-wallet/enums/connections.enum.ts b/apps/api-gateway/src/cloud-wallet/enums/connections.enum.ts index a15929cf6..9c76afc13 100644 --- a/apps/api-gateway/src/cloud-wallet/enums/connections.enum.ts +++ b/apps/api-gateway/src/cloud-wallet/enums/connections.enum.ts @@ -1,4 +1,4 @@ export declare enum HandshakeProtocol { - Connections = "https://didcomm.org/connections/1.0", - DidExchange = "https://didcomm.org/didexchange/1.0" -} \ No newline at end of file + Connections = 'https://didcomm.org/connections/1.0', + DidExchange = 'https://didcomm.org/didexchange/1.0' +} diff --git a/apps/api-gateway/src/connection/connection.service.ts b/apps/api-gateway/src/connection/connection.service.ts index 88efdbad7..9f0f42c92 100644 --- a/apps/api-gateway/src/connection/connection.service.ts +++ b/apps/api-gateway/src/connection/connection.service.ts @@ -32,7 +32,7 @@ export class ConnectionService extends BaseService { try { return this.natsClient.sendNatsMessage(this.connectionServiceProxy, 'send-question', questionDto); } catch (error) { - throw new RpcException(error.response); + throw new RpcException(error?.response ?? error); } } @@ -44,7 +44,7 @@ export class ConnectionService extends BaseService { basicMessageDto ); } catch (error) { - throw new RpcException(error.response); + throw new RpcException(error?.response ?? error); } } @@ -60,7 +60,7 @@ export class ConnectionService extends BaseService { const connectionDetails = { referenceId }; return this.natsClient.sendNats(this.connectionServiceProxy, 'get-connection-url', connectionDetails); } catch (error) { - throw new RpcException(error.response); + throw new RpcException(error?.response ?? error); } } diff --git a/apps/api-gateway/src/connection/dtos/connection.dto.ts b/apps/api-gateway/src/connection/dtos/connection.dto.ts index da4cef77d..3720426f1 100644 --- a/apps/api-gateway/src/connection/dtos/connection.dto.ts +++ b/apps/api-gateway/src/connection/dtos/connection.dto.ts @@ -1,369 +1,376 @@ -import { ArrayNotEmpty, IsArray, IsBoolean, IsNotEmpty, IsNumber, IsOptional, IsString, IsUrl, ValidateNested } from 'class-validator'; +import { + ArrayNotEmpty, + IsArray, + IsBoolean, + IsNotEmpty, + IsNumber, + IsOptional, + IsString, + IsUrl, + ValidateNested +} from 'class-validator'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Type } from 'class-transformer'; -import { HandshakeProtocol } from '../enums/connections.enum'; import { IsNotSQLInjection } from '@credebl/common/cast.helper'; +import { HandshakeProtocol } from '@credebl/enum/enum'; export class CreateOutOfBandConnectionInvitation { - @ApiPropertyOptional() - @IsOptional() - label?: string; - - @ApiPropertyOptional() - @IsOptional() - alias?: string; - - @ApiPropertyOptional() - @IsOptional() - imageUrl?: string; - - @ApiPropertyOptional() - @IsOptional() - goalCode?: string; - - @ApiPropertyOptional() - @IsOptional() - goal?: string; - - @ApiPropertyOptional() - @IsOptional() - handshake?: boolean; - - @ApiPropertyOptional() - @IsOptional() - handshakeProtocols?: HandshakeProtocol[]; - - @ApiPropertyOptional() - @IsOptional() - messages?: object[]; - - @ApiPropertyOptional() - @IsOptional() - multiUseInvitation?: boolean; - - @ApiPropertyOptional() - @IsOptional() - IsReuseConnection?: boolean; - - @ApiPropertyOptional() - @IsOptional() - autoAcceptConnection?: boolean; - - @ApiPropertyOptional() - @IsOptional() - routing?: object; - - @ApiPropertyOptional() - @IsOptional() - appendedAttachments?: object[]; - - @ApiPropertyOptional() - @IsString() - @IsOptional() - @IsNotEmpty({ message: 'Please provide recipientKey' }) - recipientKey: string; - - @ApiPropertyOptional() - @IsString() - @IsOptional() - @IsNotEmpty({ message: 'Please provide invitation did' }) - invitationDid?: string; - - orgId; + @ApiPropertyOptional() + @IsOptional() + label?: string; + + @ApiPropertyOptional() + @IsOptional() + alias?: string; + + @ApiPropertyOptional() + @IsOptional() + imageUrl?: string; + + @ApiPropertyOptional() + @IsOptional() + goalCode?: string; + + @ApiPropertyOptional() + @IsOptional() + goal?: string; + + @ApiPropertyOptional() + @IsOptional() + handshake?: boolean; + + @ApiPropertyOptional() + @IsOptional() + handshakeProtocols?: HandshakeProtocol[]; + + @ApiPropertyOptional() + @IsOptional() + messages?: object[]; + + @ApiPropertyOptional() + @IsOptional() + multiUseInvitation?: boolean; + + @ApiPropertyOptional() + @IsOptional() + IsReuseConnection?: boolean; + + @ApiPropertyOptional() + @IsOptional() + autoAcceptConnection?: boolean; + + @ApiPropertyOptional() + @IsOptional() + routing?: object; + + @ApiPropertyOptional() + @IsOptional() + appendedAttachments?: object[]; + + @ApiPropertyOptional() + @IsString() + @IsOptional() + @IsNotEmpty({ message: 'Please provide recipientKey' }) + recipientKey: string; + + @ApiPropertyOptional() + @IsString() + @IsOptional() + @IsNotEmpty({ message: 'Please provide invitation did' }) + invitationDid?: string; + + orgId; } export class CreateConnectionDto { - @ApiPropertyOptional() - @IsOptional() - @IsString({ message: 'alias must be a string' }) - @IsNotEmpty({ message: 'please provide valid alias' }) - @IsNotSQLInjection({ message: 'alias is required.' }) - alias: string; - - @ApiPropertyOptional() - @IsOptional() - @IsString({ message: 'label must be a string' }) - @IsNotEmpty({ message: 'please provide valid label' }) - @IsNotSQLInjection({ message: 'label is required.' }) - label: string; - - @ApiPropertyOptional() - @IsOptional() - @IsNotEmpty({ message: 'please provide valid imageUrl' }) - @IsString({ message: 'imageUrl must be a string' }) - imageUrl: string; - - @ApiPropertyOptional() - @IsBoolean() - @IsOptional() - @IsNotEmpty({ message: 'please provide multiUseInvitation' }) - multiUseInvitation: boolean; - - @ApiPropertyOptional() - @IsBoolean() - @IsOptional() - @IsNotEmpty({ message: 'Please provide autoAcceptConnection' }) - autoAcceptConnection: boolean; - - @ApiPropertyOptional() - @IsString() - @IsOptional() - @IsNotEmpty({ message: 'Please provide goalCode' }) - goalCode: string; - - @ApiPropertyOptional() - @IsString() - @IsOptional() - @IsNotEmpty({ message: 'Please provide goal' }) - goal: string; - - @ApiPropertyOptional() - @IsBoolean() - @IsOptional() - @IsNotEmpty({ message: 'Please provide handshake' }) - handshake: boolean; - - @ApiPropertyOptional() - @IsArray() - @ArrayNotEmpty() - @IsOptional() - @IsString({ each: true }) - handshakeProtocols: string[]; - - orgId: string; - - @ApiPropertyOptional() - @IsString() - @IsOptional() - @IsNotEmpty({ message: 'Please provide recipientKey' }) - recipientKey: string; - - @ApiPropertyOptional() - @IsString() - @IsOptional() - @IsNotEmpty({ message: 'Please provide invitation did' }) - invitationDid?: string; + @ApiPropertyOptional() + @IsOptional() + @IsString({ message: 'alias must be a string' }) + @IsNotEmpty({ message: 'please provide valid alias' }) + @IsNotSQLInjection({ message: 'alias is required.' }) + alias: string; + + @ApiPropertyOptional() + @IsOptional() + @IsString({ message: 'label must be a string' }) + @IsNotEmpty({ message: 'please provide valid label' }) + @IsNotSQLInjection({ message: 'label is required.' }) + label: string; + + @ApiPropertyOptional() + @IsOptional() + @IsNotEmpty({ message: 'please provide valid imageUrl' }) + @IsString({ message: 'imageUrl must be a string' }) + imageUrl: string; + + @ApiPropertyOptional() + @IsBoolean() + @IsOptional() + @IsNotEmpty({ message: 'please provide multiUseInvitation' }) + multiUseInvitation: boolean; + + @ApiPropertyOptional() + @IsBoolean() + @IsOptional() + @IsNotEmpty({ message: 'Please provide autoAcceptConnection' }) + autoAcceptConnection: boolean; + + @ApiPropertyOptional() + @IsString() + @IsOptional() + @IsNotEmpty({ message: 'Please provide goalCode' }) + goalCode: string; + + @ApiPropertyOptional() + @IsString() + @IsOptional() + @IsNotEmpty({ message: 'Please provide goal' }) + goal: string; + + @ApiPropertyOptional() + @IsBoolean() + @IsOptional() + @IsNotEmpty({ message: 'Please provide handshake' }) + handshake: boolean; + + @ApiPropertyOptional() + @IsArray() + @ArrayNotEmpty() + @IsOptional() + @IsString({ each: true }) + handshakeProtocols: string[]; + + orgId: string; + + @ApiPropertyOptional() + @IsString() + @IsOptional() + @IsNotEmpty({ message: 'Please provide recipientKey' }) + recipientKey: string; + + @ApiPropertyOptional() + @IsString() + @IsOptional() + @IsNotEmpty({ message: 'Please provide invitation did' }) + invitationDid?: string; } export class ConnectionDto { - @ApiPropertyOptional() - @IsOptional() - id: string; + @ApiPropertyOptional() + @IsOptional() + id: string; - @ApiPropertyOptional() - @IsOptional() - createdAt: string; + @ApiPropertyOptional() + @IsOptional() + createdAt: string; - @ApiPropertyOptional() - @IsOptional() - did: string; + @ApiPropertyOptional() + @IsOptional() + did: string; - @ApiPropertyOptional() - @IsOptional() - theirDid: string; + @ApiPropertyOptional() + @IsOptional() + theirDid: string; - @ApiPropertyOptional() - @IsOptional() - theirLabel: string; + @ApiPropertyOptional() + @IsOptional() + theirLabel: string; - @ApiPropertyOptional() - @IsOptional() - state: string; + @ApiPropertyOptional() + @IsOptional() + state: string; - @ApiPropertyOptional() - @IsOptional() - role: string; + @ApiPropertyOptional() + @IsOptional() + role: string; - @ApiPropertyOptional() - @IsOptional() - imageUrl: string; + @ApiPropertyOptional() + @IsOptional() + imageUrl: string; - @ApiPropertyOptional() - @IsOptional() - autoAcceptConnection: boolean; + @ApiPropertyOptional() + @IsOptional() + autoAcceptConnection: boolean; - @ApiPropertyOptional() - @IsOptional() - threadId: string; + @ApiPropertyOptional() + @IsOptional() + threadId: string; - @ApiPropertyOptional() - @IsOptional() - protocol: string; + @ApiPropertyOptional() + @IsOptional() + protocol: string; - @ApiPropertyOptional() - @IsOptional() - outOfBandId: string; + @ApiPropertyOptional() + @IsOptional() + outOfBandId: string; - @ApiPropertyOptional() - @IsOptional() - updatedAt: string; + @ApiPropertyOptional() + @IsOptional() + updatedAt: string; - @ApiPropertyOptional() - @IsOptional() - contextCorrelationId: string; + @ApiPropertyOptional() + @IsOptional() + contextCorrelationId: string; - @ApiPropertyOptional() - @IsOptional() - type: string; + @ApiPropertyOptional() + @IsOptional() + type: string; - @ApiPropertyOptional() - @IsOptional() - orgId: string; + @ApiPropertyOptional() + @IsOptional() + orgId: string; - @ApiPropertyOptional() - @IsOptional() - outOfBandRecord?: object; + @ApiPropertyOptional() + @IsOptional() + outOfBandRecord?: object; - @ApiPropertyOptional() - @IsOptional() - reuseThreadId?: string; + @ApiPropertyOptional() + @IsOptional() + reuseThreadId?: string; } class ReceiveInvitationCommonDto { - @ApiPropertyOptional() - @IsOptional() - @IsString({ message: 'alias must be a string' }) - @IsNotEmpty({ message: 'please provide valid alias' }) - alias: string; - - @ApiPropertyOptional() - @IsOptional() - @IsString({ message: 'label must be a string' }) - @IsNotEmpty({ message: 'please provide valid label' }) - label: string; - - @ApiPropertyOptional() - @IsOptional() - @IsString({ message: 'imageUrl must be a string' }) - @IsNotEmpty({ message: 'please provide valid imageUrl' }) - @IsString() - imageUrl: string; - - @ApiPropertyOptional() - @IsOptional() - @IsBoolean({ message: 'autoAcceptConnection must be a boolean' }) - @IsNotEmpty({ message: 'please provide valid autoAcceptConnection' }) - autoAcceptConnection: boolean; - - @ApiPropertyOptional() - @IsOptional() - @IsBoolean({ message: 'autoAcceptInvitation must be a boolean' }) - @IsNotEmpty({ message: 'please provide valid autoAcceptInvitation' }) - autoAcceptInvitation: boolean; - - @ApiPropertyOptional() - @IsOptional() - @IsBoolean({ message: 'reuseConnection must be a boolean' }) - @IsNotEmpty({ message: 'please provide valid reuseConnection' }) - reuseConnection: boolean; - - @ApiPropertyOptional() - @IsOptional() - @IsNumber() - @IsNotEmpty({ message: 'please provide valid acceptInvitationTimeoutMs' }) - acceptInvitationTimeoutMs: number; + @ApiPropertyOptional() + @IsOptional() + @IsString({ message: 'alias must be a string' }) + @IsNotEmpty({ message: 'please provide valid alias' }) + alias: string; + + @ApiPropertyOptional() + @IsOptional() + @IsString({ message: 'label must be a string' }) + @IsNotEmpty({ message: 'please provide valid label' }) + label: string; + + @ApiPropertyOptional() + @IsOptional() + @IsString({ message: 'imageUrl must be a string' }) + @IsNotEmpty({ message: 'please provide valid imageUrl' }) + @IsString() + imageUrl: string; + + @ApiPropertyOptional() + @IsOptional() + @IsBoolean({ message: 'autoAcceptConnection must be a boolean' }) + @IsNotEmpty({ message: 'please provide valid autoAcceptConnection' }) + autoAcceptConnection: boolean; + + @ApiPropertyOptional() + @IsOptional() + @IsBoolean({ message: 'autoAcceptInvitation must be a boolean' }) + @IsNotEmpty({ message: 'please provide valid autoAcceptInvitation' }) + autoAcceptInvitation: boolean; + + @ApiPropertyOptional() + @IsOptional() + @IsBoolean({ message: 'reuseConnection must be a boolean' }) + @IsNotEmpty({ message: 'please provide valid reuseConnection' }) + reuseConnection: boolean; + + @ApiPropertyOptional() + @IsOptional() + @IsNumber() + @IsNotEmpty({ message: 'please provide valid acceptInvitationTimeoutMs' }) + acceptInvitationTimeoutMs: number; } export class ReceiveInvitationUrlDto extends ReceiveInvitationCommonDto { - - @ApiProperty() - @IsOptional() - @IsString({ message: 'invitationUrl must be a string' }) - @IsNotEmpty({ message: 'please provide valid invitationUrl' }) - invitationUrl: string; + @ApiProperty() + @IsOptional() + @IsString({ message: 'invitationUrl must be a string' }) + @IsNotEmpty({ message: 'please provide valid invitationUrl' }) + invitationUrl: string; } - class ServiceDto { - @ApiProperty() - @IsString() - @IsNotEmpty({ message: 'please provide valid id' }) - id: string; - - @ApiProperty() - @IsString() - @IsNotEmpty({ message: 'please provide valid serviceEndpoint' }) - @IsUrl({}, { message: 'Invalid serviceEndpoint format' }) - serviceEndpoint: string; - - @ApiProperty() - @IsString() - @IsNotEmpty({ message: 'please provide valid type' }) - type: string; - - @ApiProperty() - @IsString({ each: true }) - recipientKeys: string[]; - - @ApiPropertyOptional() - @IsOptional() - @IsString({ each: true }) - routingKeys: string[]; - - @ApiPropertyOptional() - @IsOptional() - @IsString({ each: true }) - accept: string[]; + @ApiProperty() + @IsString() + @IsNotEmpty({ message: 'please provide valid id' }) + id: string; + + @ApiProperty() + @IsString() + @IsNotEmpty({ message: 'please provide valid serviceEndpoint' }) + @IsUrl({}, { message: 'Invalid serviceEndpoint format' }) + serviceEndpoint: string; + + @ApiProperty() + @IsString() + @IsNotEmpty({ message: 'please provide valid type' }) + type: string; + + @ApiProperty() + @IsString({ each: true }) + recipientKeys: string[]; + + @ApiPropertyOptional() + @IsOptional() + @IsString({ each: true }) + routingKeys: string[]; + + @ApiPropertyOptional() + @IsOptional() + @IsString({ each: true }) + accept: string[]; } class InvitationDto { - @ApiPropertyOptional() - @IsOptional() - @IsString() - @IsNotEmpty({ message: 'please provide valid @id' }) - '@id': string; - - @ApiProperty() - @IsString() - @IsNotEmpty({ message: 'please provide valid @type' }) - '@type': string; - - @ApiProperty() - @IsString() - @IsNotEmpty({ message: 'please provide valid label' }) - label: string; - - @ApiPropertyOptional() - @IsOptional() - @IsString() - @IsNotEmpty({ message: 'please provide valid goalCode' }) - goalCode: string; - - @ApiPropertyOptional() - @IsOptional() - @IsString() - @IsNotEmpty({ message: 'please provide valid goal' }) - goal: string; - - @ApiPropertyOptional() - @IsOptional() - @IsString({ each: true }) - accept: string[]; - - @ApiPropertyOptional() - @IsOptional() - @IsString({ each: true }) - // eslint-disable-next-line camelcase - handshake_protocols: string[]; - - @ApiProperty() - @ValidateNested({ each: true }) - @Type(() => ServiceDto) - services: ServiceDto[]; - - @ApiPropertyOptional() - @IsString() - @IsOptional() - @IsNotEmpty({ message: 'please provide valid imageUrl' }) - @IsString() - imageUrl?: string; + @ApiPropertyOptional() + @IsOptional() + @IsString() + @IsNotEmpty({ message: 'please provide valid @id' }) + '@id': string; + + @ApiProperty() + @IsString() + @IsNotEmpty({ message: 'please provide valid @type' }) + '@type': string; + + @ApiProperty() + @IsString() + @IsNotEmpty({ message: 'please provide valid label' }) + label: string; + + @ApiPropertyOptional() + @IsOptional() + @IsString() + @IsNotEmpty({ message: 'please provide valid goalCode' }) + goalCode: string; + + @ApiPropertyOptional() + @IsOptional() + @IsString() + @IsNotEmpty({ message: 'please provide valid goal' }) + goal: string; + + @ApiPropertyOptional() + @IsOptional() + @IsString({ each: true }) + accept: string[]; + + @ApiPropertyOptional() + @IsOptional() + @IsString({ each: true }) + // eslint-disable-next-line camelcase + handshake_protocols: string[]; + + @ApiProperty() + @ValidateNested({ each: true }) + @Type(() => ServiceDto) + services: ServiceDto[]; + + @ApiPropertyOptional() + @IsString() + @IsOptional() + @IsNotEmpty({ message: 'please provide valid imageUrl' }) + @IsString() + imageUrl?: string; } export class ReceiveInvitationDto extends ReceiveInvitationCommonDto { - - @ApiProperty() - @ValidateNested() - @Type(() => InvitationDto) - invitation: InvitationDto; -} \ No newline at end of file + @ApiProperty() + @ValidateNested() + @Type(() => InvitationDto) + invitation: InvitationDto; +} diff --git a/apps/api-gateway/src/connection/enums/connections.enum.ts b/apps/api-gateway/src/connection/enums/connections.enum.ts deleted file mode 100644 index a15929cf6..000000000 --- a/apps/api-gateway/src/connection/enums/connections.enum.ts +++ /dev/null @@ -1,4 +0,0 @@ -export declare enum HandshakeProtocol { - Connections = "https://didcomm.org/connections/1.0", - DidExchange = "https://didcomm.org/didexchange/1.0" -} \ No newline at end of file diff --git a/apps/api-gateway/src/connection/interfaces/index.ts b/apps/api-gateway/src/connection/interfaces/index.ts index 20f8f584b..acba5bc15 100644 --- a/apps/api-gateway/src/connection/interfaces/index.ts +++ b/apps/api-gateway/src/connection/interfaces/index.ts @@ -26,8 +26,7 @@ export interface ISelectedOrgInterface { export interface IOrganizationInterface { name: string; description: string; - org_agents: IOrgAgentInterface[] - + org_agents: IOrgAgentInterface[]; } export interface IOrgAgentInterface { @@ -40,7 +39,6 @@ export interface IOrgAgentInterface { orgId: string; } - export class IConnectionInterface { tag: object; createdAt: string; @@ -71,8 +69,8 @@ interface IOutOfBandInvitationService { } interface IOutOfBandInvitation { - "@type": string; - "@id": string; + '@type': string; + '@id': string; label: string; accept: string[]; handshake_protocols: string[]; @@ -114,4 +112,4 @@ interface IConnectionRecord { export interface IReceiveInvitationRes { outOfBandRecord: IOutOfBandRecord; connectionRecord: IConnectionRecord; -} \ No newline at end of file +} diff --git a/apps/api-gateway/src/dtos/credential-offer.dto.ts b/apps/api-gateway/src/dtos/credential-offer.dto.ts index c59e54406..39a4e2b7d 100644 --- a/apps/api-gateway/src/dtos/credential-offer.dto.ts +++ b/apps/api-gateway/src/dtos/credential-offer.dto.ts @@ -1,40 +1,39 @@ -import { ApiProperty } from "@nestjs/swagger"; -import { IsArray, IsNotEmpty, IsString } from "class-validator"; +import { ApiProperty } from '@nestjs/swagger'; +import { IsArray, IsNotEmpty, IsString } from 'class-validator'; interface attributeValue { - name: string, - value: string, + name: string; + value: string; } export class IssueCredentialOffer { + @ApiProperty({ example: { protocolVersion: 'v1' } }) + @IsNotEmpty({ message: 'Please provide valid protocol-version' }) + @IsString({ message: 'protocol-version should be string' }) + protocolVersion: string; - @ApiProperty({ example: { 'protocolVersion': 'v1' } }) - @IsNotEmpty({ message: 'Please provide valid protocol-version' }) - @IsString({ message: 'protocol-version should be string' }) - protocolVersion: string; - - @ApiProperty({ example: { 'attributes': [{ 'value': 'string', 'name': 'string' }] } }) - @IsNotEmpty({ message: 'Please provide valid attributes' }) - @IsArray({ message: 'attributes should be array' }) - attributes: attributeValue[]; + @ApiProperty({ example: { attributes: [{ value: 'string', name: 'string' }] } }) + @IsNotEmpty({ message: 'Please provide valid attributes' }) + @IsArray({ message: 'attributes should be array' }) + attributes: attributeValue[]; - @ApiProperty({ example: { 'credentialDefinitionId': 'string' } }) - @IsNotEmpty({ message: 'Please provide valid credentialDefinitionId' }) - @IsString({ message: 'credentialDefinitionId should be string' }) - credentialDefinitionId: string; + @ApiProperty({ example: { credentialDefinitionId: 'string' } }) + @IsNotEmpty({ message: 'Please provide valid credentialDefinitionId' }) + @IsString({ message: 'credentialDefinitionId should be string' }) + credentialDefinitionId: string; - @ApiProperty({ example: { autoAcceptCredential: 'always' } }) - @IsNotEmpty({ message: 'Please provide valid autoAcceptCredential' }) - @IsString({ message: 'autoAcceptCredential should be string' }) - autoAcceptCredential: string; + @ApiProperty({ example: { autoAcceptCredential: 'always' } }) + @IsNotEmpty({ message: 'Please provide valid autoAcceptCredential' }) + @IsString({ message: 'autoAcceptCredential should be string' }) + autoAcceptCredential: string; - @ApiProperty({ example: { comment: 'string' } }) - @IsNotEmpty({ message: 'Please provide valid comment' }) - @IsString({ message: 'comment should be string' }) - comment: string; + @ApiProperty({ example: { comment: 'string' } }) + @IsNotEmpty({ message: 'Please provide valid comment' }) + @IsString({ message: 'comment should be string' }) + comment: string; - @ApiProperty({ example: { connectionId: '3fa85f64-5717-4562-b3fc-2c963f66afa6' } }) - @IsNotEmpty({ message: 'Please provide valid connection-id' }) - @IsString({ message: 'Connection-id should be string' }) - connectionId: string; + @ApiProperty({ example: { connectionId: '3fa85f64-5717-4562-b3fc-2c963f66afa6' } }) + @IsNotEmpty({ message: 'Please provide valid connection-id' }) + @IsString({ message: 'Connection-id should be string' }) + connectionId: string; } diff --git a/apps/api-gateway/src/dtos/credential-send-offer.dto.ts b/apps/api-gateway/src/dtos/credential-send-offer.dto.ts index 1467ef6af..94afa97ee 100644 --- a/apps/api-gateway/src/dtos/credential-send-offer.dto.ts +++ b/apps/api-gateway/src/dtos/credential-send-offer.dto.ts @@ -1,10 +1,9 @@ -import { ApiProperty } from "@nestjs/swagger"; -import { IsNotEmpty, IsString } from "class-validator"; +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; export class CredentialSendOffer { - - @ApiProperty({ example: 'string' }) - @IsNotEmpty({ message: 'Please provide valid credentialRecordId' }) - @IsString({ message: 'credentialRecordId should be string' }) - credentialRecordId: string; + @ApiProperty({ example: 'string' }) + @IsNotEmpty({ message: 'Please provide valid credentialRecordId' }) + @IsString({ message: 'credentialRecordId should be string' }) + credentialRecordId: string; } diff --git a/apps/api-gateway/src/dtos/issue-credential-offer.dto .ts b/apps/api-gateway/src/dtos/issue-credential-offer.dto .ts deleted file mode 100644 index 9eed1f5ed..000000000 --- a/apps/api-gateway/src/dtos/issue-credential-offer.dto .ts +++ /dev/null @@ -1,64 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsString, IsNotEmpty, IsBoolean, IsNotEmptyObject, IsObject } from 'class-validator'; -interface ICredAttrSpec { - 'mime-type': string, - name: string, - value: string -} - -interface ICredentialPreview { - '@type': string, - attributes: ICredAttrSpec[] -} - -export class IssueCredentialOfferDto { - - @ApiProperty({ example: true }) - @IsNotEmpty({message:'Please provide valid auto-issue'}) - @IsBoolean({message:'Auto-issue should be boolean'}) - auto_issue: boolean; - - @ApiProperty({ example: true }) - @IsNotEmpty({message:'Please provide valid auto-remove'}) - @IsBoolean({message:'Auto-remove should be boolean'}) - auto_remove: boolean; - - @ApiProperty({ example: 'comments' }) - @IsNotEmpty({message:'Please provide valid comment'}) - @IsString({message:'Comment should be string'}) - comment: string; - - @ApiProperty({ example: 'WgWxqztrNooG92RXvxSTWv:3:CL:20:tag' }) - @IsNotEmpty({message:'Please provide valid cred-def-id'}) - @IsString({message:'Cred-def-id should be string'}) - cred_def_id: string; - - @ApiProperty({ example: '3fa85f64-5717-4562-b3fc-2c963f66afa6' }) - @IsNotEmpty({message:'Please provide valid connection-id'}) - @IsString({message:'Connection-id should be string'}) - connection_id: string; - - @ApiProperty({ example: false }) - @IsNotEmpty({message:'Please provide valid trace'}) - @IsBoolean({message:'Trace should be boolean'}) - trace: boolean; - - - @ApiProperty({ - example: { - '@type': 'issue-credential/1.0/credential-preview', - 'attributes': [ - { - 'mime-type': 'image/jpeg', - 'name': 'favourite_drink', - 'value': 'martini' - } - ] - } - } - ) - - @IsObject({message:'Credential-preview should be object'}) - credential_preview: ICredentialPreview; -} - diff --git a/apps/api-gateway/src/helper-files/file-operation.helper.ts b/apps/api-gateway/src/helper-files/file-operation.helper.ts index e02d7315b..b5ab26542 100644 --- a/apps/api-gateway/src/helper-files/file-operation.helper.ts +++ b/apps/api-gateway/src/helper-files/file-operation.helper.ts @@ -1,36 +1,26 @@ -import { promisify } from "util"; -import * as fs from "fs"; +import { promisify } from 'util'; +import * as fs from 'fs'; +export const checkIfFileOrDirectoryExists = (path: string): boolean => fs.existsSync(path); -export const createFile = async ( - path: string, - fileName: string, - data: string - ): Promise => { - // eslint-disable-next-line @typescript-eslint/no-use-before-define - if (!checkIfFileOrDirectoryExists(path)) { - - fs.mkdirSync(path); - } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const writeFile = promisify(fs.writeFile); - return fs.writeFileSync(`${path}/${fileName}`, data, 'utf8'); - }; +const writeFile = promisify(fs.writeFile); +const mkdir = promisify(fs.mkdir); - export const checkIfFileOrDirectoryExists = (path: string): boolean => fs.existsSync(path); - - export const getFile = async ( - path: string, - encoding: BufferEncoding - ): Promise => { - const readFile = promisify(fs.readFile); - - return encoding ? readFile(path, {encoding}) : readFile(path, {}); - }; +export const createFile = async (dirPath: string, fileName: string, data: string): Promise => { + if (!checkIfFileOrDirectoryExists(dirPath)) { + await mkdir(dirPath, { recursive: true }); + } + await writeFile(`${dirPath}/${fileName}`, data, 'utf8'); +}; +export const getFile = async (path: string, encoding: BufferEncoding): Promise => { + const readFile = promisify(fs.readFile); - export const deleteFile = async (path: string): Promise => { - const unlink = promisify(fs.unlink); - - return unlink(path); - }; \ No newline at end of file + return encoding ? readFile(path, { encoding }) : readFile(path, {}); +}; + +export const deleteFile = async (path: string): Promise => { + const unlink = promisify(fs.unlink); + + return unlink(path); +}; diff --git a/apps/api-gateway/src/issuance/issuance.controller.ts b/apps/api-gateway/src/issuance/issuance.controller.ts index 86add6131..c6bb03b89 100644 --- a/apps/api-gateway/src/issuance/issuance.controller.ts +++ b/apps/api-gateway/src/issuance/issuance.controller.ts @@ -946,7 +946,6 @@ export class IssuanceController { @Res() res: Response ): Promise { issueCredentialDto.type = 'Issuance'; - if (id && 'default' === issueCredentialDto.contextCorrelationId) { issueCredentialDto.orgId = id; } diff --git a/apps/api-gateway/src/issuance/issuance.service.ts b/apps/api-gateway/src/issuance/issuance.service.ts index b09b8a4a3..5fd03a93c 100644 --- a/apps/api-gateway/src/issuance/issuance.service.ts +++ b/apps/api-gateway/src/issuance/issuance.service.ts @@ -1,4 +1,3 @@ -/* eslint-disable camelcase */ import { Injectable, Inject } from '@nestjs/common'; import { BaseService } from 'libs/service/base.service'; import { IUserRequest } from '@credebl/user-request/user-request.interface'; @@ -25,9 +24,9 @@ import { IIssuedCredential } from '@credebl/common/interfaces/issuance.interface'; import { IssueCredentialDto } from './dtos/multi-connection.dto'; -import { user } from '@prisma/client'; import { NATSClient } from '@credebl/common/NATSClient'; import { ClientProxy } from '@nestjs/microservices'; +import { user } from '@prisma/client'; @Injectable() export class IssuanceService extends BaseService { constructor( diff --git a/apps/api-gateway/src/main.ts b/apps/api-gateway/src/main.ts index e92468ce8..7c609ac4e 100644 --- a/apps/api-gateway/src/main.ts +++ b/apps/api-gateway/src/main.ts @@ -14,7 +14,6 @@ import { getNatsOptions } from '@credebl/common/nats.config'; import helmet from 'helmet'; import { CommonConstants } from '@credebl/common/common.constant'; import NestjsLoggerServiceAdapter from '@credebl/logger/nestjsLoggerServiceAdapter'; -import { NatsInterceptor } from '@credebl/common'; import { UpdatableValidationPipe } from '@credebl/common/custom-overrideable-validation-pipe'; import * as useragent from 'express-useragent'; @@ -117,8 +116,14 @@ async function bootstrap(): Promise { xssFilter: true }) ); - app.useGlobalInterceptors(new NatsInterceptor()); await app.listen(process.env.API_GATEWAY_PORT, `${process.env.API_GATEWAY_HOST}`); Logger.log(`API Gateway is listening on port ${process.env.API_GATEWAY_PORT}`); + + if ('true' === (process.env.HIDE_EXPERIMENTAL_OIDC_CONTROLLERS || 'true').trim().toLowerCase()) { + Logger.warn('Hiding experimental OIDC Controllers: OID4VC, OID4VP, x509 in OpenAPI docs'); + Logger.verbose( + "To enable the use of experimental OIDC controllers. Set, 'HIDE_EXPERIMENTAL_OIDC_CONTROLLERS' env variable to false" + ); + } } bootstrap(); diff --git a/apps/api-gateway/src/oid4vc-issuance/dtos/issuer-sessions.dto.ts b/apps/api-gateway/src/oid4vc-issuance/dtos/issuer-sessions.dto.ts new file mode 100644 index 000000000..d32d1a95f --- /dev/null +++ b/apps/api-gateway/src/oid4vc-issuance/dtos/issuer-sessions.dto.ts @@ -0,0 +1,389 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable @typescript-eslint/naming-convention, @typescript-eslint/explicit-function-return-type, @typescript-eslint/explicit-module-boundary-types, camelcase */ +import { + IsArray, + IsNotEmpty, + IsObject, + IsOptional, + IsString, + ValidateNested, + registerDecorator, + ValidationOptions, + ArrayMinSize, + IsInt, + Min, + IsIn, + IsUrl, + ValidatorConstraint, + ValidatorConstraintInterface, + ValidationArguments, + Validate, + IsDate +} from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { dateToSeconds } from '@credebl/common/date-only'; + +/* ========= disclosureFrame custom validator ========= */ +function isDisclosureFrameValue(v: unknown): boolean { + if ('boolean' === typeof v) { + return true; + } + if (v && 'object' === typeof v && !Array.isArray(v)) { + return Object.values(v as Record).every((x) => 'boolean' === typeof x); + } + return false; +} + +export function IsDisclosureFrame(options?: ValidationOptions) { + return function (object: unknown, propertyName: string) { + registerDecorator({ + name: 'IsDisclosureFrame', + target: (object as object).constructor, + propertyName, + options, + validator: { + validate(value: unknown) { + if (value === undefined) { + return true; + } // optional + if (!value || 'object' !== typeof value || Array.isArray(value)) { + return false; + } + return Object.values(value as Record).every(isDisclosureFrameValue); + }, + defaultMessage() { + return 'disclosureFrame must be a map of booleans or nested maps of booleans'; + } + } + }); + }; +} + +/* ========= Auth flow DTOs ========= */ +export class TxCodeDto { + @ApiPropertyOptional({ example: 'test abc' }) + @IsOptional() + @IsString() + description?: string; + + @ApiProperty({ example: 4 }) + @IsInt() + @Min(1) + length!: number; + + @ApiProperty({ example: 'numeric', enum: ['numeric'] }) + @IsString() + @IsIn(['numeric']) + input_mode!: 'numeric'; +} + +export class PreAuthorizedCodeFlowConfigDto { + @ApiProperty({ type: TxCodeDto }) + @ValidateNested() + @Type(() => TxCodeDto) + txCode!: TxCodeDto; + + @ApiProperty({ + example: 'http://localhost:4001/oid4vci/abc-gov', + description: 'AS (Authorization Server) base URL' + }) + @IsUrl({ require_tld: false }) + authorizationServerUrl!: string; +} + +export class AuthorizationCodeFlowConfigDto { + @ApiProperty({ + example: 'https://id.example.com/realms/issuer', + description: 'AS (Authorization Server) base URL' + }) + @IsUrl({ require_tld: false }) + authorizationServerUrl!: string; +} + +/* ========= XOR class-level validator (exactly one config) ========= */ +@ValidatorConstraint({ name: 'ExactlyOneOf', async: false }) +class ExactlyOneOfConstraint implements ValidatorConstraintInterface { + validate(_: unknown, args: ValidationArguments) { + const obj = args.object as Record; + const keys = (args.constraints ?? []) as string[]; + const present = keys.filter((k) => obj[k] !== undefined && null !== obj[k]); + return 1 === present.length; + } + defaultMessage(args: ValidationArguments) { + const keys = (args.constraints ?? []) as string[]; + return `Exactly one of [${keys.join(', ')}] must be provided (not both, not neither).`; + } +} +function ExactlyOneOf(keys: string[], options?: ValidationOptions) { + return Validate(ExactlyOneOfConstraint, keys, options); +} + +export class ValidityInfo { + @ApiProperty({ + example: '2025-04-23T14:34:09.188Z', + required: true + }) + @IsNotEmpty() + @Type(() => Date) + @IsDate() + validFrom: Date; + + @ApiProperty({ + example: '2026-05-03T14:34:09.188Z', + required: true + }) + @IsNotEmpty() + @Type(() => Date) + @IsDate() + validUntil: Date; +} + +/* ========= Request DTOs ========= */ +export class CredentialRequestDto { + @ApiProperty({ + example: 'c49bdee0-d028-4595-85dc-177c85ea391c', + description: 'Must match credential template id' + }) + @IsString() + @IsNotEmpty() + templateId!: string; + + @ApiProperty({ + description: 'Dynamic claims object', + example: { name: 'Garry', DOB: '2000-01-01', additionalProp3: 'Africa' }, + type: 'object', + additionalProperties: true + }) + @IsObject() + payload!: Record; + + @ApiPropertyOptional({ + example: { validFrom: '2025-04-23T14:34:09.188Z', validUntil: '2026-05-03T14:34:09.188Z' }, + required: false + }) + @IsOptional() + validityInfo?: ValidityInfo; +} + +export class CreateOidcCredentialOfferDto { + @ApiProperty({ + type: [CredentialRequestDto], + description: 'At least one credential to be issued.' + }) + @IsArray() + @ArrayMinSize(1) + @ValidateNested({ each: true }) + @Type(() => CredentialRequestDto) + credentials!: CredentialRequestDto[]; + + @ApiProperty({ + example: 'preAuthorizedCodeFlow', + enum: ['preAuthorizedCodeFlow', 'authorizationCodeFlow'], + description: 'Authorization type' + }) + @IsString() + @IsIn(['preAuthorizedCodeFlow', 'authorizationCodeFlow']) + authorizationType!: 'preAuthorizedCodeFlow' | 'authorizationCodeFlow'; + + issuerId?: string; +} + +export class GetAllCredentialOfferDto { + @ApiProperty({ required: false, example: 'credebl university' }) + @IsOptional() + publicIssuerId: string = ''; + + @ApiProperty({ required: false, example: '568345' }) + @IsOptional() + preAuthorizedCode: string = ''; + + @ApiProperty({ required: false, example: 'openid-credential-offer://?credential_offer_uri=http%3A%2F%2.....' }) + @IsOptional() + credentialOfferUri: string = ''; + + @ApiProperty({ required: false, example: 'Bob' }) + @IsOptional() + authorizationCode: string = ''; +} + +export class UpdateCredentialRequestDto { + @ApiPropertyOptional({ + description: 'Issuer metadata (any valid JSON object)', + type: 'object', + additionalProperties: true + }) + @IsOptional() + @IsObject() + issuerMetadata?: Record; + + issuerId?: string; + + credentialOfferId?: string; +} + +export class SignerOptionsDto { + @IsString() + @IsIn(['did', 'x5c'], { message: 'method must be either "did" or "x5c"' }) + method: string; + + @IsString() + @IsOptional() + did?: string; + + @IsArray() + @IsOptional() + x5c?: string[]; +} + +export class CredentialDto { + @ApiProperty({ + description: 'Unique ID of the supported credential', + example: 'DrivingLicenseCredential-mdoc' + }) + @IsString() + credentialSupportedId: string; + + @ApiProperty({ + description: 'Signer options for credential issuance', + example: { + method: 'x5c', + x5c: [ + 'MIIB3jCCAZCgAwIBAgIQQfBdIK9v3TIzKR+1HjlixDAFBgMrZXAwJDEUMBIGA1UEAxMLRFkgdGVzdCBvcmcxDDAKBgNVBAYTA0lORDAeFw0yNTA5MjQwMDAwMDBaFw0yODA5MjQwMDAwMDBaMCQxFDASBgNVBAMTC0RZIHRlc3Qgb3JnMQwwCgYDVQQGEwNJTkQwKjAFBgMrZXADIQDIkLycOlkHP6+MG4rprj8fyxRfwqhH8Xx9v0XxCd175aOB1zCB1DAdBgNVHQ4EFgQUbqjjbQgbAx3lPjkPBVQwvvF14agwDgYDVR0PAQH/BAQDAgGGMBUGA1UdJQEB/wQLMAkGByiBjF0FAQIwOwYDVR0SBDQwMoIXaHR0cDovL3Rlc3QuZXhhbXBsZS5jb22GF2h0dHA6Ly90ZXN0LmV4YW1wbGUuY29tMDsGA1UdEQQ0MDKCF2h0dHA6Ly90ZXN0LmV4YW1wbGUuY29thhdodHRwOi8vdGVzdC5leGFtcGxlLmNvbTASBgNVHRMBAf8ECDAGAQH/AgEAMAUGAytlcANBALTqC64XSTRUoMmwYbCD/z46U/Je6IeQsh6qq4qXh+wfnMIfJMvLQnG+nMkfeAs3zYAwjK6sCZ/7lHkEJnYObQ4=' + ] + } + }) + @ValidateNested() + @Type(() => SignerOptionsDto) + signerOptions: SignerOptionsDto; + + @ApiProperty({ + description: 'Credential format type', + enum: ['mso_mdoc', 'vc+sd-jwt'], + example: 'mso_mdoc' + }) + @IsString() + @IsIn(['mso_mdoc', 'vc+sd-jwt'], { message: 'format must be either "mso_mdoc" or "vc+sd-jwt"' }) + format: string; + + @ApiProperty({ + description: 'Credential payload (namespace data, validity info, etc.)', + example: [ + { + namespaces: { + 'org.iso.23220.photoID.1': { + birth_date: '1970-02-14', + family_name: 'Müller-Lüdenscheid', + given_name: 'Ford Praxibetel', + document_number: 'LA001801M' + } + }, + validityInfo: { + validFrom: '2025-04-23T14:34:09.188Z', + validUntil: '2026-05-03T14:34:09.188Z' + } + }, + { + full_name: 'Garry', + address: { + street_address: 'M.G. Road', + locality: 'Pune', + country: 'India' + }, + iat: 1698151532, + nbf: dateToSeconds(new Date()), + exp: dateToSeconds(new Date(Date.now() + 5 * 365 * 24 * 60 * 60 * 1000)) + } + ] + }) + @ValidateNested() + payload: object; +} + +export class CreateCredentialOfferD2ADto { + @ApiProperty({ + description: 'Public identifier of the issuer visible to verifiers and wallets.', + example: 'dy-gov' + }) + @IsString() + publicIssuerId: string; + + @ApiProperty({ + description: 'List of credentials to be issued under this offer.', + type: [CredentialDto], + example: [ + { + credentialSupportedId: 'DrivingLicenseCredential-mdoc', + signerOptions: { + method: 'x5c', + x5c: [ + 'MIIB3jCCAZCgAwIBAgIQQfBdIK9v3TIzKR+1HjlixDAFBgMrZXAwJDEUMBIGA1UEAxMLRFkgdGVzdCBvcmcxDDAKBgNVBAYTA0lORDAeFw0yNTA5MjQwMDAwMDBaFw0yODA5MjQwMDAwMDBaMCQxFDASBgNVBAMTC0RZIHRlc3Qgb3JnMQwwCgYDVQQGEwNJTkQwKjAFBgMrZXADIQDIkLycOlkHP6+MG4rprj8fyxRfwqhH8Xx9v0XxCd175aOB1zCB1DAdBgNVHQ4EFgQUbqjjbQgbAx3lPjkPBVQwvvF14agwDgYDVR0PAQH/BAQDAgGGMBUGA1UdJQEB/wQLMAkGByiBjF0FAQIwOwYDVR0SBDQwMoIXaHR0cDovL3Rlc3QuZXhhbXBsZS5jb22GF2h0dHA6Ly90ZXN0LmV4YW1wbGUuY29tMDsGA1UdEQQ0MDKCF2h0dHA6Ly90ZXN0LmV4YW1wbGUuY29thhdodHRwOi8vdGVzdC5leGFtcGxlLmNvbTASBgNVHRMBAf8ECDAGAQH/AgEAMAUGAytlcANBALTqC64XSTRUoMmwYbCD/z46U/Je6IeQsh6qq4qXh+wfnMIfJMvLQnG+nMkfeAs3zYAwjK6sCZ/7lHkEJnYObQ4=' + ] + }, + format: 'mso_mdoc', + payload: { + namespaces: { + 'org.iso.23220.photoID.1': { + birth_date: '1970-02-14', + family_name: 'Müller-Lüdenscheid', + given_name: 'Ford Praxibetel', + document_number: 'LA001801M' + } + }, + validityInfo: { + validFrom: '2025-04-23T14:34:09.188Z', + validUntil: '2026-05-03T14:34:09.188Z' + } + } + } + ] + }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => CredentialDto) + credentials: CredentialDto[]; + + @ApiPropertyOptional({ + description: 'Pre-Authorized Code Flow configuration. Provide this OR authorizationCodeFlowConfig (XOR rule).', + type: PreAuthorizedCodeFlowConfigDto, + example: { + preAuthorizedCode: 'abcd1234xyz', + txCode: { + length: 8, + inputMode: 'numeric' + }, + userPinRequired: true + } + }) + @IsOptional() + @ValidateNested() + @Type(() => PreAuthorizedCodeFlowConfigDto) + preAuthorizedCodeFlowConfig?: PreAuthorizedCodeFlowConfigDto; + + @ApiPropertyOptional({ + description: 'Authorization Code Flow configuration. Provide this OR preAuthorizedCodeFlowConfig (XOR rule).', + type: AuthorizationCodeFlowConfigDto, + example: { + clientId: 'wallet-app', + redirectUri: 'https://wallet.example.org/callback', + scope: 'openid vc_authn', + state: 'xyz-987654321' + } + }) + @IsOptional() + @ValidateNested() + @Type(() => AuthorizationCodeFlowConfigDto) + authorizationCodeFlowConfig?: AuthorizationCodeFlowConfigDto; + + @ApiPropertyOptional({ + description: 'Internal identifier of the issuer (optional, for backend use).', + example: 'issuer-12345' + }) + @IsOptional() + issuerId?: string; + + @ExactlyOneOf(['preAuthorizedCodeFlowConfig', 'authorizationCodeFlowConfig'], { + message: 'Provide exactly one of preAuthorizedCodeFlowConfig or authorizationCodeFlowConfig.' + }) + private readonly _exactlyOne?: unknown; +} diff --git a/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-credential-wh.dto.ts b/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-credential-wh.dto.ts new file mode 100644 index 000000000..7a450c021 --- /dev/null +++ b/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-credential-wh.dto.ts @@ -0,0 +1,73 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsArray, IsObject, IsOptional, IsString } from 'class-validator'; + +export class CredentialOfferPayloadDto { + @ApiProperty({ type: [String] }) + @IsArray() + // eslint-disable-next-line camelcase + credential_configuration_ids!: string[]; +} + +export class IssuanceMetadataDto { + @ApiProperty() + @IsString() + issuerDid!: string; + + @ApiProperty({ type: [Object] }) + @IsArray() + credentials!: Record[]; +} + +export class OidcIssueCredentialDto { + @ApiProperty() + @IsString() + id!: string; + + @ApiProperty() + @IsString() + credentialOfferId!: string; + + @ApiProperty({ type: [Object] }) + @IsArray() + issuedCredentials!: Record[]; + + @ApiProperty({ type: CredentialOfferPayloadDto }) + @IsObject() + credentialOfferPayload!: CredentialOfferPayloadDto; + + @ApiProperty() + @IsString() + state!: string; + + @ApiProperty() + @IsString() + createdAt!: string; + + @ApiProperty() + @IsString() + updatedAt!: string; + + @ApiProperty() + @IsString() + contextCorrelationId!: string; + + @ApiProperty() + @IsString() + issuerId!: string; + + @ApiPropertyOptional() + @IsOptional() + type: string; + + @ApiPropertyOptional() + @IsOptional() + orgId: string; +} + +/** + * Utility: return only credential_configuration_ids from a webhook payload + */ +export function extractCredentialConfigurationIds(payload: Partial): string[] { + const cfg = payload?.credentialOfferPayload?.credential_configuration_ids; + return Array.isArray(cfg) ? cfg : []; +} diff --git a/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer-template.dto.ts b/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer-template.dto.ts new file mode 100644 index 000000000..ad8d175ba --- /dev/null +++ b/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer-template.dto.ts @@ -0,0 +1,288 @@ +/* eslint-disable camelcase */ +import { + IsString, + IsBoolean, + IsOptional, + IsEnum, + ValidateNested, + IsObject, + IsNotEmpty, + IsArray, + ValidateIf, + IsEmpty, + ArrayNotEmpty, + IsDefined, + NotEquals +} from 'class-validator'; +import { ApiExtraModels, ApiProperty, ApiPropertyOptional, getSchemaPath, PartialType } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { SignerOption } from '@prisma/client'; +import { AttributeType, CredentialFormat } from '@credebl/enum/enum'; + +class CredentialAttributeDisplayDto { + @ApiPropertyOptional({ example: 'First Name' }) + @IsString() + @IsNotEmpty() + name: string; + + @ApiPropertyOptional({ example: 'en' }) + @IsString() + @IsOptional() + locale?: string; +} +export class CredentialAttributeDto { + @ApiProperty({ description: 'Unique key for this attribute (e.g., full_name, org.iso.23220.photoID.1.birth_date)' }) + @IsString() + key: string; + + @ApiProperty({ required: false, description: 'Whether the attribute is mandatory' }) + @IsOptional() + @IsBoolean() + mandatory?: boolean; + + // TODO: Check how do we handle claims with only path rpoperty like email, etc. + @ApiProperty({ enum: AttributeType, description: 'Type of the attribute value (string, number, date, etc.)' }) + @IsEnum(AttributeType) + // TODO: changes value_type: AttributeType; + value_type: AttributeType; + + @ApiProperty({ description: 'Whether this attribute should be disclosed (for SD-JWT)' }) + @IsOptional() + @IsBoolean() + disclose?: boolean; + + @ApiProperty({ type: [CredentialAttributeDisplayDto], required: false, description: 'Localized display values' }) + @IsOptional() + @ValidateNested({ each: true }) + @Type(() => CredentialAttributeDisplayDto) + display?: CredentialAttributeDisplayDto[]; + + @ApiProperty({ + description: 'Nested attributes if type is object or array', + required: false, + type: () => [CredentialAttributeDto] + }) + @IsOptional() + @IsArray() + @ValidateNested({ each: true }) + @Type(() => CredentialAttributeDto) + children?: CredentialAttributeDto[]; +} +class LogoDto { + @ApiPropertyOptional({ + example: 'https://upload.wikimedia.org/wikipedia/commons/2/2f/ABC-2021-LOGO.svg' + }) + @IsString() + @IsNotEmpty() + uri: string; + + @ApiPropertyOptional({ example: 'abc_logo' }) + @IsString() + @IsOptional() + alt_text?: string; +} + +class CredentialDisplayDto { + @ApiPropertyOptional({ example: 'Birth Certificate' }) + @IsString() + @IsNotEmpty() + name: string; + + @ApiPropertyOptional({ example: 'Official record of birth' }) + @IsString() + @IsOptional() + description?: string; + + @ApiPropertyOptional({ example: 'en' }) + @IsString() + @IsOptional() + locale?: string; + + @ApiPropertyOptional({ + example: { + uri: 'https://upload.wikimedia.org/wikipedia/commons/2/2f/ABC-2021-LOGO.svg', + alt_text: 'abc_logo' + } + }) + @IsOptional() + @IsObject() + @ValidateNested() + @Type(() => LogoDto) + logo?: LogoDto; + + @ApiPropertyOptional({ example: '#12107c' }) + @IsString() + @IsOptional() + background_color?: string; + + @ApiPropertyOptional({ example: '#FFFFFF' }) + @IsString() + @IsOptional() + text_color?: string; + + @ApiPropertyOptional({ example: { uri: 'https://upload.wikimedia.org/wikipedia/commons/2/2f/ABC-2021-LOGO.svg' } }) + @IsObject() + @IsOptional() + background_image?: { + uri: string; + }; +} + +export class AppearanceDto { + @ApiPropertyOptional({ + example: [ + { + locale: 'de', + name: 'Geburtsurkunde', + description: 'Offizielle Geburtsbescheinigung', + logo: { + uri: 'https://upload.wikimedia.org/wikipedia/commons/2/2f/ABC-2021-LOGO.svg', + alt_text: 'abc_logo' + } + }, + { + locale: 'en', + name: 'Birth Certificate', + description: 'Official record of birth', + logo: { + uri: 'https://upload.wikimedia.org/wikipedia/commons/2/2f/ABC-2021-LOGO.svg', + alt_text: 'abc_logo' + } + } + ] + }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => CredentialDisplayDto) + display: CredentialDisplayDto[]; +} + +export class MdocNamespaceDto { + @ApiProperty({ description: 'Namespace key (e.g., org.iso.23220.photoID.1)' }) + @IsString() + namespace: string; + + @ApiProperty({ type: () => [CredentialAttributeDto] }) + @IsArray() + @ArrayNotEmpty() + @ValidateNested({ each: true }) + @Type(() => CredentialAttributeDto) + attributes: CredentialAttributeDto[]; +} +export class MdocTemplateDto { + @ApiProperty({ + description: 'Document type (required when format is "mso_mdoc"; must NOT be provided when format is "vc+sd-jwt")', + example: 'org.iso.23220.photoID.1' + }) + //@ValidateIf((o: CreateCredentialTemplateDto) => 'mso_mdoc' === o.format) + @IsString() + doctype: string; + + @ApiProperty({ type: () => [MdocNamespaceDto] }) + @IsArray() + @ArrayNotEmpty() + @ValidateNested({ each: true }) + @Type(() => MdocNamespaceDto) + namespaces: MdocNamespaceDto[]; +} + +export class SdJwtTemplateDto { + @ApiProperty({ + description: + 'Verifiable Credential Type (required when format is "vc+sd-jwt"; must NOT be provided when format is "mso_mdoc")', + example: 'BirthCertificateCredential-sdjwt' + }) + // @ValidateIf((o: CreateCredentialTemplateDto) => 'vc+sd-jwt' === o.format) + @IsString() + vct: string; + + @ApiProperty({ + type: 'array', + items: { $ref: getSchemaPath(CredentialAttributeDto) }, + description: 'Attributes included in the credential template' + }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => CredentialAttributeDto) + attributes: CredentialAttributeDto[]; +} + +@ApiExtraModels(CredentialAttributeDto, SdJwtTemplateDto, MdocTemplateDto) +export class CreateCredentialTemplateDto { + @ApiProperty({ description: 'Template name' }) + @IsString() + name: string; + + @ApiProperty({ required: false, description: 'Optional description for the template' }) + @IsOptional() + @IsString() + description?: string; + + @ApiProperty({ enum: SignerOption, description: 'Signer option type' }) + @IsEnum(SignerOption) + @ValidateIf((o) => o.format === CredentialFormat.Mdoc) + @IsDefined({ message: 'signerOption is required when format is Mdoc' }) + @NotEquals(SignerOption.DID, { message: 'signerOption must NOT be DID when format is Mdoc' }) + signerOption!: SignerOption; + + @ApiProperty({ enum: CredentialFormat, description: 'Credential format type' }) + @IsEnum(CredentialFormat) + format: CredentialFormat; + + @ValidateIf((o: CreateCredentialTemplateDto) => CredentialFormat.SdJwtVc === o.format) + @IsEmpty({ message: 'doctype must not be provided when format is "vc+sd-jwt"' }) + readonly _doctypeAbsentGuard?: unknown; + + @ValidateIf((o: CreateCredentialTemplateDto) => CredentialFormat.Mdoc === o.format) + @IsEmpty({ message: 'vct must not be provided when format is "mso_mdoc"' }) + readonly _vctAbsentGuard?: unknown; + + @ApiProperty({ + type: Object, + oneOf: [{ $ref: getSchemaPath(SdJwtTemplateDto) }, { $ref: getSchemaPath(MdocTemplateDto) }], + description: 'Credential template definition (depends on credentialFormat)' + }) + @ValidateNested() + @Type(({ object }) => { + if (object.format === CredentialFormat.Mdoc) { + return MdocTemplateDto; + } else if (object.format === CredentialFormat.SdJwtVc) { + return SdJwtTemplateDto; + } + }) + template: SdJwtTemplateDto | MdocTemplateDto; + + @ApiProperty({ default: false, description: 'Indicates whether credentials can be revoked' }) + @IsBoolean() + canBeRevoked = false; + + @ApiProperty({ + type: Object, + required: false, + description: 'Appearance configuration for credentials (branding, colors, etc.)' + }) + @ApiPropertyOptional({ + type: AppearanceDto, + example: { + display: [ + { + locale: 'en', + name: 'Birth Certificate', + description: 'Official record of birth', + logo: { + uri: 'https://upload.wikimedia.org/wikipedia/commons/2/2f/ABC-2021-LOGO.svg', + alt_text: 'abc_logo' + } + } + ] + } + }) + @IsOptional() + @ValidateNested() + @Type(() => AppearanceDto) + appearance?: AppearanceDto; + + issuerId: string; +} + +export class UpdateCredentialTemplateDto extends PartialType(CreateCredentialTemplateDto) {} diff --git a/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer.dto.ts b/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer.dto.ts new file mode 100644 index 000000000..2675d7514 --- /dev/null +++ b/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer.dto.ts @@ -0,0 +1,180 @@ +/* eslint-disable camelcase */ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsString, IsOptional, IsArray, ValidateNested, IsUrl, IsInt } from 'class-validator'; +import { Type } from 'class-transformer'; + +export class LogoDto { + @ApiProperty({ + description: 'URI pointing to the logo image', + example: 'https://upload.wikimedia.org/wikipedia/commons/2/2f/ABC-2021-LOGO.svg' + }) + @IsUrl() + uri: string; + + @ApiProperty({ + description: 'Alternative text for the logo (for accessibility)', + example: 'ABC Company Logo' + }) + @IsString() + alt_text: string; +} + +export class IssuerDisplayDto { + @ApiProperty({ + description: 'The locale for display text', + example: 'en-US' + }) + @IsString() + locale: string; + + @ApiProperty({ + description: 'The display name for the credential/claim', + example: 'Student ID Card' + }) + @IsString({ message: 'Error from DisplayDto -> name' }) + name: string; + + @ApiPropertyOptional({ + description: 'Logo display information for the issuer', + type: LogoDto + }) + @IsOptional() + @Type(() => LogoDto) + logo?: LogoDto; +} + +export class ClientAuthenticationDto { + @ApiProperty({ + description: 'OAuth2 client ID for the authorization server', + example: 'issuer-server' + }) + @IsString() + clientId: string; + + @ApiProperty({ + description: 'OAuth2 client secret for the authorization server', + example: '2qKMWulZpMBzXIdfPO5AEs0xaTaKs1uk' + }) + @IsString() + clientSecret: string; +} + +export class AuthorizationServerConfigDto { + @ApiProperty({ + description: 'Authorization server issuer URL', + example: 'https://example-oid4vc-provider.com' + }) + @IsString() + issuer: string; + + @ApiProperty({ + description: 'Client authentication configuration', + type: () => ClientAuthenticationDto + }) + @ValidateNested() + @Type(() => ClientAuthenticationDto) + clientAuthentication: ClientAuthenticationDto; +} + +export enum AccessTokenSignerKeyType { + ED25519 = 'ed25519' +} + +// @ApiExtraModels(CredentialConfigurationDto) +export class IssuerCreationDto { + @ApiProperty({ + description: 'Unique identifier of the issuer (usually a short code or DID-based identifier)', + example: 'credebl-university' + }) + @IsString({ message: 'issuerId must be a string' }) + issuerId: string; + + @ApiPropertyOptional({ + description: 'Maximum number of credentials that can be issued in a single batch issuance operation', + example: 100, + type: Number + }) + @IsOptional() + @IsInt({ message: 'batchCredentialIssuanceSize must be an integer' }) + batchCredentialIssuanceSize?: number; + + @ApiProperty({ + description: + 'Localized display information for the issuer — shown in wallet apps or credential metadata display (multi-lingual supported)', + type: [IssuerDisplayDto], + example: [ + { + locale: 'en', + name: 'Credebl University', + description: 'Accredited institution issuing verified student credentials', + logo: { + uri: 'https://university.example.io/assets/logo-en.svg', + alt_text: 'Credebl University logo' + } + }, + { + locale: 'de', + name: 'Credebl Universität', + description: 'Akkreditierte Institution für digitale Studentenausweise', + logo: { + uri: 'https://university.example.io/assets/logo-de.svg', + alt_text: 'Credebl Universität Logo' + } + } + ] + }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => IssuerDisplayDto) + display: IssuerDisplayDto[]; + + @ApiProperty({ + example: 'https://issuer.example.io', + description: 'Base URL of the Authorization Server supporting OID4VC issuance flows' + }) + @IsUrl({ require_tld: false }, { message: 'authorizationServerUrl must be a valid URL' }) + authorizationServerUrl: string; + + @ApiProperty({ + description: + 'Additional configuration details for the authorization server (token endpoint, credential endpoint, grant types, etc.)', + type: AuthorizationServerConfigDto, + example: { + issuer: 'https://example.com/realms/abc', + clientAuthentication: { + clientId: 'issuer-server', + clientSecret: 'issuer-client-secret' + } + } + }) + @IsOptional() + @ValidateNested() + @Type(() => AuthorizationServerConfigDto) + authorizationServerConfigs?: AuthorizationServerConfigDto; +} + +export class IssuerUpdationDto { + issuerId?: string; + + @ApiProperty({ + description: 'Localized display information for the credential', + type: [IssuerDisplayDto] + }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => IssuerDisplayDto) + display: IssuerDisplayDto[]; + + @ApiProperty({ + description: 'batchCredentialIssuanceSize', + example: '50' + }) + @ApiPropertyOptional({ + description: 'Maximum number of credentials that can be issued in a batch', + example: 50, + type: Number + }) + @IsOptional() + @IsInt({ message: 'batchCredentialIssuanceSize must be an integer' }) + batchCredentialIssuanceSize?: number; +} diff --git a/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vp-presentation-wh.dto.ts b/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vp-presentation-wh.dto.ts new file mode 100644 index 000000000..055f31427 --- /dev/null +++ b/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vp-presentation-wh.dto.ts @@ -0,0 +1,39 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsOptional, IsString } from 'class-validator'; +export class Oid4vpPresentationWhDto { + @ApiProperty() + @IsString() + id!: string; + + @ApiProperty() + @IsString() + state!: string; + + @ApiProperty() + @IsString() + authorizationRequestId!: string; + + @ApiProperty() + @IsString() + createdAt!: string; + + @ApiProperty() + @IsString() + updatedAt!: string; + + @ApiProperty() + @IsString() + contextCorrelationId!: string; + + @ApiProperty() + @IsString() + verifierId!: string; + + @ApiPropertyOptional() + @IsOptional() + type: string; + + @ApiPropertyOptional() + @IsOptional() + orgId: string; +} diff --git a/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.controller.ts b/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.controller.ts new file mode 100644 index 000000000..797909344 --- /dev/null +++ b/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.controller.ts @@ -0,0 +1,667 @@ +/* eslint-disable default-param-last */ +/* eslint-disable no-param-reassign */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable no-console */ +/* eslint-disable camelcase */ +import { + Controller, + Post, + Body, + UseGuards, + HttpStatus, + Res, + Get, + Param, + UseFilters, + BadRequestException, + ParseUUIDPipe, + Delete, + Patch, + Query, + Put, + Logger +} from '@nestjs/common'; +import { + ApiTags, + ApiOperation, + ApiResponse, + ApiBearerAuth, + ApiForbiddenResponse, + ApiUnauthorizedResponse, + ApiExcludeEndpoint +} from '@nestjs/swagger'; +import { AuthGuard } from '@nestjs/passport'; +import { ApiResponseDto } from '../dtos/apiResponse.dto'; +import { UnauthorizedErrorDto } from '../dtos/unauthorized-error.dto'; +import { ForbiddenErrorDto } from '../dtos/forbidden-error.dto'; +import { Response } from 'express'; +import IResponseType, { IResponse } from '@credebl/common/interfaces/response.interface'; +import { User } from '../authz/decorators/user.decorator'; +import { ResponseMessages } from '@credebl/common/response-messages'; +import { Roles } from '../authz/decorators/roles.decorator'; +import { OrgRoles } from 'libs/org-roles/enums'; +import { OrgRolesGuard } from '../authz/guards/org-roles.guard'; +import { CustomExceptionFilter } from 'apps/api-gateway/common/exception-handler'; +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { user } from '@prisma/client'; +import { IssuerCreationDto, IssuerUpdationDto } from './dtos/oid4vc-issuer.dto'; +import { CreateCredentialTemplateDto, UpdateCredentialTemplateDto } from './dtos/oid4vc-issuer-template.dto'; +import { OidcIssueCredentialDto } from './dtos/oid4vc-credential-wh.dto'; +import { Oid4vcIssuanceService } from './oid4vc-issuance.service'; +import { + CreateCredentialOfferD2ADto, + CreateOidcCredentialOfferDto, + GetAllCredentialOfferDto, + UpdateCredentialRequestDto +} from './dtos/issuer-sessions.dto'; + +@Controller() +@UseFilters(CustomExceptionFilter) +@ApiTags('OID4VC') +@ApiUnauthorizedResponse({ description: 'Unauthorized', type: UnauthorizedErrorDto }) +@ApiForbiddenResponse({ description: 'Forbidden', type: ForbiddenErrorDto }) +export class Oid4vcIssuanceController { + private readonly logger = new Logger('Oid4vcIssuanceController'); + constructor(private readonly oid4vcIssuanceService: Oid4vcIssuanceService) {} + /** + * Create issuer against a org(tenant) + * @param orgId The ID of the organization + * @param user The user making the request + * @param res The response object + * @returns The status of the deletion operation + */ + + @Post('/orgs/:orgId/oid4vc/issuers') + @ApiOperation({ + summary: 'Create OID4VC issuer', + description: 'Creates a new OID4VC issuer for the specified organization.' + }) + @ApiResponse({ status: HttpStatus.OK, description: 'Issuer created successfully.', type: ApiResponseDto }) + @ApiBearerAuth() + @Roles(OrgRoles.OWNER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + async oidcIssuerCreate( + @Param( + 'orgId', + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + } + }) + ) + orgId: string, + @User() user: user, + @Body() issueCredentialDto: IssuerCreationDto, + @Res() res: Response + ): Promise { + const createIssuer = await this.oid4vcIssuanceService.oidcIssuerCreate(issueCredentialDto, orgId, user); + const finalResponse: IResponse = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.oidcIssuer.success.issuerConfig, + data: createIssuer + }; + return res.status(HttpStatus.CREATED).json(finalResponse); + } + + @Post('/orgs/:orgId/oid4vc/issuers/:issuerId') + @ApiOperation({ + summary: 'Update OID4VC issuer', + description: 'Updates an existing OID4VC issuer for the specified organization.' + }) + @ApiResponse({ status: HttpStatus.OK, description: 'Issuer updated successfully.', type: ApiResponseDto }) + @ApiBearerAuth() + @Roles(OrgRoles.OWNER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + async oidcIssuerUpdate( + @Param( + 'orgId', + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + } + }) + ) + orgId: string, + @User() user: user, + @Param('issuerId') + issuerId: string, + @Body() issueCredentialDto: IssuerUpdationDto, + @Res() res: Response + ): Promise { + issueCredentialDto.issuerId = issuerId; + const createIssuer = await this.oid4vcIssuanceService.oidcIssuerUpdate(issueCredentialDto, orgId, user); + const finalResponse: IResponse = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.oidcIssuer.success.issuerConfigUpdate, + data: createIssuer + }; + return res.status(HttpStatus.CREATED).json(finalResponse); + } + + @Get('/orgs/:orgId/oid4vc/issuers/:id') + @ApiOperation({ summary: 'Get OID4VC issuer', description: 'Retrieves an OID4VC issuer by id.' }) + @ApiResponse({ status: HttpStatus.OK, description: 'Issuer fetched successfully.', type: ApiResponseDto }) + @ApiBearerAuth() + @Roles(OrgRoles.OWNER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + async oidcGetIssuerById( + @Param( + 'orgId', + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + } + }) + ) + orgId: string, + @Param('id') + id: string, + @Res() res: Response + ): Promise { + const oidcIssuer = await this.oid4vcIssuanceService.oidcGetIssuerById(id, orgId); + const finalResponse: IResponse = { + statusCode: HttpStatus.OK, + message: ResponseMessages.oidcIssuer.success.fetch, + data: oidcIssuer + }; + return res.status(HttpStatus.OK).json(finalResponse); + } + + @Get('/orgs/:orgId/oid4vc/issuers') + @ApiOperation({ + summary: 'Get OID4VC issuers', + description: 'Retrieves all OID4VC issuers for the specified organization.' + }) + @ApiResponse({ status: HttpStatus.OK, description: 'Issuers fetched successfully.', type: ApiResponseDto }) + @ApiBearerAuth() + @Roles(OrgRoles.OWNER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + async oidcGetIssuers( + @Param( + 'orgId', + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + } + }) + ) + orgId: string, + @Res() res: Response + ): Promise { + const oidcIssuer = await this.oid4vcIssuanceService.oidcGetIssuers(orgId); + const finalResponse: IResponse = { + statusCode: HttpStatus.OK, + message: ResponseMessages.oidcIssuer.success.fetch, + data: oidcIssuer + }; + return res.status(HttpStatus.OK).json(finalResponse); + } + + @Delete('/orgs/:orgId/oid4vc/issuers/:id') + @ApiOperation({ + summary: 'Delete OID4VC issuer', + description: 'Deletes an OID4VC issuer for the specified organization.' + }) + @ApiBearerAuth() + @Roles(OrgRoles.OWNER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + async deleteOidcIssuer( + @Param( + 'orgId', + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + } + }) + ) + orgId: string, + @Param( + 'id', + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + } + }) + ) + id: string, + @User() user: user, + @Res() res: Response + ): Promise { + await this.oid4vcIssuanceService.oidcDeleteIssuer(user, orgId, id); + + const finalResponse: IResponse = { + statusCode: HttpStatus.OK, + message: ResponseMessages.oidcIssuer.success.delete + }; + + return res.status(HttpStatus.OK).json(finalResponse); + } + + @Post('/orgs/:orgId/oid4vc/:issuerId/template') + @ApiOperation({ + summary: 'Create credential template', + description: 'Creates a new credential template for the specified issuer.' + }) + @ApiResponse({ status: HttpStatus.CREATED, description: 'Template created successfully.' }) + @ApiBearerAuth() + @Roles(OrgRoles.OWNER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + async createTemplate( + @Param('orgId') + orgId: string, + @Param('issuerId') + issuerId: string, + @User() user: user, + @Body() CredentialTemplate: CreateCredentialTemplateDto, + @Res() res: Response + ): Promise { + CredentialTemplate.issuerId = issuerId; + const template = await this.oid4vcIssuanceService.createTemplate(CredentialTemplate, user, orgId, issuerId); + + const finalResponse: IResponse = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.oidcTemplate.success.create, + data: template + }; + + return res.status(HttpStatus.CREATED).json(finalResponse); + } + + @Get('/orgs/:orgId/oid4vc/:issuerId/template') + @ApiOperation({ + summary: 'List credential templates', + description: 'Lists all credential templates for the specified issuer.' + }) + @ApiResponse({ status: HttpStatus.OK, description: 'List of templates.' }) + @ApiBearerAuth() + @Roles(OrgRoles.OWNER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + async listTemplates( + @Param( + 'orgId', + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + } + }) + ) + orgId: string, + @Param( + 'issuerId', + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.oidcTemplate.error.invalidId); + } + }) + ) + issuerId: string, + @Res() res: Response + ): Promise { + const templates = await this.oid4vcIssuanceService.findAllTemplate(orgId, issuerId); + + const finalResponse: IResponse = { + statusCode: HttpStatus.OK, + message: ResponseMessages.oidcTemplate.success.fetch, + data: templates + }; + + return res.status(HttpStatus.OK).json(finalResponse); + } + + @Get('/orgs/:orgId/oid4vc/:issuerId/template/:templateId') + @ApiOperation({ summary: 'Get credential template by ID', description: 'Retrieves a credential template by its ID.' }) + @ApiBearerAuth() + @Roles(OrgRoles.OWNER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + async getTemplateById( + @Param( + 'orgId', + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + } + }) + ) + orgId: string, + @Param('templateId') + templateId: string, + @Res() res: Response + ): Promise { + const template = await this.oid4vcIssuanceService.findByIdTemplate(orgId, templateId); + + const finalResponse: IResponse = { + statusCode: HttpStatus.OK, + message: ResponseMessages.oidcTemplate.success.getById, + data: template + }; + + return res.status(HttpStatus.OK).json(finalResponse); + } + + @Patch('/orgs/:orgId/oid4vc/:issuerId/template/:templateId') + @ApiOperation({ + summary: 'Update credential template', + description: 'Updates a credential template for the specified issuer.' + }) + @ApiBearerAuth() + @Roles(OrgRoles.OWNER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + async updateTemplate( + @Param( + 'orgId', + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + } + }) + ) + orgId: string, + @Param( + 'issuerId', + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + } + }) + ) + issuerId: string, + @Param( + 'templateId', + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + } + }) + ) + templateId: string, + @User() user: user, + @Body() dto: UpdateCredentialTemplateDto, + @Res() res: Response + ): Promise { + const updated = await this.oid4vcIssuanceService.updateTemplate(user, orgId, templateId, dto, issuerId); + + const finalResponse: IResponse = { + statusCode: HttpStatus.OK, + message: ResponseMessages.oidcTemplate.success.update, + data: updated + }; + + return res.status(HttpStatus.OK).json(finalResponse); + } + + @Delete('/orgs/:orgId/oid4vc/:issuerId/template/:templateId') + @ApiOperation({ + summary: 'Delete credential template', + description: 'Deletes a credential template for the specified issuer.' + }) + @ApiBearerAuth() + @Roles(OrgRoles.OWNER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + async deleteTemplate( + @Param( + 'orgId', + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + } + }) + ) + orgId: string, + @Param( + 'issuerId', + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + } + }) + ) + issuerId: string, + @Param( + 'templateId', + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + } + }) + ) + templateId: string, + @User() user: user, + @Res() res: Response + ): Promise { + await this.oid4vcIssuanceService.deleteTemplate(user, orgId, templateId, issuerId); + const finalResponse: IResponse = { + statusCode: HttpStatus.OK, + message: ResponseMessages.oidcTemplate.success.delete + }; + + return res.status(HttpStatus.OK).json(finalResponse); + } + + @Post('/orgs/:orgId/oid4vc/:issuerId/create-offer') + @ApiOperation({ + summary: 'Create OID4VC Credential Offer', + description: 'Creates a new OID4VC credential-offer for a given issuer.' + }) + @ApiResponse({ status: HttpStatus.CREATED, description: 'Credential offer created successfully.' }) + @ApiBearerAuth() + @Roles(OrgRoles.OWNER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + async createOidcCredentialOffer( + @Param('orgId') + orgId: string, + @Param('issuerId') + issuerId: string, + @User() user: user, + @Body() oidcCredentialPayload: CreateOidcCredentialOfferDto, + @Res() res: Response + ): Promise { + oidcCredentialPayload.issuerId = issuerId; + const template = await this.oid4vcIssuanceService.createOidcCredentialOffer( + oidcCredentialPayload, + user, + orgId, + issuerId + ); + const finalResponse: IResponse = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.oidcIssuerSession.success.create, + data: template + }; + + return res.status(HttpStatus.CREATED).json(finalResponse); + } + + @Put('/orgs/:orgId/oid4vc/:issuerId/:credentialId/update-offer') + @ApiOperation({ + summary: 'Update OID4VC Credential Offer', + description: 'Updates an existing OIDC4VCI credential-offer.' + }) + @ApiResponse({ status: HttpStatus.OK, description: 'Credential offer updated successfully.', type: ApiResponseDto }) + @ApiBearerAuth() + @Roles(OrgRoles.OWNER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + async updateCredentialOffers( + @Body() oidcUpdateCredentialPayload: UpdateCredentialRequestDto, + @Param('orgId') orgId: string, + @Param('issuerId') issuerId: string, + @Param('credentialId') credentialId: string, + @Res() res: Response + ): Promise { + oidcUpdateCredentialPayload.issuerId = issuerId; + oidcUpdateCredentialPayload.credentialOfferId = credentialId; + const updateCredentialOffer = await this.oid4vcIssuanceService.updateOidcCredentialOffer( + oidcUpdateCredentialPayload, + orgId, + issuerId + ); + + const finalResponse: IResponse = { + statusCode: HttpStatus.OK, + message: ResponseMessages.oidcIssuerSession.success.update, + data: updateCredentialOffer + }; + return res.status(HttpStatus.OK).json(finalResponse); + } + + @Get('/orgs/:orgId/oid4vc/credential-offer/:id') + @ApiOperation({ + summary: 'Get OID4VC credential offer', + description: 'Retrieves an OID4VC credential offer by its ID.' + }) + @ApiResponse({ status: HttpStatus.OK, description: 'Credential offer fetched successfully.', type: ApiResponseDto }) + @ApiBearerAuth() + @Roles(OrgRoles.OWNER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + async getCredentialOfferDetailsById( + @Param( + 'orgId', + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + } + }) + ) + orgId: string, + @Param('id') + id: string, + @Res() res: Response + ): Promise { + const oidcIssuer = await this.oid4vcIssuanceService.getCredentialOfferDetailsById(id, orgId); + const finalResponse: IResponse = { + statusCode: HttpStatus.OK, + message: ResponseMessages.oidcIssuerSession.success.getById, + data: oidcIssuer + }; + return res.status(HttpStatus.OK).json(finalResponse); + } + + @Get('/orgs/:orgId/oid4vc/credential-offer') + @ApiOperation({ + summary: 'Get all OID4VC credential offers', + description: 'Retrieves all OID4VC credential offers for the specified organization.' + }) + @ApiResponse({ + status: HttpStatus.OK, + description: 'All credential offers fetched successfully.', + type: ApiResponseDto + }) + @ApiBearerAuth() + @Roles(OrgRoles.OWNER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + async getAllCredentialOffers( + @Query() getAllCredentialOffer: GetAllCredentialOfferDto, + @Param('orgId') orgId: string, + @Res() res: Response + ): Promise { + const connectionDetails = await this.oid4vcIssuanceService.getAllCredentialOffers(orgId, getAllCredentialOffer); + + const finalResponse: IResponse = { + statusCode: HttpStatus.OK, + message: ResponseMessages.oidcIssuerSession.success.getAll, + data: connectionDetails + }; + return res.status(HttpStatus.OK).json(finalResponse); + } + + @Delete('/orgs/:orgId/oid4vc/:credentialId/delete-offer') + @ApiOperation({ + summary: 'Delete OID4VC credential offer', + description: 'Deletes an OID4VC credential offer for the specified organization.' + }) + @ApiResponse({ status: HttpStatus.OK, description: 'Credential offer deleted successfully.', type: ApiResponseDto }) + @ApiBearerAuth() + @Roles(OrgRoles.OWNER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + async deleteCredentialOffers( + @Param('orgId') orgId: string, + @Param('credentialId') credentialId: string, + @Res() res: Response + ): Promise { + const deletedofferDetails = await this.oid4vcIssuanceService.deleteCredentialOffers(orgId, credentialId); + + const finalResponse: IResponse = { + statusCode: HttpStatus.NO_CONTENT, + message: ResponseMessages.oidcIssuerSession.success.delete, + data: deletedofferDetails + }; + return res.status(HttpStatus.NO_CONTENT).json(finalResponse); + } + + @Post('/orgs/:orgId/oid4vc/create-offer/agent') + @ApiOperation({ + summary: 'Create OID4VC Credential Offer direct to agent', + description: 'Creates a new OIDC4VCI credential-offer for a given issuer.' + }) + @ApiResponse({ status: HttpStatus.CREATED, description: 'Credential offer created successfully.' }) + @ApiBearerAuth() + @Roles(OrgRoles.OWNER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + async createOidcCredentialOfferD2A( + @Param('orgId') + orgId: string, + @Body() oidcCredentialD2APayload: CreateCredentialOfferD2ADto, + @Res() res: Response + ): Promise { + const credentialOffer = await this.oid4vcIssuanceService.createOidcCredentialOfferD2A( + oidcCredentialD2APayload, + orgId + ); + const finalResponse: IResponse = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.oidcIssuerSession.success.create, + data: credentialOffer + }; + + return res.status(HttpStatus.CREATED).json(finalResponse); + } + + /** + * Catch issue credential webhook responses + * @param oidcIssueCredentialDto The details of the oid4vc issued credential + * @param id The ID of the organization + * @param res The response object + * @returns The details of the oid4vc issued credential + */ + @Post('wh/:id/openid4vc-issuance') + @ApiExcludeEndpoint() + @ApiOperation({ + summary: 'Catch OID4VC credential states', + description: 'Handles webhook responses for OID4VC credential issuance.' + }) + async getIssueCredentialWebhook( + @Body() oidcIssueCredentialDto: OidcIssueCredentialDto, + @Param('id') id: string, + @Res() res: Response + ): Promise { + if (id && 'default' === oidcIssueCredentialDto.contextCorrelationId) { + oidcIssueCredentialDto.orgId = id; + } + + const getCredentialDetails = await this.oid4vcIssuanceService.oidcIssueCredentialWebhook( + oidcIssueCredentialDto, + id + ); + + const finalResponse: IResponseType = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.issuance.success.create, + data: getCredentialDetails + }; + + const webhookUrl = await this.oid4vcIssuanceService + ._getWebhookUrl(oidcIssueCredentialDto.contextCorrelationId, id) + .catch((error) => { + this.logger.debug(`error in getting webhook url ::: ${JSON.stringify(error)}`); + }); + if (webhookUrl) { + this.logger.log(`Posting response to the webhook url`); + const plainIssuanceDto = JSON.parse(JSON.stringify(oidcIssueCredentialDto)); + + await this.oid4vcIssuanceService._postWebhookResponse(webhookUrl, { data: plainIssuanceDto }).catch((error) => { + this.logger.debug(`error in posting webhook response to webhook url ::: ${JSON.stringify(error)}`); + }); + } + + return res.status(HttpStatus.CREATED).json(finalResponse); + } +} diff --git a/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.module.ts b/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.module.ts new file mode 100644 index 000000000..61470bce8 --- /dev/null +++ b/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.module.ts @@ -0,0 +1,24 @@ +import { Module } from '@nestjs/common'; +import { Oid4vcIssuanceService } from './oid4vc-issuance.service'; +import { Oid4vcIssuanceController } from './oid4vc-issuance.controller'; +import { NATSClient } from '@credebl/common/NATSClient'; +import { getNatsOptions } from '@credebl/common/nats.config'; +import { CommonConstants } from '@credebl/common/common.constant'; +import { ClientsModule, Transport } from '@nestjs/microservices'; +import { HttpModule } from '@nestjs/axios'; + +@Module({ + imports: [ + HttpModule, + ClientsModule.register([ + { + name: 'NATS_CLIENT', + transport: Transport.NATS, + options: getNatsOptions(CommonConstants.ISSUANCE_SERVICE, process.env.API_GATEWAY_NKEY_SEED) + } + ]) + ], + controllers: [Oid4vcIssuanceController], + providers: [Oid4vcIssuanceService, NATSClient] +}) +export class Oid4vcIssuanceModule {} diff --git a/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.service.ts b/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.service.ts new file mode 100644 index 000000000..c5a208ede --- /dev/null +++ b/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.service.ts @@ -0,0 +1,171 @@ +import { NATSClient } from '@credebl/common/NATSClient'; +import { Inject, Injectable } from '@nestjs/common'; +import { ClientProxy } from '@nestjs/microservices'; +import { IssuerCreationDto, IssuerUpdationDto } from './dtos/oid4vc-issuer.dto'; +import { BaseService } from 'libs/service/base.service'; +// eslint-disable-next-line camelcase +import { oidc_issuer, user } from '@prisma/client'; +import { CreateCredentialTemplateDto, UpdateCredentialTemplateDto } from './dtos/oid4vc-issuer-template.dto'; +import { + CreateCredentialOfferD2ADto, + CreateOidcCredentialOfferDto, + GetAllCredentialOfferDto, + UpdateCredentialRequestDto +} from './dtos/issuer-sessions.dto'; +import { OidcIssueCredentialDto } from './dtos/oid4vc-credential-wh.dto'; + +@Injectable() +export class Oid4vcIssuanceService extends BaseService { + constructor( + @Inject('NATS_CLIENT') private readonly issuanceProxy: ClientProxy, + private readonly natsClient: NATSClient + ) { + super('Oid4vcIssuanceService'); + } + + async oidcIssuerCreate( + issueCredentialDto: IssuerCreationDto, + orgId: string, + userDetails: user + // eslint-disable-next-line camelcase + ): Promise { + const payload = { issueCredentialDto, orgId, userDetails }; + return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oid4vc-issuer-create', payload); + } + + async oidcIssuerUpdate(issueUpdationDto: IssuerUpdationDto, orgId: string, userDetails: user): Promise { + const payload = { issueUpdationDto, orgId, userDetails }; + return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oid4vc-issuer-update', payload); + } + + async oidcGetIssuerById(id: string, orgId: string): Promise { + const payload = { id, orgId }; + return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oid4vc-issuer-get-by-id', payload); + } + + async oidcGetIssuers(orgId: string): Promise { + const payload = { orgId }; + return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oid4vc-get-issuers-issuance', payload); + } + + async oidcDeleteIssuer(userDetails: user, orgId: string, id: string): Promise { + const payload = { id, orgId, userDetails }; + return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oid4vc-delete-issuer', payload); + } + + async deleteTemplate(userDetails: user, orgId: string, templateId: string, issuerId: string): Promise { + const payload = { templateId, orgId, userDetails, issuerId }; + return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oid4vc-template-delete', payload); + } + + async updateTemplate( + userDetails: user, + orgId: string, + templateId: string, + dto: UpdateCredentialTemplateDto, + issuerId: string + ): Promise { + const payload = { templateId, orgId, userDetails, dto, issuerId }; + return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oid4vc-template-update', payload); + } + + async findByIdTemplate(orgId: string, templateId: string): Promise { + const payload = { templateId, orgId }; + return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oid4vc-template-find-id', payload); + } + + async findAllTemplate(orgId: string, issuerId: string): Promise { + const payload = { orgId, issuerId }; + return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oid4vc-template-find-all', payload); + } + + async createTemplate( + CredentialTemplate: CreateCredentialTemplateDto, + userDetails: user, + orgId: string, + issuerId: string + ): Promise { + const payload = { CredentialTemplate, orgId, userDetails, issuerId }; + return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oid4vc-template-create', payload); + } + + async createOidcCredentialOffer( + oidcCredentialPayload: CreateOidcCredentialOfferDto, + userDetails: user, + orgId: string, + issuerId: string + ): Promise { + const payload = { oidcCredentialPayload, orgId, userDetails, issuerId }; + return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oid4vc-create-credential-offer', payload); + } + + async createOidcCredentialOfferD2A( + oidcCredentialD2APayload: CreateCredentialOfferD2ADto, + orgId: string + ): Promise { + const payload = { oidcCredentialD2APayload, orgId }; + return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oid4vc-create-credential-offer-D2A', payload); + } + + async updateOidcCredentialOffer( + oidcUpdateCredentialPayload: UpdateCredentialRequestDto, + orgId: string, + issuerId: string + ): Promise { + const payload = { oidcUpdateCredentialPayload, orgId, issuerId }; + return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oid4vc-update-credential-offer', payload); + } + + async getCredentialOfferDetailsById(offerId: string, orgId: string): Promise { + const payload = { offerId, orgId }; + return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oid4vc-credential-offer-get-by-id', payload); + } + async getAllCredentialOffers(orgId: string, getAllCredentialOffer: GetAllCredentialOfferDto): Promise { + const payload = { orgId, getAllCredentialOffer }; + return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oid4vc-credential-offer-get-all', payload); + } + + async deleteCredentialOffers(orgId: string, credentialId: string): Promise { + const payload = { orgId, credentialId }; + return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oid4vc-credential-offer-delete', payload); + } + + oidcIssueCredentialWebhook( + oidcIssueCredentialDto: OidcIssueCredentialDto, + id: string + ): Promise<{ + response: object; + }> { + const payload = { oidcIssueCredentialDto, id }; + return this.natsClient.sendNats(this.issuanceProxy, 'webhook-oid4vc-issue-credential', payload); + } + + async _getWebhookUrl(tenantId?: string, orgId?: string): Promise { + const pattern = { cmd: 'get-webhookurl' }; + const payload = { tenantId, orgId }; + + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const message = await this.issuanceProxy.send(pattern, payload).toPromise(); + return message; + } catch (error) { + this.logger.error(`catch: ${JSON.stringify(error)}`); + throw error; + } + } + + async _postWebhookResponse(webhookUrl: string, data: object): Promise { + const pattern = { cmd: 'post-webhook-response-to-webhook-url' }; + const payload = { webhookUrl, data }; + + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const message = await this.issuanceProxy.send(pattern, payload).toPromise(); + return message; + } catch (error) { + this.logger.error(`catch: ${JSON.stringify(error)}`); + + throw error; + } + } +} diff --git a/apps/api-gateway/src/oid4vc-verification/dtos/oid4vc-verifier-presentation.dto.ts b/apps/api-gateway/src/oid4vc-verification/dtos/oid4vc-verifier-presentation.dto.ts new file mode 100644 index 000000000..ac8dbf415 --- /dev/null +++ b/apps/api-gateway/src/oid4vc-verification/dtos/oid4vc-verifier-presentation.dto.ts @@ -0,0 +1,357 @@ +import { OpenId4VcVerificationPresentationState } from '@credebl/common/interfaces/oid4vp-verification'; +import { ApiHideProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsEnum, IsOptional, IsString, IsUrl } from 'class-validator'; +/* eslint-disable camelcase */ +import { ApiProperty } from '@nestjs/swagger'; +import { + IsArray, + IsBoolean, + IsDefined, + ValidateNested, + ValidatorConstraint, + ValidatorConstraintInterface, + ValidationArguments, + Validate, + Matches +} from 'class-validator'; +import { Type } from 'class-transformer'; +import { SignerOption } from '@prisma/client'; +import { ResponseMode } from '@credebl/enum/enum'; + +/** + * DTO for verification-presentation query parameters. + * Use with @Query() in your controller and enable ValidationPipe globally or on the route. + */ +export class VerificationPresentationQueryDto { + @ApiPropertyOptional({ + description: 'Public identifier of the verifier', + example: 'verifier_0x123' + }) + @IsOptional() + @IsString() + publicVerifierId?: string; + + @ApiPropertyOptional({ + description: 'Opaque payload state used by the client / verifier', + example: 'payload-state-xyz' + }) + @IsOptional() + @IsString() + payloadState?: string; + + @ApiPropertyOptional({ + description: 'Presentation state', + enum: OpenId4VcVerificationPresentationState, + example: OpenId4VcVerificationPresentationState.RequestCreated + }) + @IsOptional() + @IsEnum(OpenId4VcVerificationPresentationState) + state?: OpenId4VcVerificationPresentationState; + + @ApiPropertyOptional({ + description: 'Authorization request URI (if present)', + example: 'https://auth.example.com/request/abc123' + }) + @IsOptional() + @IsUrl() + authorizationRequestUri?: string; + + @ApiPropertyOptional({ + description: 'Nonce associated with the presentation', + example: 'n-0S6_WzA2Mj' + }) + @IsOptional() + @IsString() + nonce?: string; + + @ApiPropertyOptional({ + description: 'Optional id to target a specific presentation/resource', + example: 'presentation-id-987' + }) + @IsOptional() + @IsString() + id?: string; +} + +/** + * ----------- PEX DTOs ----------- + */ + +export class PexFieldConstraintDto { + @ApiProperty({ example: 'full_name', description: 'Field identifier' }) + @IsDefined() + @IsString() + id: string; + + @ApiProperty({ + example: ["$['full_name']"], + description: 'JSONPath location(s) of the requested claim field', + isArray: true, + type: String + }) + @IsDefined() + @IsArray() + @IsString({ each: true }) + path: string[]; + + @ApiPropertyOptional({ example: "Request holder's full name" }) + @IsOptional() + @IsString() + purpose?: string; + + @ApiPropertyOptional({ example: true }) + @IsOptional() + @IsBoolean() + intent_to_retain?: boolean; +} + +export class PexConstraintDto { + @ApiPropertyOptional({ example: 'required', description: 'Limit disclosure policy' }) + @IsOptional() + @IsString() + limit_disclosure?: string; + + @ApiProperty({ + type: [PexFieldConstraintDto], + description: 'List of requested claim fields', + example: [ + { + id: 'full_name', + path: ["$['full_name']"], + purpose: "Request holder's full name", + intent_to_retain: true + }, + { + id: 'birth_date', + path: ["$['birth_date']"], + purpose: "Request holder's birth date", + intent_to_retain: true + } + ] + }) + @IsDefined() + @IsArray() + @ValidateNested({ each: true }) + @Type(() => PexFieldConstraintDto) + fields: PexFieldConstraintDto[]; +} + +export class PexInputDescriptorDto { + @ApiProperty({ example: 'BirthCertificate-vc+sd-jwt' }) + @IsDefined() + @IsString() + id: string; + + @ApiPropertyOptional({ example: 'Birth Certificate (vc+sd-jwt)' }) + @IsOptional() + @IsString() + name?: string; + + @ApiProperty({ + example: { 'vc+sd-jwt': {} }, + description: 'Supported credential format' + }) + @IsDefined() + format: Record; + + @ApiProperty({ type: PexConstraintDto }) + @IsDefined() + @ValidateNested() + @Type(() => PexConstraintDto) + constraints: PexConstraintDto; +} + +export class PexDefinitionDto { + @ApiProperty({ example: 'BirthCertificate-verification' }) + @IsDefined() + @IsString() + id: string; + + @ApiPropertyOptional({ + example: 'Present your full name and birth date to verify the BirthCertificate offer' + }) + @IsOptional() + @IsString() + purpose?: string; + + @ApiProperty({ + type: [PexInputDescriptorDto], + description: 'Input descriptors specifying credential requirements' + }) + @IsDefined() + @IsArray() + @ValidateNested({ each: true }) + @Type(() => PexInputDescriptorDto) + input_descriptors: PexInputDescriptorDto[]; +} + +export class PresentationExchangeDto { + @ApiProperty({ type: PexDefinitionDto }) + @IsDefined() + @ValidateNested() + @Type(() => PexDefinitionDto) + definition: PexDefinitionDto; +} + +/** + * ----------- DCQL DTOs ----------- + */ + +export class DcqlClaimDto { + @ApiProperty({ + example: ['full_name'], + description: 'Claim path(s) requested from the credential' + }) + @IsDefined() + @IsArray() + @IsString({ each: true }) + path: string[]; + + @ApiPropertyOptional({ example: true }) + @IsOptional() + @IsBoolean() + intent_to_retain?: boolean; +} + +export class DcqlCredentialDto { + @ApiProperty({ example: 'birthcertificate-dc_sd_jwt' }) + @IsDefined() + @IsString() + @Matches(/^[a-zA-Z0-9_-]+$/, { + message: 'id must only contain alphanumeric characters, underscores, and hyphens (dots are not allowed)' + }) + id: string; + + @ApiProperty({ example: 'dc+sd-jwt' }) + @IsDefined() + @IsString() + format: string; + + @ApiPropertyOptional({ example: { vct: 'urn:example:vc+sd-jwt' } }) + @IsOptional() + meta?: Record; + + @ApiPropertyOptional({ example: true }) + @IsOptional() + @IsBoolean() + require_cryptographic_holder_binding?: boolean; + + @ApiProperty({ + type: [DcqlClaimDto], + description: 'List of claims requested from the credential' + }) + @IsDefined() + @IsArray() + @ValidateNested({ each: true }) + @Type(() => DcqlClaimDto) + claims: DcqlClaimDto[]; +} + +export class DcqlQueryDto { + @ApiPropertyOptional({ example: 'all' }) + @IsOptional() + @IsString() + combine?: string; + + @ApiProperty({ + type: [DcqlCredentialDto], + description: 'List of credential queries' + }) + @IsDefined() + @IsArray() + @ValidateNested({ each: true }) + @Type(() => DcqlCredentialDto) + credentials: DcqlCredentialDto[]; +} + +export class DcqlDto { + @ApiProperty({ type: DcqlQueryDto }) + @IsDefined() + @ValidateNested() + @Type(() => DcqlQueryDto) + query: DcqlQueryDto; +} + +/** + * ----------- ROOT DTO ----------- + */ +/** + * Class-level validator: exactly one of the specified properties must be present. + */ +@ValidatorConstraint({ name: 'OnlyOneOf', async: false }) +export class OnlyOneOfConstraint implements ValidatorConstraintInterface { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + validate(_: any, args: ValidationArguments): Promise | boolean { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const object = args.object as Record; + const properties = args.constraints as string[]; + let present = 0; + for (const p of properties) { + if (object[p] !== undefined && null !== object[p]) { + present++; + } + } + return 1 === present; + } + + defaultMessage(args: ValidationArguments): string { + const properties = args.constraints as string[]; + return `Exactly one of [${properties.join(', ')}] must be provided`; + } +} + +export class PresentationRequestDto { + @ApiPropertyOptional({ + example: { + method: 'DID' + }, + description: 'Signer option type' + }) + @IsOptional() + requestSigner?: { + method: SignerOption; + }; + + @ApiPropertyOptional({ + type: PresentationExchangeDto, + description: 'PEX-based presentation exchange definition' + }) + @IsOptional() + @ValidateNested() + @Type(() => PresentationExchangeDto) + presentationExchange?: PresentationExchangeDto; + + @ApiPropertyOptional({ + type: DcqlDto, + description: 'DCQL-based presentation query definition' + }) + @IsOptional() + @ValidateNested() + @Type(() => DcqlDto) + dcql?: DcqlDto; + + @ApiProperty({ + example: ResponseMode.DIRECT_POST_JWT, + description: 'Response mode for the verifier', + enum: ResponseMode + }) + @IsDefined() + @IsEnum(ResponseMode) + responseMode: ResponseMode; + //TODO: check e2e flow and add ResponseMode based restrictions + @IsOptional() + expectedOrigins: string[]; + + /** + * Dummy property used to run a class-level validation ensuring mutual exclusivity. + * This property is not serialized into requests/responses but is required so `class-validator` + * executes the validator with access to the whole object. + */ + @ApiHideProperty() + @ApiPropertyOptional({ + description: 'Internal: ensures exactly one of dcql or presentationExchange is present' + }) + @Validate(OnlyOneOfConstraint, ['dcql', 'presentationExchange']) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + _oneOfCheck?: any; +} diff --git a/apps/api-gateway/src/oid4vc-verification/dtos/oid4vc-verifier.dto.ts b/apps/api-gateway/src/oid4vc-verification/dtos/oid4vc-verifier.dto.ts new file mode 100644 index 000000000..17a2db754 --- /dev/null +++ b/apps/api-gateway/src/oid4vc-verification/dtos/oid4vc-verifier.dto.ts @@ -0,0 +1,44 @@ +/* eslint-disable camelcase */ +import { IsOptional, IsString, ValidateNested } from 'class-validator'; +import { Type } from 'class-transformer'; +import { ApiProperty, ApiPropertyOptional, OmitType, PartialType } from '@nestjs/swagger'; + +class ClientMetadataDto { + @ApiProperty({ + description: 'Name of the client application or verifier', + example: 'Example Verifier App' + }) + @IsString() + client_name: string; + + @ApiProperty({ + description: 'Logo URL of the client application', + example: 'https://example.com/logo.png' + }) + @IsString() + logo_uri: string; +} + +export class CreateVerifierDto { + @ApiProperty({ + description: 'Unique identifier for the verifier', + example: 'verifier-12345' + }) + @IsString() + verifierId: string; + + @ApiPropertyOptional({ + description: 'Optional metadata for the verifier’s client configuration', + type: () => ClientMetadataDto, + example: { + client_name: 'Example Verifier App', + logo_uri: 'https://example.com/logo.png' + } + }) + @IsOptional() + @ValidateNested() + @Type(() => ClientMetadataDto) + clientMetadata?: ClientMetadataDto; +} + +export class UpdateVerifierDto extends PartialType(OmitType(CreateVerifierDto, ['verifierId'])) {} diff --git a/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.controller.ts b/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.controller.ts new file mode 100644 index 000000000..12b36bf45 --- /dev/null +++ b/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.controller.ts @@ -0,0 +1,460 @@ +/* eslint-disable default-param-last */ +/* eslint-disable no-param-reassign */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable no-console */ +/* eslint-disable camelcase */ +import { + Controller, + Post, + Body, + UseGuards, + HttpStatus, + Res, + Param, + UseFilters, + BadRequestException, + ParseUUIDPipe, + Get, + Query, + Put, + Delete, + Logger +} from '@nestjs/common'; +import { + ApiTags, + ApiOperation, + ApiResponse, + ApiBearerAuth, + ApiForbiddenResponse, + ApiUnauthorizedResponse, + ApiQuery, + ApiExcludeEndpoint +} from '@nestjs/swagger'; +import { AuthGuard } from '@nestjs/passport'; +import { ApiResponseDto } from '../dtos/apiResponse.dto'; +import { UnauthorizedErrorDto } from '../dtos/unauthorized-error.dto'; +import { ForbiddenErrorDto } from '../dtos/forbidden-error.dto'; +import { Response } from 'express'; +import IResponseType, { IResponse } from '@credebl/common/interfaces/response.interface'; +import { User } from '../authz/decorators/user.decorator'; +import { ResponseMessages } from '@credebl/common/response-messages'; +import { Roles } from '../authz/decorators/roles.decorator'; +import { OrgRoles } from 'libs/org-roles/enums'; +import { OrgRolesGuard } from '../authz/guards/org-roles.guard'; +import { CustomExceptionFilter } from 'apps/api-gateway/common/exception-handler'; +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { user } from '@prisma/client'; +import { Oid4vcVerificationService } from './oid4vc-verification.service'; +import { CreateVerifierDto, UpdateVerifierDto } from './dtos/oid4vc-verifier.dto'; +import { PresentationRequestDto, VerificationPresentationQueryDto } from './dtos/oid4vc-verifier-presentation.dto'; +import { Oid4vpPresentationWhDto } from '../oid4vc-issuance/dtos/oid4vp-presentation-wh.dto'; + +@Controller() +@UseFilters(CustomExceptionFilter) +@ApiTags('OID4VP') +@ApiUnauthorizedResponse({ description: 'Unauthorized', type: UnauthorizedErrorDto }) +@ApiForbiddenResponse({ description: 'Forbidden', type: ForbiddenErrorDto }) +export class Oid4vcVerificationController { + private readonly logger = new Logger('Oid4vcVerificationController'); + + constructor(private readonly oid4vcVerificationService: Oid4vcVerificationService) {} + /** + * Create issuer against a org(tenant) + * @param orgId The ID of the organization + * @param user The user making the request + * @param res The response object + * @returns The status of the deletion operation + */ + + @Post('/orgs/:orgId/oid4vp/verifier') + @ApiOperation({ + summary: 'Create OID4VP verifier', + description: 'Creates a new OID4VP verifier for the specified organization.' + }) + @ApiResponse({ status: HttpStatus.CREATED, description: 'Verifier created successfully.', type: ApiResponseDto }) + @ApiBearerAuth() + @Roles(OrgRoles.OWNER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + async oidcIssuerCreate( + @Param( + 'orgId', + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + } + }) + ) + orgId: string, + @User() user: user, + @Body() createVerifier: CreateVerifierDto, + @Res() res: Response + ): Promise { + this.logger.debug(`[oidcIssuerCreate] Called with orgId=${orgId}, user=${user.id}`); + + const createVerifierRes = await this.oid4vcVerificationService.oid4vpCreateVerifier(createVerifier, orgId, user); + + this.logger.debug(`[oidcIssuerCreate] Verifier created: ${createVerifierRes.id}`); + + const finalResponse: IResponse = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.oid4vp.success.create, + data: createVerifierRes + }; + return res.status(HttpStatus.CREATED).json(finalResponse); + } + + /** + * Create issuer against a org(tenant) + * @param orgId The ID of the organization + * @param verifierId The ID of the Verifier + * @param user The user making the request + * @param res The response object + * @returns The status of the verifier update operation + */ + @Put('/orgs/:orgId/oid4vp/verifier/:verifierId') + @ApiOperation({ + summary: 'Update OID4VP verifier', + description: 'Updates OID4VP verifier for the specified organization.' + }) + @ApiResponse({ status: HttpStatus.OK, description: 'Verifier updated successfully.', type: ApiResponseDto }) + @ApiBearerAuth() + @Roles(OrgRoles.OWNER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + async oidcIssuerUpdate( + @Param( + 'orgId', + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + } + }) + ) + orgId: string, + @Param('verifierId') + verifierId: string, + @User() user: user, + @Body() updateVerifier: UpdateVerifierDto, + @Res() res: Response + ): Promise { + this.logger.debug(`[oidcIssuerUpdate] Called with orgId=${orgId}, verifierId=${verifierId}, user=${user.id}`); + const updateVerifierRes = await this.oid4vcVerificationService.oid4vpUpdateVerifier( + updateVerifier, + orgId, + verifierId, + user + ); + + this.logger.debug(`[oidcIssuerUpdate] Verifier updated: ${updateVerifierRes.id}`); + + const finalResponse: IResponse = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.oid4vp.success.update, + data: updateVerifierRes + }; + return res.status(HttpStatus.OK).json(finalResponse); + } + + @Get('/orgs/:orgId/oid4vp/verifier') + @ApiOperation({ + summary: 'Get OID4VP verifier details', + description: 'Retrieves details of a specific OID4VP verifier by its ID for the specified organization.' + }) + @ApiQuery({ + name: 'verifierId', + required: false, + type: String, + description: 'UUID of the verifier (optional)' + }) + @ApiResponse({ status: HttpStatus.OK, description: 'Verifier details retrieved successfully.', type: ApiResponseDto }) + @ApiBearerAuth() + @Roles(OrgRoles.OWNER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + async getVerifierDetails( + @Param( + 'orgId', + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + } + }) + ) + orgId: string, + @Res() res: Response, + @Query( + 'verifierId', + new ParseUUIDPipe({ + version: '4', + optional: true, + exceptionFactory: (): Error => { + throw new BadRequestException('Invalid verifier ID'); + } + }) + ) + verifierId?: string + ): Promise { + this.logger.debug(`[getVerifierDetails] Called with orgId=${orgId}, verifierId=${verifierId}`); + + const verifierDetails = await this.oid4vcVerificationService.oid4vpGetVerifier(orgId, verifierId); + + this.logger.debug(`[getVerifierDetails] Result fetched successfully`); + const finalResponse: IResponse = { + statusCode: HttpStatus.OK, + message: ResponseMessages.oid4vp.success.fetch, + data: verifierDetails + }; + return res.status(HttpStatus.OK).json(finalResponse); + } + + @Delete('/orgs/:orgId/oid4vp/verifier') + @ApiOperation({ + summary: 'Delete OID4VP verifier details', + description: 'Delete a specific OID4VP verifier by its ID for the specified organization.' + }) + @ApiResponse({ status: HttpStatus.OK, description: 'Verifier deleted successfully.', type: ApiResponseDto }) + @ApiBearerAuth() + @Roles(OrgRoles.OWNER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + async deleteVerifierDetails( + @Param( + 'orgId', + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + } + }) + ) + orgId: string, + @Res() res: Response, + @Query( + 'verifierId', + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException('Invalid verifier ID'); + } + }) + ) + verifierId: string + ): Promise { + this.logger.debug(`[deleteVerifierDetails] Called with orgId=${orgId}, verifierId=${verifierId}`); + + const verifierDetails = await this.oid4vcVerificationService.oid4vpDeleteVerifier(orgId, verifierId); + + this.logger.debug(`[deleteVerifierDetails] Deleted verifier: ${verifierId}`); + const finalResponse: IResponse = { + statusCode: HttpStatus.OK, + message: ResponseMessages.oid4vp.success.delete, + data: verifierDetails + }; + return res.status(HttpStatus.OK).json(finalResponse); + } + + @Post('/orgs/:orgId/oid4vp/presentation') + @ApiOperation({ + summary: 'Create verification presentation', + description: 'Creates a new OID4VP verification presentation for the specified organization.' + }) + @ApiResponse({ + status: HttpStatus.CREATED, + description: 'Verification presentation created successfully.', + type: ApiResponseDto + }) + @ApiBearerAuth() + @Roles(OrgRoles.OWNER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + async createVerificationPresentation( + @Param( + 'orgId', + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + } + }) + ) + orgId: string, + @Query( + 'verifierId', + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException('Invalid verifier ID'); + } + }) + ) + verifierId: string, + @User() user: user, + @Body() createPresentationDto: PresentationRequestDto, + @Res() res: Response + ): Promise { + this.logger.debug( + `[createVerificationPresentation] Called with orgId=${orgId}, verifierId=${verifierId}, user=${user.id}` + ); + + const presentation = await this.oid4vcVerificationService.oid4vpCreateVerificationSession( + createPresentationDto, + orgId, + user, + verifierId + ); + + this.logger.debug(`[createVerificationPresentation] Presentation created successfully`); + + const finalResponse: IResponse = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.oid4vpSession.success.create, + data: presentation + }; + return res.status(HttpStatus.CREATED).json(finalResponse); + } + + @Get('/orgs/:orgId/oid4vp/verifier-presentation') + @ApiOperation({ + summary: 'Get OID4VP verifier presentation details', + description: + 'Retrieves details of all OID4VP verifier presentations or a single presentation by its ID for the specified organization.' + }) + @ApiResponse({ + status: HttpStatus.OK, + description: 'Verifier details retrieved successfully.', + type: ApiResponseDto + }) + @ApiBearerAuth() + @Roles(OrgRoles.OWNER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + async getVerificationPresentation( + @Param( + 'orgId', + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + } + }) + ) + orgId: string, + @Query() query: VerificationPresentationQueryDto, + @Res() res: Response + ): Promise { + try { + this.logger.debug(`getVerificationPresentation() called with orgId: ${orgId}`); + const result = await this.oid4vcVerificationService.oid4vpGetVerifierSession(orgId, query); + + this.logger.debug(`Verifier session details fetched successfully for orgId: ${orgId}`); + + return res.status(HttpStatus.OK).json({ + success: true, + message: 'Verifier details retrieved successfully.', + data: result + }); + } catch (error) { + this.logger.debug( + `Error in getVerificationPresentation(): ${error.message || 'Failed to fetch verifier presentation.'}` + ); + throw new BadRequestException(error.message || 'Failed to fetch verifier presentation.'); + } + } + + @Get('/orgs/:orgId/oid4vp/verifier-presentation-response') + @ApiOperation({ + summary: 'Get OID4VP verifier presentation response details', + description: + 'Retrieves details of OID4VP verifier presentations response by its verification presentation ID for the specified organization.' + }) + @ApiResponse({ + status: HttpStatus.OK, + description: 'Verifier presentation response details retrieved successfully.', + type: ApiResponseDto + }) + @ApiBearerAuth() + @Roles(OrgRoles.OWNER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + async getVerificationPresentationResponse( + @Param( + 'orgId', + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + } + }) + ) + orgId: string, + @Query( + 'verificationPresentationId', + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException('Invalid verificationPresentationId ID'); + } + }) + ) + verificationPresentationId: string, + @Res() res: Response + ): Promise { + try { + this.logger.debug( + `getVerificationPresentationResponse() called with orgId: ${orgId}, verificationPresentationId: ${verificationPresentationId}` + ); + const result = await this.oid4vcVerificationService.getVerificationSessionResponse( + orgId, + verificationPresentationId + ); + + this.logger.debug( + `Verifier presentation response details fetched successfully for verificationPresentationId: ${verificationPresentationId}` + ); + + return res.status(HttpStatus.OK).json({ + success: true, + message: 'Verifier presentation response details retrieved successfully.', + data: result + }); + } catch (error) { + this.logger.debug( + `Error in getVerificationPresentationResponse(): ${error.message || 'Failed to fetch verifier presentation response details.'}` + ); + throw new BadRequestException(error.message || 'Failed to fetch verifier presentation response details.'); + } + } + /** + * Catch issue credential webhook responses + * @param oid4vpPresentationWhDto The details of the oid4vp presentation webhook + * @param id The ID of the organization + * @param res The response object + * @returns The details of the oid4vp presentation webhook + */ + @Post('wh/:id/openid4vc-verification') + @ApiExcludeEndpoint() + @ApiOperation({ + summary: 'Catch OID4VP presentation states', + description: 'Handles webhook responses for OID4VP presentation states.' + }) + async storePresentationWebhook( + @Body() oid4vpPresentationWhDto: Oid4vpPresentationWhDto, + @Param('id') id: string, + @Res() res: Response + ): Promise { + oid4vpPresentationWhDto.type = 'Oid4vpPresentation'; + if (id && 'default' === oid4vpPresentationWhDto.contextCorrelationId) { + oid4vpPresentationWhDto.orgId = id; + } + + await this.oid4vcVerificationService.oid4vpPresentationWebhook(oid4vpPresentationWhDto, id); + const finalResponse: IResponseType = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.oid4vpSession.success.webhookReceived, + data: [] + }; + + const webhookUrl = await this.oid4vcVerificationService + ._getWebhookUrl(oid4vpPresentationWhDto?.contextCorrelationId, id) + .catch((error) => { + this.logger.debug(`error in getting webhook url ::: ${JSON.stringify(error)}`); + }); + + if (webhookUrl) { + await this.oid4vcVerificationService + ._postWebhookResponse(webhookUrl, { data: oid4vpPresentationWhDto }) + .catch((error) => { + this.logger.debug(`error in posting webhook response to webhook url ::: ${JSON.stringify(error)}`); + }); + } + + return res.status(HttpStatus.CREATED).json(finalResponse); + } +} diff --git a/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.module.ts b/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.module.ts new file mode 100644 index 000000000..b4cbf3344 --- /dev/null +++ b/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.module.ts @@ -0,0 +1,26 @@ +import { Module } from '@nestjs/common'; +import { Oid4vcVerificationService } from './oid4vc-verification.service'; +import { Oid4vcVerificationController } from './oid4vc-verification.controller'; +import { NATSClient } from '@credebl/common/NATSClient'; +import { getNatsOptions } from '@credebl/common/nats.config'; +import { CommonConstants } from '@credebl/common/common.constant'; +import { ClientsModule, Transport } from '@nestjs/microservices'; +import { HttpModule } from '@nestjs/axios'; +import { LoggerModule } from '@credebl/logger'; + +@Module({ + imports: [ + HttpModule, + LoggerModule, + ClientsModule.register([ + { + name: 'NATS_CLIENT', + transport: Transport.NATS, + options: getNatsOptions(CommonConstants.OIDC4VC_VERIFICATION_SERVICE, process.env.API_GATEWAY_NKEY_SEED) + } + ]) + ], + controllers: [Oid4vcVerificationController], + providers: [Oid4vcVerificationService, NATSClient] +}) +export class Oid4vpModule {} diff --git a/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.service.ts b/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.service.ts new file mode 100644 index 000000000..e319f5674 --- /dev/null +++ b/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.service.ts @@ -0,0 +1,123 @@ +import { NATSClient } from '@credebl/common/NATSClient'; +import { Inject, Injectable, Logger } from '@nestjs/common'; +import { ClientProxy } from '@nestjs/microservices'; +// eslint-disable-next-line camelcase +import { oid4vp_verifier, user } from '@prisma/client'; +import { CreateVerifierDto, UpdateVerifierDto } from './dtos/oid4vc-verifier.dto'; +import { VerificationPresentationQueryDto } from './dtos/oid4vc-verifier-presentation.dto'; +import { Oid4vpPresentationWhDto } from '../oid4vc-issuance/dtos/oid4vp-presentation-wh.dto'; + +@Injectable() +export class Oid4vcVerificationService { + private readonly logger = new Logger('Oid4vcVerificationService'); + + constructor( + @Inject('NATS_CLIENT') private readonly oid4vpProxy: ClientProxy, + private readonly natsClient: NATSClient + ) {} + + async oid4vpCreateVerifier( + createVerifier: CreateVerifierDto, + orgId: string, + userDetails: user + // eslint-disable-next-line camelcase + ): Promise { + const payload = { createVerifier, orgId, userDetails }; + this.logger.debug(`[oid4vpCreateVerifier] Called with orgId=${orgId}, user=${userDetails?.id}`); + return this.natsClient.sendNatsMessage(this.oid4vpProxy, 'oid4vp-verifier-create', payload); + } + + async oid4vpUpdateVerifier( + updateVerifier: UpdateVerifierDto, + orgId: string, + verifierId: string, + userDetails: user + // eslint-disable-next-line camelcase + ): Promise { + const payload = { updateVerifier, orgId, verifierId, userDetails }; + this.logger.debug( + `[oid4vpUpdateVerifier] Called with orgId=${orgId}, verifierId=${verifierId}, user=${userDetails?.id}` + ); + return this.natsClient.sendNatsMessage(this.oid4vpProxy, 'oid4vp-verifier-update', payload); + } + + async oid4vpGetVerifier(orgId, verifierId?: string): Promise { + const payload = { orgId, verifierId }; + this.logger.debug(`[oid4vpGetVerifier] Called with orgId=${orgId}, verifierId=${verifierId ?? 'N/A'}`); + return this.natsClient.sendNatsMessage(this.oid4vpProxy, 'oid4vp-verifier-get', payload); + } + + async oid4vpDeleteVerifier(orgId, verifierId: string): Promise { + const payload = { orgId, verifierId }; + this.logger.debug(`[oid4vpDeleteVerifier] Called with orgId=${orgId}, verifierId=${verifierId}`); + return this.natsClient.sendNatsMessage(this.oid4vpProxy, 'oid4vp-verifier-delete', payload); + } + + async oid4vpGetVerifierSession(orgId, query?: VerificationPresentationQueryDto): Promise { + const payload = { orgId, query }; + this.logger.debug( + `[oid4vpGetVerifierSession] Called with orgId=${orgId}, queryParams=${Object.keys(query || {}).length}` + ); + return this.natsClient.sendNatsMessage(this.oid4vpProxy, 'oid4vp-verifier-session-get', payload); + } + async getVerificationSessionResponse(orgId, verificationSessionId: string): Promise { + const payload = { orgId, verificationSessionId }; + this.logger.debug( + `[getVerificationSessionResponse] Called with orgId=${orgId}, verificationSessionId=${verificationSessionId}` + ); + return this.natsClient.sendNatsMessage(this.oid4vpProxy, 'oid4vp-verifier-session-response-get', payload); + } + + async oid4vpCreateVerificationSession( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + sessionRequest: any, + orgId: string, + userDetails: user, + verifierId?: string + ): Promise { + const payload = { sessionRequest, orgId, verifierId, userDetails }; + this.logger.debug( + `[oid4vpCreateVerificationSession] Called with orgId=${orgId}, verifierId=${verifierId ?? 'N/A'}, user=${userDetails?.id ?? 'N/A'}` + ); + return this.natsClient.sendNatsMessage(this.oid4vpProxy, 'oid4vp-verification-session-create', payload); + } + + oid4vpPresentationWebhook( + oid4vpPresentationWhDto: Oid4vpPresentationWhDto, + id: string + ): Promise<{ + response: object; + }> { + const payload = { oid4vpPresentationWhDto, id }; + return this.natsClient.sendNats(this.oid4vpProxy, 'webhook-oid4vp-presentation', payload); + } + + async _getWebhookUrl(tenantId?: string, orgId?: string): Promise { + const pattern = { cmd: 'get-webhookurl' }; + const payload = { tenantId, orgId }; + + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const message = await this.oid4vpProxy.send(pattern, payload).toPromise(); + return message; + } catch (error) { + this.logger.error(`catch: ${JSON.stringify(error)}`); + throw error; + } + } + + async _postWebhookResponse(webhookUrl: string, data: object): Promise { + const pattern = { cmd: 'post-webhook-response-to-webhook-url' }; + const payload = { webhookUrl, data }; + + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const message = await this.oid4vpProxy.send(pattern, payload).toPromise(); + return message; + } catch (error) { + this.logger.error(`catch: ${JSON.stringify(error)}`); + + throw error; + } + } +} diff --git a/apps/api-gateway/src/verification/dto/webhook-proof.dto.ts b/apps/api-gateway/src/verification/dto/webhook-proof.dto.ts index d4f150c48..1f71f5c52 100644 --- a/apps/api-gateway/src/verification/dto/webhook-proof.dto.ts +++ b/apps/api-gateway/src/verification/dto/webhook-proof.dto.ts @@ -1,83 +1,82 @@ -import { ApiPropertyOptional } from "@nestjs/swagger"; -import { IsOptional } from "class-validator"; +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { IsOptional } from 'class-validator'; interface IWebhookPresentationProof { - threadId: string; - state: string; - connectionId + threadId: string; + state: string; + connectionId; } export class WebhookPresentationProofDto { + @ApiPropertyOptional() + @IsOptional() + metadata: object; - @ApiPropertyOptional() - @IsOptional() - metadata: object; - - @ApiPropertyOptional() - @IsOptional() - _tags: IWebhookPresentationProof; - - @ApiPropertyOptional() - @IsOptional() - id: string; - - @ApiPropertyOptional() - @IsOptional() - createdAt: string; - - @ApiPropertyOptional() - @IsOptional() - protocolVersion: string; - - @ApiPropertyOptional() - @IsOptional() - state: string; - - @ApiPropertyOptional() - @IsOptional() - connectionId: string; - - @ApiPropertyOptional() - @IsOptional() - threadId: string; - - @ApiPropertyOptional() - @IsOptional() - parentThreadId?: string; - - @ApiPropertyOptional() - @IsOptional() - presentationId: string; - - @ApiPropertyOptional() - @IsOptional() - autoAcceptProof: string; - - @ApiPropertyOptional() - @IsOptional() - updatedAt: string; - - @ApiPropertyOptional() - @IsOptional() - isVerified: boolean; - - @ApiPropertyOptional() - @IsOptional() - contextCorrelationId: string; - - @ApiPropertyOptional() - @IsOptional() - type: string; - - @ApiPropertyOptional() - @IsOptional() - proofData: object; - - @ApiPropertyOptional() - @IsOptional() - orgId: string; - - @ApiPropertyOptional() - @IsOptional() - errorMessage: string; -} \ No newline at end of file + @ApiPropertyOptional() + @IsOptional() + _tags: IWebhookPresentationProof; + + @ApiPropertyOptional() + @IsOptional() + id: string; + + @ApiPropertyOptional() + @IsOptional() + createdAt: string; + + @ApiPropertyOptional() + @IsOptional() + protocolVersion: string; + + @ApiPropertyOptional() + @IsOptional() + state: string; + + @ApiPropertyOptional() + @IsOptional() + connectionId: string; + + @ApiPropertyOptional() + @IsOptional() + threadId: string; + + @ApiPropertyOptional() + @IsOptional() + parentThreadId?: string; + + @ApiPropertyOptional() + @IsOptional() + presentationId: string; + + @ApiPropertyOptional() + @IsOptional() + autoAcceptProof: string; + + @ApiPropertyOptional() + @IsOptional() + updatedAt: string; + + @ApiPropertyOptional() + @IsOptional() + isVerified: boolean; + + @ApiPropertyOptional() + @IsOptional() + contextCorrelationId: string; + + @ApiPropertyOptional() + @IsOptional() + type: string; + + @ApiPropertyOptional() + @IsOptional() + proofData: object; + + @ApiPropertyOptional() + @IsOptional() + orgId: string; + + @ApiPropertyOptional() + @IsOptional() + errorMessage: string; +} diff --git a/apps/api-gateway/src/verification/interfaces/verification.interface.ts b/apps/api-gateway/src/verification/interfaces/verification.interface.ts index cac18fa67..7877821fa 100644 --- a/apps/api-gateway/src/verification/interfaces/verification.interface.ts +++ b/apps/api-gateway/src/verification/interfaces/verification.interface.ts @@ -1,92 +1,84 @@ -import { IUserRequestInterface } from "../../interfaces/IUserRequestInterface"; +import { IUserRequestInterface } from '../../interfaces/IUserRequestInterface'; export interface IProofRequestAttribute { - attributeName: string; - condition?: string; - value?: string; - credDefId: string; - credentialName: string; + attributeName: string; + condition?: string; + value?: string; + credDefId: string; + credentialName: string; } export interface IProofRequestSearchCriteria { - pageNumber: number; - pageSize: number; - sortField: string; - sortBy: string; - search: string; - user?: IUserRequestInterface + pageNumber: number; + pageSize: number; + sortField: string; + sortBy: string; + search: string; + user?: IUserRequestInterface; } export interface IProofRequest { - metadata: object; - id: string; -} + metadata: object; + id: string; +} export interface IProofPresentation { - createdAt: string; - protocolVersion: string; - state: string; - connectionId: string; - threadId: string; - autoAcceptProof: string; - updatedAt: string; - isVerified: boolean; - } + createdAt: string; + protocolVersion: string; + state: string; + connectionId: string; + threadId: string; + autoAcceptProof: string; + updatedAt: string; + isVerified: boolean; +} -export interface IPresentation { - _tags: ITags; - metadata: object; - id: string; - createdAt: string; - protocolVersion: string; - state: string; - connectionId: string; - threadId: string; - autoAcceptProof: string; - updatedAt: string; - isVerified: boolean; - } +export interface IPresentation extends IProofPresentation { + _tags: ITags; + metadata: object; + id: string; +} interface ITags { - connectionId: string; - state: string; - threadId: string; -} + connectionId: string; + state: string; + threadId: string; +} export interface IProofFormats { - indy: IndyProof + indy: IndyProof; } interface IndyProof { - name: string; - version: string; - requested_attributes: IRequestedAttributes; - requested_predicates: IRequestedPredicates; + name: string; + version: string; + requested_attributes: IRequestedAttributes; + requested_predicates: IRequestedPredicates; } interface IRequestedAttributes { - [key: string]: IRequestedAttributesName; + [key: string]: IRequestedAttributesName; } interface IRequestedAttributesName { - name?: string; - names?: string; - restrictions: IRequestedRestriction[] + name?: string; + names?: string; + restrictions: IRequestedRestriction[]; } interface IRequestedPredicates { - [key: string]: IRequestedPredicatesName; + [key: string]: IRequestedPredicatesName; } interface IRequestedPredicatesName { - name: string; - restrictions: IRequestedRestriction[] + name: string; + restrictions: IRequestedRestriction[]; } interface IRequestedRestriction { - cred_def_id?: string; - schema_id?: string; - schema_issuer_did?: string; - schema_name?: string; - issuer_did?: string; - schema_version?: string; -} \ No newline at end of file + cred_def_id?: string; + schema_id?: string; + schema_issuer_did?: string; + schema_name?: string; + issuer_did?: string; + schema_version?: string; +} diff --git a/apps/api-gateway/src/x509/dtos/x509.dto.ts b/apps/api-gateway/src/x509/dtos/x509.dto.ts new file mode 100644 index 000000000..595f81d33 --- /dev/null +++ b/apps/api-gateway/src/x509/dtos/x509.dto.ts @@ -0,0 +1,382 @@ +import { ApiProperty, ApiPropertyOptional, getSchemaPath } from '@nestjs/swagger'; +import { + ArrayNotEmpty, + IsArray, + IsBoolean, + IsDate, + IsEnum, + IsNotEmpty, + IsNotEmptyObject, + IsNumber, + IsOptional, + IsString, + Max, + Min, + ValidateNested +} from 'class-validator'; +import { Transform, Type } from 'class-transformer'; +import { SortFields, X509ExtendedKeyUsage, X509KeyUsage, x5cKeyType, x5cRecordStatus } from '@credebl/enum/enum'; +import { IX509SearchCriteria } from '@credebl/common/interfaces/x509.interface'; +import { toNumber, trim } from '@credebl/common/cast.helper'; + +export class AuthorityAndSubjectKeyDto { + @ApiProperty({ + enum: x5cKeyType, + //default: x5cKeyType.P256.toString(), + description: 'Type of the key used for signing the X.509 Certificate (default is p256)' + }) + @IsOptional() + @IsEnum(x5cKeyType) + keyType: x5cKeyType = x5cKeyType.P256; +} + +export enum GeneralNameType { + DNS = 'dns', + DN = 'dn', + EMAIL = 'email', + GUID = 'guid', + IP = 'ip', + URL = 'url', + UPN = 'upn', + REGISTERED_ID = 'id' +} + +export class NameDto { + @ApiProperty({ + example: Object.keys(GeneralNameType), + enum: GeneralNameType + }) + @IsNotEmpty() + @IsEnum(GeneralNameType) + type: GeneralNameType; + + @ApiProperty() + @IsNotEmpty() + @IsString() + value: string; +} + +export class X509CertificateIssuerAndSubjectOptionsDto { + @ApiPropertyOptional() @IsOptional() @IsString() countryName?: string; + @ApiPropertyOptional() @IsOptional() @IsString() stateOrProvinceName?: string; + @ApiPropertyOptional() @IsOptional() @IsString() organizationalUnit?: string; + @ApiPropertyOptional() @IsOptional() @IsString() commonName?: string; +} + +class ValidityDto { + @ApiPropertyOptional() @IsOptional() @IsDate() @Type(() => Date) notBefore?: Date; + @ApiPropertyOptional() @IsOptional() @IsDate() @Type(() => Date) notAfter?: Date; +} + +export class KeyUsageDto { + @ApiProperty({ + enum: X509KeyUsage, + isArray: true, + example: Object.keys(X509KeyUsage) + }) + @IsArray() + @IsEnum(X509KeyUsage, { each: true }) + usages: X509KeyUsage[]; + + @ApiPropertyOptional() + @IsOptional() + @IsBoolean() + markAsCritical?: boolean; +} + +export class ExtendedKeyUsageDto { + @ApiProperty({ + enum: X509ExtendedKeyUsage, + isArray: true, + example: Object.keys(X509ExtendedKeyUsage) + }) + @IsArray() + @IsEnum(X509ExtendedKeyUsage, { each: true }) + usages: X509ExtendedKeyUsage[]; + + @ApiPropertyOptional() + @IsOptional() + @IsBoolean() + markAsCritical?: boolean; +} + +export class NameListDto { + @ApiProperty({ type: [NameDto] }) + @ArrayNotEmpty() + @IsArray() + @ValidateNested() + @Type(() => NameDto) + name: NameDto[]; + + @ApiPropertyOptional() + @IsOptional() + @IsBoolean() + markAsCritical?: boolean; +} + +export class AuthorityAndSubjectKeyIdentifierDto { + @ApiPropertyOptional() + @IsBoolean() + include: boolean; + + @ApiPropertyOptional() + @IsBoolean() + @IsOptional() + markAsCritical?: boolean; +} + +export class BasicConstraintsDto { + @ApiProperty() + @IsBoolean() + ca: boolean; + + @ApiPropertyOptional() + @IsNumber() + @IsOptional() + pathLenConstraint?: number; + + @ApiPropertyOptional() + @IsBoolean() + @IsOptional() + markAsCritical?: boolean; +} + +export class CrlDistributionPointsDto { + @ApiProperty({ type: [String] }) + @IsArray() + @IsString({ each: true }) + urls: string[]; + + @ApiPropertyOptional() + @IsBoolean() + @IsOptional() + markAsCritical?: boolean; +} + +export class X509CertificateExtensionsOptionsDto { + @ApiPropertyOptional({ type: KeyUsageDto }) + @IsOptional() + @ValidateNested() + @Type(() => KeyUsageDto) + keyUsage?: KeyUsageDto; + + @ApiPropertyOptional({ type: ExtendedKeyUsageDto }) + @IsOptional() + @ValidateNested() + @Type(() => ExtendedKeyUsageDto) + extendedKeyUsage?: ExtendedKeyUsageDto; + + @ApiPropertyOptional({ type: AuthorityAndSubjectKeyIdentifierDto }) + @IsOptional() + @ValidateNested() + @Type(() => AuthorityAndSubjectKeyIdentifierDto) + authorityKeyIdentifier?: AuthorityAndSubjectKeyIdentifierDto; + + @ApiPropertyOptional({ type: AuthorityAndSubjectKeyIdentifierDto }) + @IsOptional() + @ValidateNested() + @Type(() => AuthorityAndSubjectKeyIdentifierDto) + subjectKeyIdentifier?: AuthorityAndSubjectKeyIdentifierDto; + + @ApiPropertyOptional({ type: NameListDto }) + @IsOptional() + @ValidateNested() + @Type(() => NameListDto) + issuerAlternativeName?: NameListDto; + + @ApiPropertyOptional({ type: NameListDto }) + @IsOptional() + @ValidateNested() + @Type(() => NameListDto) + subjectAlternativeName?: NameListDto; + + @ApiPropertyOptional({ type: BasicConstraintsDto }) + @IsOptional() + @ValidateNested() + @Type(() => BasicConstraintsDto) + basicConstraints?: BasicConstraintsDto; + + @ApiPropertyOptional({ type: CrlDistributionPointsDto }) + @IsOptional() + @ValidateNested() + @Type(() => CrlDistributionPointsDto) + crlDistributionPoints?: CrlDistributionPointsDto; +} + +// Main DTO +//@ApiExtraModels(X509CertificateIssuerAndSubjectOptionsDto) +export class X509CreateCertificateOptionsDto { + @ApiPropertyOptional({ type: () => AuthorityAndSubjectKeyDto }) + @IsOptional() + @ValidateNested() + @Type(() => AuthorityAndSubjectKeyDto) + authorityKey?: AuthorityAndSubjectKeyDto; + + /** + * + * The key that is the subject of the X.509 Certificate + * + * If the `subjectPublicKey` is not included, the `authorityKey` will be used. + * This means that the certificate is self-signed + * + */ + @ApiPropertyOptional({ type: () => AuthorityAndSubjectKeyDto }) + @IsOptional() + @ValidateNested() + @Type(() => AuthorityAndSubjectKeyDto) + subjectPublicKey?: AuthorityAndSubjectKeyDto; + + @ApiPropertyOptional() + @IsOptional() + @IsString() + serialNumber?: string; + + @ApiProperty({ oneOf: [{ $ref: getSchemaPath(X509CertificateIssuerAndSubjectOptionsDto) }, { type: 'string' }] }) + // @ApiProperty({ type: X509CertificateIssuerAndSubjectOptionsDto }) + @ValidateNested() + @Type(() => X509CertificateIssuerAndSubjectOptionsDto) + issuer: X509CertificateIssuerAndSubjectOptionsDto | string; + + @ApiPropertyOptional({ + oneOf: [{ $ref: getSchemaPath(X509CertificateIssuerAndSubjectOptionsDto) }, { type: 'string' }] + }) + @IsOptional() + subject?: X509CertificateIssuerAndSubjectOptionsDto | string; + + @ApiPropertyOptional({ type: () => ValidityDto }) + @IsOptional() + @ValidateNested() + @Type(() => ValidityDto) + validity?: ValidityDto; + + @ApiPropertyOptional({ type: () => X509CertificateExtensionsOptionsDto }) + @IsOptional() + @ValidateNested() + @Type(() => X509CertificateExtensionsOptionsDto) + extensions?: X509CertificateExtensionsOptionsDto; +} + +export class X509ImportCertificateOptionsDto { + @ApiProperty({ + description: 'certificate', + required: true + }) + @IsString() + certificate: string; + + @ApiPropertyOptional({ + description: 'Private key in base64 string format' + }) + @IsOptional() + @IsString() + privateKey?: string; + + @ApiProperty({ + enum: x5cKeyType, + //default: x5cKeyType.P256.toString(), + description: 'Type of the key used for signing the X.509 Certificate (default is p256)' + }) + @IsOptional() + @IsEnum(x5cKeyType) + keyType: x5cKeyType = x5cKeyType.P256; +} + +export class x509Input { + @ApiProperty({ + description: 'certificate', + required: true + }) + @IsString() + certificate: string; +} + +export class X509CertificateSubjectOptionsDto { + @ApiProperty() @IsNotEmpty() @IsString() countryName: string; + // @ApiPropertyOptional() @IsOptional() @IsString() stateOrProvinceName?: string; + // @ApiPropertyOptional() @IsOptional() @IsString() organizationalUnit?: string; + @ApiProperty() @IsNotEmpty() @IsString() commonName: string; +} + +export class BasicX509CreateCertificateConfig { + @ApiProperty({ type: () => X509CertificateSubjectOptionsDto, required: true }) + @IsNotEmptyObject() + @ValidateNested() + @Type(() => X509CertificateSubjectOptionsDto) + subject: X509CertificateSubjectOptionsDto; + + @ApiPropertyOptional({ type: () => AuthorityAndSubjectKeyDto }) + @IsOptional() + @ValidateNested() + @Type(() => AuthorityAndSubjectKeyDto) + subjectKey?: AuthorityAndSubjectKeyDto; +} + +export interface X509GenericRecordContent { + dcs?: string | string[]; + root?: string; +} + +export interface X509GenericRecord { + id: string; + content?: X509GenericRecordContent; +} + +export class x509OptionsDto { + @ApiProperty({ example: 'exampleOrg' }) + @IsNotEmpty() + @IsString() + commonName: string; + + @ApiProperty({ example: 'IN' }) + @IsNotEmpty() + @IsString() + countryName: string; +} + +export class X509SearchCriteriaDto implements IX509SearchCriteria { + @ApiProperty({ required: false, example: '1' }) + @Transform(({ value }) => toNumber(value)) + @IsOptional() + pageNumber: number = 1; + + @ApiProperty({ required: false, example: '10' }) + @IsOptional() + @Transform(({ value }) => toNumber(value)) + @Min(1, { message: 'Page size must be greater than 0' }) + @Max(100, { message: 'Page size must be less than 100' }) + pageSize: number = 10; + + @ApiProperty({ required: false }) + @IsOptional() + @Transform(({ value }) => trim(value)) + @Type(() => String) + searchByText: string = ''; + + @ApiProperty({ + required: false + }) + @Transform(({ value }) => trim(value)) + @IsOptional() + @IsEnum(SortFields) + sortField: string = SortFields.CREATED_DATE_TIME; + + @ApiProperty({ + required: false, + enum: x5cKeyType, + enumName: 'keyType' + }) + @Transform(({ value }) => trim(value)) + @IsOptional() + @IsEnum(x5cKeyType) + keyType: x5cKeyType; + + @ApiProperty({ + required: false, + enum: x5cRecordStatus, + enumName: 'status' + }) + @Transform(({ value }) => trim(value)) + @IsOptional() + @IsEnum(x5cRecordStatus) + status: x5cRecordStatus; +} diff --git a/apps/api-gateway/src/x509/x509.controller.ts b/apps/api-gateway/src/x509/x509.controller.ts new file mode 100644 index 000000000..a572cdedd --- /dev/null +++ b/apps/api-gateway/src/x509/x509.controller.ts @@ -0,0 +1,275 @@ +import { + ApiBearerAuth, + ApiForbiddenResponse, + ApiOperation, + ApiQuery, + ApiResponse, + ApiTags, + ApiUnauthorizedResponse +} from '@nestjs/swagger'; +import { + Controller, + Get, + Put, + Param, + UseGuards, + UseFilters, + Post, + Body, + Res, + HttpStatus, + Query, + ParseUUIDPipe, + BadRequestException +} from '@nestjs/common'; + +import IResponse from '@credebl/common/interfaces/response.interface'; +import { Response } from 'express'; +import { ApiResponseDto } from '../dtos/apiResponse.dto'; +import { UnauthorizedErrorDto } from '../dtos/unauthorized-error.dto'; +import { ForbiddenErrorDto } from '../dtos/forbidden-error.dto'; +import { AuthGuard } from '@nestjs/passport'; +import { User } from '../authz/decorators/user.decorator'; +import { user } from '@prisma/client'; +import { ResponseMessages } from '@credebl/common/response-messages'; +import { OrgRolesGuard } from '../authz/guards/org-roles.guard'; +import { Roles } from '../authz/decorators/roles.decorator'; +import { OrgRoles } from 'libs/org-roles/enums'; +import { CustomExceptionFilter } from 'apps/api-gateway/common/exception-handler'; + +import { TrimStringParamPipe } from '@credebl/common/cast.helper'; +import { X509Service } from './x509.service'; +import { + X509CreateCertificateOptionsDto, + X509ImportCertificateOptionsDto, + X509SearchCriteriaDto +} from './dtos/x509.dto'; +import { SortFields, x5cKeyType, x5cRecordStatus } from '@credebl/enum/enum'; + +@UseFilters(CustomExceptionFilter) +@Controller('x509') +@ApiTags('x509') +@ApiUnauthorizedResponse({ description: 'Unauthorized', type: UnauthorizedErrorDto }) +@ApiForbiddenResponse({ description: 'Forbidden', type: ForbiddenErrorDto }) +export class X509Controller { + constructor(private readonly x509Service: X509Service) {} + + /** + * Create a new x509 + * @param createDto The details of the x509 to be created + * @returns Created x509 details + */ + @Post('/:orgId') + @ApiOperation({ + summary: 'Create a new X509', + description: 'Create a new x509 with the provided details.' + }) + @ApiResponse({ status: HttpStatus.CREATED, description: 'Success', type: ApiResponseDto }) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN) + @ApiBearerAuth() + async createX509( + @Param( + 'orgId', + TrimStringParamPipe, + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + } + }) + ) + orgId: string, + @Body() createDto: X509CreateCertificateOptionsDto, + @Res() res: Response, + @User() reqUser: user + ): Promise { + const record = await this.x509Service.createX509(orgId, createDto, reqUser); + const finalResponse: IResponse = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.x509.success.create, + data: record + }; + return res.status(HttpStatus.CREATED).json(finalResponse); + } + + @Put('/:orgId/activate/:id') + @ApiOperation({ + summary: 'Activate X509 certificate', + description: 'Activate X509 certificate' + }) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN) + @ApiBearerAuth() + async activateX509( + @Param( + 'orgId', + TrimStringParamPipe, + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + } + }) + ) + orgId: string, + @Param('id', ParseUUIDPipe) id: string, + @Res() res: Response, + @User() reqUser: user + ): Promise { + const record = await this.x509Service.activateX509(orgId, id, reqUser); + const finalResponse: IResponse = { + statusCode: HttpStatus.OK, + message: ResponseMessages.x509.success.activated, + data: record + }; + return res.status(HttpStatus.OK).json(finalResponse); + } + + @Put('/:orgId/deactivate/:id') + @ApiOperation({ + summary: 'Deactive X509 certificate', + description: 'Deactive X509 certificate' + }) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN) + @ApiBearerAuth() + async deActivateX509( + @Param( + 'orgId', + TrimStringParamPipe, + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + } + }) + ) + orgId: string, + @Param('id', ParseUUIDPipe) id: string, + @Res() res: Response, + @User() reqUser: user + ): Promise { + const record = await this.x509Service.deActivateX509(orgId, id, reqUser); + const finalResponse: IResponse = { + statusCode: HttpStatus.OK, + message: ResponseMessages.x509.success.deActivated, + data: record + }; + return res.status(HttpStatus.OK).json(finalResponse); + } + + @Get('/:orgId') + @ApiOperation({ + summary: 'Get all X509 certificate', + description: 'Get all X509 certificate' + }) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN) + @ApiBearerAuth() + @ApiQuery({ + name: 'keyType', + enum: x5cKeyType, + required: false + }) + @ApiQuery({ + name: 'status', + enum: x5cRecordStatus, + required: false + }) + @ApiQuery({ + name: 'sortField', + enum: SortFields, + required: false + }) + async getAllX509ByOrgId( + @Query() x509SearchCriteriaDto: X509SearchCriteriaDto, + @Param( + 'orgId', + TrimStringParamPipe, + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + } + }) + ) + orgId: string, + @Res() res: Response, + @User() reqUser: user + ): Promise { + const record = await this.x509Service.getX509CertificatesByOrgId(orgId, x509SearchCriteriaDto, reqUser); + const finalResponse: IResponse = { + statusCode: HttpStatus.OK, + message: ResponseMessages.x509.success.fetchAll, + data: record + }; + return res.status(HttpStatus.OK).json(finalResponse); + } + + @Get('/:orgId/:id') + @ApiOperation({ + summary: 'Get X509 certificate', + description: 'Get X509 certificate' + }) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN) + @ApiBearerAuth() + async getX509Certificate( + @Param( + 'orgId', + TrimStringParamPipe, + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + } + }) + ) + orgId: string, + @Param('id', ParseUUIDPipe) id: string, + @Res() res: Response, + @User() reqUser: user + ): Promise { + const record = await this.x509Service.getX509Certificate(orgId, id, reqUser); + const finalResponse: IResponse = { + statusCode: HttpStatus.OK, + message: ResponseMessages.x509.success.fetch, + data: record + }; + return res.status(HttpStatus.OK).json(finalResponse); + } + + /** + * Import a new x509 + * @param importDto The details of the x509 to be created + * @returns Imported x509 certificate + */ + @Post('/:orgId/import') + @ApiOperation({ + summary: 'Import a new X509', + description: 'Import a new x509 with the provided details.' + }) + @ApiResponse({ status: HttpStatus.CREATED, description: 'Success', type: ApiResponseDto }) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN) + @ApiBearerAuth() + async importX509( + @Param( + 'orgId', + TrimStringParamPipe, + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + } + }) + ) + orgId: string, + @Body() importDto: X509ImportCertificateOptionsDto, + @Res() res: Response, + @User() reqUser: user + ): Promise { + const record = await this.x509Service.importX509(orgId, importDto, reqUser); + const finalResponse: IResponse = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.x509.success.import, + data: record + }; + return res.status(HttpStatus.CREATED).json(finalResponse); + } +} diff --git a/apps/api-gateway/src/x509/x509.module.ts b/apps/api-gateway/src/x509/x509.module.ts new file mode 100644 index 000000000..abcb35365 --- /dev/null +++ b/apps/api-gateway/src/x509/x509.module.ts @@ -0,0 +1,26 @@ +import { ClientsModule, Transport } from '@nestjs/microservices'; +import { ConfigModule } from '@nestjs/config'; +import { HttpModule } from '@nestjs/axios'; +import { Module } from '@nestjs/common'; +import { X509Controller } from './x509.controller'; +import { X509Service } from './x509.service'; +import { getNatsOptions } from '@credebl/common/nats.config'; +import { AwsService } from '@credebl/aws'; +import { CommonConstants } from '@credebl/common/common.constant'; +import { NATSClient } from '@credebl/common/NATSClient'; +@Module({ + imports: [ + HttpModule, + ConfigModule.forRoot(), + ClientsModule.register([ + { + name: 'NATS_CLIENT', + transport: Transport.NATS, + options: getNatsOptions(CommonConstants.X509_SERVICE, process.env.API_GATEWAY_NKEY_SEED) + } + ]) + ], + controllers: [X509Controller], + providers: [X509Service, AwsService, NATSClient] +}) +export class X509Module {} diff --git a/apps/api-gateway/src/x509/x509.service.ts b/apps/api-gateway/src/x509/x509.service.ts new file mode 100644 index 000000000..e4ce19597 --- /dev/null +++ b/apps/api-gateway/src/x509/x509.service.ts @@ -0,0 +1,87 @@ +import { Inject } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; +import { ClientProxy } from '@nestjs/microservices'; +import { BaseService } from 'libs/service/base.service'; + +import { user } from '@prisma/client'; + +import { NATSClient } from '@credebl/common/NATSClient'; +import { + X509CreateCertificateOptionsDto, + X509ImportCertificateOptionsDto, + X509SearchCriteriaDto +} from './dtos/x509.dto'; +import { X509CertificateRecord } from '@credebl/common/interfaces/x509.interface'; + +@Injectable() +export class X509Service extends BaseService { + constructor( + @Inject('NATS_CLIENT') private readonly serviceProxy: ClientProxy, + private readonly natsClient: NATSClient + ) { + super('X509Service'); + } + + /** + * + * @param createDto + * @returns X509 creation Success + */ + async createX509( + orgId: string, + createDto: X509CreateCertificateOptionsDto, + reqUser: user + ): Promise { + this.logger.log(`Start creating x509 certficate`); + const payload = { options: createDto, user: reqUser, orgId }; + return this.natsClient.sendNatsMessage(this.serviceProxy, 'create-x509-certificate', payload); + } + + async activateX509(orgId: string, id: string, reqUser: user): Promise { + this.logger.log(`Start activating x509 certficate`); + this.logger.debug(`certificate Id : `, id); + const payload = { orgId, id, user: reqUser }; + return this.natsClient.sendNatsMessage(this.serviceProxy, 'activate-x509-certificate', payload); + } + + async deActivateX509(orgId: string, id: string, reqUser: user): Promise { + this.logger.log(`Start deactivating x509 certficate`); + this.logger.debug(`certificate Id : `, id); + const payload = { orgId, id, user: reqUser }; + return this.natsClient.sendNatsMessage(this.serviceProxy, 'deActivate-x509-certificate', payload); + } + + async getX509CertificatesByOrgId( + orgId: string, + x509SearchCriteriaDto: X509SearchCriteriaDto, + reqUser: user + ): Promise { + this.logger.log(`Start getting x509 certficate for org`); + this.logger.debug(`Filters applied : `, x509SearchCriteriaDto); + const payload = { orgId, options: x509SearchCriteriaDto, user: reqUser }; + return this.natsClient.sendNatsMessage(this.serviceProxy, 'get-all-certificates', payload); + } + + async getX509Certificate(orgId: string, id: string, reqUser: user): Promise { + this.logger.log(`Start getting x509 certficate by id`); + this.logger.debug(`certificate Id : `, id); + const payload = { id, orgId, user: reqUser }; + return this.natsClient.sendNatsMessage(this.serviceProxy, 'get-certificate', payload); + } + + /** + * + * @param importDto + * @returns X509 import Success + */ + async importX509( + orgId: string, + importDto: X509ImportCertificateOptionsDto, + reqUser: user + ): Promise { + this.logger.log(`Start importing x509 certficate by id`); + this.logger.debug(`certificate : `, importDto.certificate); + const payload = { orgId, options: importDto, user: reqUser }; + return this.natsClient.sendNatsMessage(this.serviceProxy, 'import-x509-certificate', payload); + } +} diff --git a/apps/cloud-wallet/src/cloud-wallet.service.ts b/apps/cloud-wallet/src/cloud-wallet.service.ts index 0a396251a..988ada3db 100644 --- a/apps/cloud-wallet/src/cloud-wallet.service.ts +++ b/apps/cloud-wallet/src/cloud-wallet.service.ts @@ -3,14 +3,11 @@ import { CommonService } from '@credebl/common'; import { BadRequestException, ConflictException, - Inject, Injectable, InternalServerErrorException, Logger, NotFoundException } from '@nestjs/common'; -import { Cache } from 'cache-manager'; -import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { IAcceptOffer, ICreateCloudWalletDid, @@ -40,17 +37,13 @@ import { CloudWalletRepository } from './cloud-wallet.repository'; import { ResponseMessages } from '@credebl/common/response-messages'; import { CloudWalletType } from '@credebl/enum/enum'; import { CommonConstants } from '@credebl/common/common.constant'; -import { ClientProxy } from '@nestjs/microservices'; @Injectable() export class CloudWalletService { constructor( private readonly commonService: CommonService, - @Inject('NATS_CLIENT') private readonly cloudWalletServiceProxy: ClientProxy, private readonly cloudWalletRepository: CloudWalletRepository, - private readonly logger: Logger, - // TODO: Remove duplicate, unused variable - @Inject(CACHE_MANAGER) private cacheService: Cache + private readonly logger: Logger ) {} /** diff --git a/apps/connection/src/connection.controller.ts b/apps/connection/src/connection.controller.ts index 0cc2e63d7..cb5c92fbd 100644 --- a/apps/connection/src/connection.controller.ts +++ b/apps/connection/src/connection.controller.ts @@ -46,15 +46,15 @@ export class ConnectionController { } @MessagePattern({ cmd: 'get-all-agent-connection-list' }) - async getConnectionListFromAgent(payload: GetAllConnections): Promise { - const {orgId, connectionSearchCriteria } = payload; + async getConnectionListFromAgent(payload: GetAllConnections): Promise { + const { orgId, connectionSearchCriteria } = payload; return this.connectionService.getAllConnectionListFromAgent(orgId, connectionSearchCriteria); } /** - * + * * @param connectionId - * @param orgId + * @param orgId * @returns connection details by connection Id */ @MessagePattern({ cmd: 'get-connection-details-by-connectionId' }) @@ -64,7 +64,7 @@ export class ConnectionController { } @MessagePattern({ cmd: 'get-connection-records' }) - async getConnectionRecordsByOrgId(payload: { orgId: string, userId: string }): Promise { + async getConnectionRecordsByOrgId(payload: { orgId: string; userId: string }): Promise { const { orgId } = payload; return this.connectionService.getConnectionRecords(orgId); } @@ -80,7 +80,7 @@ export class ConnectionController { const { user, receiveInvitation, orgId } = payload; return this.connectionService.receiveInvitation(user, receiveInvitation, orgId); } - + @MessagePattern({ cmd: 'send-question' }) async sendQuestion(payload: IQuestionPayload): Promise { return this.connectionService.sendQuestion(payload); @@ -97,13 +97,13 @@ export class ConnectionController { } @MessagePattern({ cmd: 'delete-connection-records' }) - async deleteConnectionRecords(payload: {orgId: string, userDetails: user}): Promise { + async deleteConnectionRecords(payload: { orgId: string; userDetails: user }): Promise { const { orgId, userDetails } = payload; return this.connectionService.deleteConnectionRecords(orgId, userDetails); } @MessagePattern({ cmd: 'send-basic-message-on-connection' }) - async sendBasicMessage(payload: {content: string, orgId: string, connectionId: string}): Promise { - return this.connectionService.sendBasicMesage(payload); + async sendBasicMessage(payload: { content: string; orgId: string; connectionId: string }): Promise { + return this.connectionService.sendBasicMessage(payload); } } diff --git a/apps/connection/src/connection.service.ts b/apps/connection/src/connection.service.ts index 8442c7421..0d45f2ae1 100644 --- a/apps/connection/src/connection.service.ts +++ b/apps/connection/src/connection.service.ts @@ -3,7 +3,6 @@ import { CommonService } from '@credebl/common'; import { CommonConstants } from '@credebl/common/common.constant'; import { HttpException, HttpStatus, Inject, Injectable, Logger, NotFoundException } from '@nestjs/common'; import { ClientProxy, RpcException } from '@nestjs/microservices'; -import { from, map } from 'rxjs'; import { ConnectionResponseDetail, AgentConnectionSearchCriteria, @@ -19,8 +18,6 @@ import { ConnectionRepository } from './connection.repository'; import { ResponseMessages } from '@credebl/common/response-messages'; import { IUserRequest } from '@credebl/user-request/user-request.interface'; import { ConnectionProcessState } from '@credebl/enum/enum'; -import { Cache } from 'cache-manager'; -import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { IConnectionList, ICreateConnectionUrl, @@ -41,8 +38,6 @@ export class ConnectionService { private readonly connectionRepository: ConnectionRepository, private readonly userActivityRepository: UserActivityRepository, private readonly logger: Logger, - // TODO: Remove unused variable - @Inject(CACHE_MANAGER) private readonly cacheService: Cache, private readonly natsClient: NATSClient ) {} @@ -57,7 +52,7 @@ export class ConnectionService { return saveConnectionDetails; } catch (error) { this.logger.error(`[getConnectionWebhook] - error in fetch connection webhook: ${error}`); - throw new RpcException(error.response ? error.response : error); + throw new RpcException(error?.response ?? error); } } @@ -66,19 +61,14 @@ export class ConnectionService { * @param orgId * @returns connection invitation URL */ - async _createConnectionInvitation( - connectionPayload: object, - url: string, - orgId: string - ): Promise<{ - response; - }> { + async _createConnectionInvitation(connectionPayload: object, url: string, orgId: string): Promise { //nats call in agent-service to create an invitation url const pattern = { cmd: 'agent-create-connection-legacy-invitation' }; const payload = { connectionPayload, url, orgId }; try { - return await this.natsCall(pattern, payload); + const result = await this.natsClient.send(this.connectionServiceProxy, pattern, payload); + return result; } catch (error) { this.logger.error(`catch: ${JSON.stringify(error)}`); throw new HttpException( @@ -107,7 +97,7 @@ export class ConnectionService { this.logger.error( `[getConnectionRecords ] [NATS call]- error in get connection records count : ${JSON.stringify(error)}` ); - throw new RpcException(error.response ? error.response : error); + throw new RpcException(error?.response ?? error); } } @@ -122,7 +112,7 @@ export class ConnectionService { return urlDetails.referenceId; } catch (error) { this.logger.error(`Error in get url in connection service: ${JSON.stringify(error)}`); - throw new RpcException(error.response ? error.response : error); + throw new RpcException(error?.response ?? error); } } @@ -178,14 +168,14 @@ export class ConnectionService { } catch (error) { this.logger.error(`[getConnections] [NATS call]- error in fetch connections details : ${JSON.stringify(error)}`); - throw new RpcException(error.response ? error.response : error); + throw new RpcException(error?.response ?? error); } } async getAllConnectionListFromAgent( orgId: string, connectionSearchCriteria: AgentConnectionSearchCriteria - ): Promise { + ): Promise { try { const { alias, myDid, outOfBandId, state, theirDid, theirLabel } = connectionSearchCriteria; const agentDetails = await this.connectionRepository.getAgentEndPoint(orgId); @@ -223,26 +213,22 @@ export class ConnectionService { } const connectionResponse = await this._getAllConnections(url, orgId); - return connectionResponse.response; + return connectionResponse; } catch (error) { this.logger.error( `[getConnectionsFromAgent] [NATS call]- error in fetch connections details : ${JSON.stringify(error)}` ); - throw new RpcException(error.response ? error.response : error); + throw new RpcException(error); } } - async _getAllConnections( - url: string, - orgId: string - ): Promise<{ - response: string; - }> { + async _getAllConnections(url: string, orgId: string): Promise { try { const pattern = { cmd: 'agent-get-all-connections' }; const payload = { url, orgId }; - return await this.natsCall(pattern, payload); + const result = await this.natsClient.send(this.connectionServiceProxy, pattern, payload); + return result; } catch (error) { this.logger.error( `[_getAllConnections] [NATS call]- error in fetch connections details : ${JSON.stringify(error)}` @@ -260,18 +246,18 @@ export class ConnectionService { } const url = `${agentEndPoint}${CommonConstants.URL_CONN_GET_CONNECTION_BY_ID}`.replace('#', connectionId); const createConnectionInvitation = await this._getConnectionsByConnectionId(url, orgId); - return createConnectionInvitation?.response; + return createConnectionInvitation; } catch (error) { this.logger.error(`[getConnectionsById] - error in get connections : ${JSON.stringify(error)}`); - if (error?.response?.error?.reason) { + if (error?.error?.reason) { throw new RpcException({ message: ResponseMessages.connection.error.connectionNotFound, - statusCode: error?.response?.status, - error: error?.response?.error?.reason + statusCode: error?.status, + error: error?.error?.reason }); } else { - throw new RpcException(error.response ? error.response : error); + throw new RpcException(error); } } } @@ -293,41 +279,24 @@ export class ConnectionService { } } - async _getConnectionsByConnectionId( - url: string, - orgId: string - ): Promise<{ - response; - }> { + async _getConnectionsByConnectionId(url: string, orgId: string): Promise { //nats call in agent service for fetch connection details const pattern = { cmd: 'agent-get-connection-details-by-connectionId' }; const payload = { url, orgId }; - return this.natsCall(pattern, payload); + return this.natsClient.send(this.connectionServiceProxy, pattern, payload); } async _getQuestionAnswersRecord(url: string, orgId: string): Promise { - const pattern = { cmd: 'agent-get-question-answer-record' }; - const payload = { url, orgId }; - return this.natsCall(pattern, payload); - } - - async _getOrgAgentApiKey(orgId: string): Promise<{ - response: string; - }> { - const pattern = { cmd: 'get-org-agent-api-key' }; - const payload = { orgId }; - try { - return await this.natsCall(pattern, payload); + const pattern = { cmd: 'agent-get-question-answer-record' }; + const payload = { url, orgId }; + const result = await this.natsClient.send(this.connectionServiceProxy, pattern, payload); + return result; } catch (error) { - this.logger.error(`catch: ${JSON.stringify(error)}`); - throw new HttpException( - { - status: error.status, - error: error.message - }, - error.status + this.logger.error( + `[_getQuestionAnswersRecord ] [NATS call]- error in get question and answer records : ${JSON.stringify(error)}` ); + throw error; } } @@ -344,7 +313,7 @@ export class ConnectionService { } const url = `${agentEndPoint}${CommonConstants.URL_RECEIVE_INVITATION_URL}`; const createConnectionInvitation = await this._receiveInvitationUrl(url, orgId, receiveInvitationUrl); - return createConnectionInvitation.response; + return createConnectionInvitation; } catch (error) { this.logger.error(`[receiveInvitationUrl] - error in receive invitation url : ${JSON.stringify(error, null, 2)}`); @@ -355,14 +324,14 @@ export class ConnectionService { message: customErrorMessage, error: ResponseMessages.errorMessages.conflict }); - } else if (error?.response?.error?.reason) { + } else if (error?.error?.reason) { throw new RpcException({ message: ResponseMessages.connection.error.connectionNotFound, - statusCode: error?.response?.status, - error: error?.response?.error?.reason + statusCode: error?.status, + error: error?.error?.reason }); } else { - throw new RpcException(error.response ? error.response : error); + throw new RpcException(error?.response ?? error); } } } @@ -371,14 +340,17 @@ export class ConnectionService { url: string, orgId: string, receiveInvitationUrl: IReceiveInvitationUrl - ): Promise<{ - response; - }> { + ): Promise { const pattern = { cmd: 'agent-receive-invitation-url' }; const payload = { url, orgId, receiveInvitationUrl }; try { - return await this.natsCall(pattern, payload); + const result = await this.natsClient.send( + this.connectionServiceProxy, + pattern, + payload + ); + return result; } catch (error) { this.logger.error(`catch: ${JSON.stringify(error)}`); throw new HttpException( @@ -404,18 +376,18 @@ export class ConnectionService { } const url = `${agentEndPoint}${CommonConstants.URL_RECEIVE_INVITATION}`; const createConnectionInvitation = await this._receiveInvitation(url, orgId, receiveInvitation); - return createConnectionInvitation?.response; + return createConnectionInvitation; } catch (error) { this.logger.error(`[receiveInvitation] - error in receive invitation : ${JSON.stringify(error)}`); - if (error?.response?.error?.reason) { + if (error?.error?.reason) { throw new RpcException({ message: ResponseMessages.connection.error.connectionNotFound, - statusCode: error?.response?.status, - error: error?.response?.error?.reason + statusCode: error?.status, + error: error?.error?.reason }); } else { - throw new RpcException(error.response ? error.response : error); + throw new RpcException(error?.response ?? error); } } } @@ -424,18 +396,16 @@ export class ConnectionService { url: string, orgId: string, receiveInvitation: IReceiveInvitation - ): Promise<{ - response; - }> { + ): Promise { const pattern = { cmd: 'agent-receive-invitation' }; const payload = { url, orgId, receiveInvitation }; - return this.natsCall(pattern, payload); + return this.natsClient.send(this.connectionServiceProxy, pattern, payload); } async _sendQuestion(questionPayload: IQuestionPayload, url: string, orgId: string): Promise { const pattern = { cmd: 'agent-send-question' }; const payload = { questionPayload, url, orgId }; - return this.natsCall(pattern, payload); + return this.natsClient.send(this.connectionServiceProxy, pattern, payload); } async sendQuestion(payload: IQuestionPayload): Promise { @@ -467,7 +437,7 @@ export class ConnectionService { statusCode: error.status?.code ?? HttpStatus.INTERNAL_SERVER_ERROR }); } - throw new RpcException(error.response || error); + throw new RpcException(error?.response ?? error); } } @@ -478,8 +448,8 @@ export class ConnectionService { const payload = { persistent, storeObj }; try { - const message = await this.natsCall(pattern, payload); - return message.response; + const message = await this.natsClient.send(this.connectionServiceProxy, pattern, payload); + return message; } catch (error) { this.logger.error(`catch: ${JSON.stringify(error)}`); throw new HttpException( @@ -561,13 +531,13 @@ export class ConnectionService { }; const url = await getAgentUrl(agentEndPoint, CommonConstants.CONNECTION_INVITATION); const createConnectionInvitation = await this._createOutOfBandConnectionInvitation(connectionPayload, url, orgId); - const connectionInvitationUrl = createConnectionInvitation?.response?.invitationUrl; + const connectionInvitationUrl = createConnectionInvitation?.invitationUrl; const shortenedUrl = await this.storeConnectionObjectAndReturnUrl( connectionInvitationUrl, connectionPayload.multiUseInvitation ); - const invitationsDid = createConnectionInvitation?.response?.invitationDid || invitationDid; + const invitationsDid = createConnectionInvitation?.invitationDid || invitationDid; const saveConnectionDetails = await this.connectionRepository.saveAgentConnectionInvitations( shortenedUrl, agentId, @@ -584,7 +554,7 @@ export class ConnectionService { createdBy: saveConnectionDetails.createdBy, lastChangedDateTime: saveConnectionDetails.lastChangedDateTime, lastChangedBy: saveConnectionDetails.lastChangedBy, - recordId: createConnectionInvitation.response.outOfBandRecord.id, + recordId: createConnectionInvitation.outOfBandRecord.id, invitationDid: saveConnectionDetails.invitationDid }; return connectionStorePayload; @@ -603,15 +573,15 @@ export class ConnectionService { connectionPayload: ICreateConnectionInvitation, url: string, orgId: string - ): Promise<{ - response; - }> { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ): Promise { //nats call in agent-service to create an invitation url const pattern = { cmd: 'agent-create-connection-invitation' }; const payload = { connectionPayload, url, orgId }; try { - return await this.natsCall(pattern, payload); + const result = await this.natsClient.send(this.connectionServiceProxy, pattern, payload); + return result; } catch (error) { this.logger.error(`catch: ${JSON.stringify(error)}`); throw new HttpException( @@ -624,36 +594,6 @@ export class ConnectionService { } } - async natsCall( - pattern: object, - payload: object - ): Promise<{ - response: string; - }> { - try { - return from(this.natsClient.send(this.connectionServiceProxy, pattern, payload)) - .pipe( - map((response) => ({ - response - })) - ) - .toPromise() - .catch((error) => { - this.logger.error(`catch: ${JSON.stringify(error)}`); - throw new HttpException( - { - status: error.statusCode, - error: error.message - }, - error.error - ); - }); - } catch (error) { - this.logger.error(`[ConnectionService natsCall] - error in nats call : ${JSON.stringify(error)}`); - throw error; - } - } - handleError(error): Promise { if (error?.status?.message?.error) { throw new RpcException({ @@ -661,7 +601,7 @@ export class ConnectionService { statusCode: error.status?.code ?? HttpStatus.INTERNAL_SERVER_ERROR }); } - throw new RpcException(error.response || error); + throw new RpcException(error?.response ?? error); } async deleteConnectionRecords(orgId: string, user: user): Promise { @@ -703,11 +643,11 @@ export class ConnectionService { return deleteConnections; } catch (error) { this.logger.error(`[deleteConnectionRecords] - error in deleting connection records: ${JSON.stringify(error)}`); - throw new RpcException(error.response ? error.response : error); + throw new RpcException(error?.response ?? error); } } - async sendBasicMesage(payload: IBasicMessage): Promise { + async sendBasicMessage(payload: IBasicMessage): Promise { const { content, orgId, connectionId } = payload; try { const agentDetails = await this.connectionRepository.getAgentEndPoint(orgId); @@ -733,14 +673,13 @@ export class ConnectionService { statusCode: error.status?.code ?? HttpStatus.INTERNAL_SERVER_ERROR }); } - throw new RpcException(error.response || error); + throw new RpcException(error?.response ?? error); } } async _sendBasicMessageToAgent(content: IBasicMessage, url: string, orgId: string): Promise { const pattern = { cmd: 'agent-send-basic-message' }; const payload = { content, url, orgId }; - // eslint-disable-next-line no-return-await - return await this.natsCall(pattern, payload); + return this.natsClient.send(this.connectionServiceProxy, pattern, payload); } } diff --git a/apps/connection/src/interfaces/connection.interfaces.ts b/apps/connection/src/interfaces/connection.interfaces.ts index 020e4821d..dad9de317 100644 --- a/apps/connection/src/interfaces/connection.interfaces.ts +++ b/apps/connection/src/interfaces/connection.interfaces.ts @@ -16,7 +16,7 @@ export interface IConnection { handshakeProtocols: string[]; orgId: string; recipientKey?: string; - invitationDid?: string + invitationDid?: string; } export interface IUserRequestInterface { userId: string; @@ -130,7 +130,7 @@ export interface IConnectionSearchCriteria { sortField: string; sortBy: string; searchByText: string; - user: IUserRequestInterface + user: IUserRequestInterface; } export interface AgentConnectionSearchCriteria { @@ -143,9 +143,9 @@ export interface AgentConnectionSearchCriteria { } export interface IReceiveInvitationByUrlOrg { - user: IUserRequestInterface, - receiveInvitationUrl: IReceiveInvitationUrl, - orgId: string + user: IUserRequestInterface; + receiveInvitationUrl: IReceiveInvitationUrl; + orgId: string; } export interface IReceiveInvitationUrl extends IReceiveInvite { @@ -153,9 +153,9 @@ export interface IReceiveInvitationUrl extends IReceiveInvite { } export interface IReceiveInvitationByOrg { - user: IUserRequestInterface, - receiveInvitation: IReceiveInvitation, - orgId: string + user: IUserRequestInterface; + receiveInvitation: IReceiveInvitation; + orgId: string; } interface Service { @@ -210,8 +210,8 @@ interface OutOfBandInvitationService { } interface OutOfBandInvitation { - "@type": string; - "@id": string; + '@type': string; + '@id': string; label: string; accept: string[]; handshake_protocols: string[]; @@ -266,7 +266,7 @@ export interface ConnectionResponseDetail { lastChangedDateTime: Date; lastChangedBy: number; recordId: string; - invitationDid?: string + invitationDid?: string; } export interface ICreateConnectionInvitation { @@ -289,6 +289,6 @@ export interface ICreateConnectionInvitation { } export interface ICreateOutOfbandConnectionInvitation { - user: IUserRequestInterface, - createOutOfBandConnectionInvitation: ICreateConnectionInvitation, -} \ No newline at end of file + user: IUserRequestInterface; + createOutOfBandConnectionInvitation: ICreateConnectionInvitation; +} diff --git a/apps/issuance/constant/issuance.ts b/apps/issuance/constant/issuance.ts new file mode 100644 index 000000000..d32498997 --- /dev/null +++ b/apps/issuance/constant/issuance.ts @@ -0,0 +1,6 @@ +import { AccessTokenSignerKeyType } from '../interfaces/oid4vc-issuance.interfaces'; + +export const dpopSigningAlgValuesSupported = ['RS256', 'ES256', 'EdDSA']; +export const credentialConfigurationsSupported = {}; +export const accessTokenSignerKeyType = 'ed25519' as AccessTokenSignerKeyType; +export const batchCredentialIssuanceDefault = 0; diff --git a/apps/issuance/src/issuance.controller.ts b/apps/issuance/src/issuance.controller.ts index b1bbf6bcc..abc653b19 100644 --- a/apps/issuance/src/issuance.controller.ts +++ b/apps/issuance/src/issuance.controller.ts @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/naming-convention, @typescript-eslint/explicit-function-return-type, @typescript-eslint/explicit-module-boundary-types, camelcase */ +// TODO: Remove this import { IClientDetails, IIssuance, diff --git a/apps/issuance/src/issuance.service.ts b/apps/issuance/src/issuance.service.ts index 33b630113..a247c943f 100644 --- a/apps/issuance/src/issuance.service.ts +++ b/apps/issuance/src/issuance.service.ts @@ -1516,7 +1516,7 @@ export class IssuanceService { return schemaDetails; } - async delay(ms): Promise { + async delay(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } diff --git a/apps/ledger/src/schema/schema.service.ts b/apps/ledger/src/schema/schema.service.ts index 276c840ed..d4b4f5f31 100644 --- a/apps/ledger/src/schema/schema.service.ts +++ b/apps/ledger/src/schema/schema.service.ts @@ -402,6 +402,7 @@ export class SchemaService extends BaseService { }; const schemaResponse = await from(this.natsClient.send(this.schemaServiceProxy, pattern, payload)) .pipe( + // TODO: remove nested mapping map((response) => ({ response })) @@ -429,6 +430,7 @@ export class SchemaService extends BaseService { }; const W3CSchemaResponse = await from(this.natsClient.send(this.schemaServiceProxy, natsPattern, payload)) .pipe( + // TODO: remove nested mapping map((response) => ({ response })) @@ -553,6 +555,7 @@ export class SchemaService extends BaseService { }; const schemaResponse = await from(this.natsClient.send(this.schemaServiceProxy, pattern, payload)) .pipe( + // TODO: remove nested mapping map((response) => ({ response })) diff --git a/apps/oid4vc-issuance/constant/issuance.ts b/apps/oid4vc-issuance/constant/issuance.ts new file mode 100644 index 000000000..d32498997 --- /dev/null +++ b/apps/oid4vc-issuance/constant/issuance.ts @@ -0,0 +1,6 @@ +import { AccessTokenSignerKeyType } from '../interfaces/oid4vc-issuance.interfaces'; + +export const dpopSigningAlgValuesSupported = ['RS256', 'ES256', 'EdDSA']; +export const credentialConfigurationsSupported = {}; +export const accessTokenSignerKeyType = 'ed25519' as AccessTokenSignerKeyType; +export const batchCredentialIssuanceDefault = 0; diff --git a/apps/oid4vc-issuance/interfaces/oid4vc-issuance.interfaces.ts b/apps/oid4vc-issuance/interfaces/oid4vc-issuance.interfaces.ts new file mode 100644 index 000000000..e3ba6413e --- /dev/null +++ b/apps/oid4vc-issuance/interfaces/oid4vc-issuance.interfaces.ts @@ -0,0 +1,151 @@ +import { organisation } from '@prisma/client'; +import { Claim } from './oid4vc-template.interfaces'; + +export interface OrgAgent { + organisation: organisation; + id: string; + createDateTime: Date; + createdBy: string; + lastChangedDateTime: Date; + lastChangedBy: string; + orgDid: string; + verkey: string; + agentEndPoint: string; + agentId: string; + isDidPublic: boolean; + ledgerId: string; + orgAgentTypeId: string; + tenantId: string; +} + +export interface Logo { + uri: string; + alt_text: string; +} + +export interface Display { + locale: string; + name: string; + description?: string; + logo?: Logo; +} + +export interface CredentialConfiguration { + format: string; + vct?: string; + doctype?: string; + scope: string; + claims: Claim[]; + credential_signing_alg_values_supported: string[]; + cryptographic_binding_methods_supported: string[]; + display: Display[]; +} + +export interface ClientAuthentication { + clientId: string; + clientSecret: string; +} + +export interface AuthorizationServerConfig { + issuer: string; + clientAuthentication: ClientAuthentication; +} + +export interface IssuerCreation { + authorizationServerUrl: string; + issuerId: string; + accessTokenSignerKeyType?: AccessTokenSignerKeyType; + display: Display[]; + dpopSigningAlgValuesSupported?: string[]; + authorizationServerConfigs: AuthorizationServerConfig; + batchCredentialIssuanceSize: number; +} + +export interface IssuerInitialConfig { + issuerId: string; + // eslint-disable-next-line @typescript-eslint/ban-types + display: Display[] | {}; + // eslint-disable-next-line @typescript-eslint/ban-types + authorizationServerConfigs: AuthorizationServerConfig | {}; + accessTokenSignerKeyType: AccessTokenSignerKeyType; + dpopSigningAlgValuesSupported: string[]; + batchCredentialIssuance?: object; + credentialConfigurationsSupported: object; +} + +export interface IssuerMetadata { + authorizationServerUrl: string; + publicIssuerId: string; + createdById: string; + orgAgentId: string; + batchCredentialIssuanceSize?: number; +} + +export interface initialIssuerDetails { + metadata: Display[]; + publicIssuerId: string; +} + +export enum AccessTokenSignerKeyType { + ED25519 = 'ed25519' +} + +export interface IssuerUpdation { + issuerId: string; + accessTokenSignerKeyType: AccessTokenSignerKeyType; + display; + batchCredentialIssuanceSize?: number; +} + +export interface IAgentNatsPayload { + url: string; + apiKey?: string; + orgId?: string; +} + +export type IAgentOIDCIssuerCreate = IAgentNatsPayload & { issuerCreation: IssuerCreation }; + +export interface TagMap { + [key: string]: string; +} + +export interface Logo { + uri: string; + alt_text: string; +} + +export interface DisplayInfo { + logo?: Logo; + name: string; + locale: string; + description: string; +} + +export interface ClientAuthentication { + clientId: string; + clientSecret: string; +} + +export interface AuthorizationServerConfig { + issuer: string; + clientAuthentication: ClientAuthentication; +} + +export interface BatchCredentialIssuance { + batchSize: number; +} + +export interface IssuerResponse { + _tags: TagMap; + metadata: Record; + id: string; + createdAt: string; + issuerId: string; + accessTokenPublicKeyFingerprint: string; + credentialConfigurationsSupported?: Record; + dpopSigningAlgValuesSupported: string[]; + display?: DisplayInfo[]; + authorizationServerConfigs?: AuthorizationServerConfig[]; + batchCredentialIssuance: BatchCredentialIssuance; + updatedAt: string; +} diff --git a/apps/oid4vc-issuance/interfaces/oid4vc-issuer-sessions.interfaces.ts b/apps/oid4vc-issuance/interfaces/oid4vc-issuer-sessions.interfaces.ts new file mode 100644 index 000000000..9a266a8ee --- /dev/null +++ b/apps/oid4vc-issuance/interfaces/oid4vc-issuer-sessions.interfaces.ts @@ -0,0 +1,63 @@ +import { OpenId4VcIssuanceSessionState } from '@credebl/enum/enum'; + +/* --------------------------------------------------------- + * Enums + * --------------------------------------------------------- */ +export enum CredentialFormat { + SdJwtVc = 'vc+sd-jwt', + MsoMdoc = 'mso_mdoc' +} + +export enum SignerMethodOption { + DID = 'did', + X5C = 'x5c' +} + +export interface SignerOption { + method: SignerMethodOption; + did?: string; + x5c?: string[]; +} + +export enum AuthenticationType { + PRE_AUTHORIZED_CODE = 'pre-authorized_code', + AUTHORIZATION_CODE = 'authorization_code' +} + +/* --------------------------------------------------------- + * Interfaces + * --------------------------------------------------------- */ +export type DisclosureFrame = Record>; + +export interface CredentialPayload { + [key: string]: unknown; // extensible for mDoc or other formats +} + +export interface CredentialRequest { + templateId: string; + payload: CredentialPayload; + validityInfo?: { + validFrom: Date; + validUntil: Date; + }; +} + +export interface CreateOidcCredentialOffer { + // e.g. "abc-gov" + authenticationType: AuthenticationType; // only option selector + credentials: CredentialRequest[]; // one or more credentials +} + +export interface GetAllCredentialOffer { + publicIssuerId?: string; + preAuthorizedCode?: string; + state?: OpenId4VcIssuanceSessionState; + credentialOfferUri?: string; + authorizationCode?: string; +} + +export interface UpdateCredentialRequest { + issuerId: string; + credentialOfferId: string; + issuerMetadata: object; +} diff --git a/apps/oid4vc-issuance/interfaces/oid4vc-template.interfaces.ts b/apps/oid4vc-issuance/interfaces/oid4vc-template.interfaces.ts new file mode 100644 index 000000000..2b33c19e1 --- /dev/null +++ b/apps/oid4vc-issuance/interfaces/oid4vc-template.interfaces.ts @@ -0,0 +1,47 @@ +import { Prisma, SignerOption } from '@prisma/client'; +import { AttributeType, CredentialFormat } from '@credebl/enum/enum'; +export interface SdJwtTemplate { + vct: string; + attributes: CredentialAttribute[]; +} + +export interface MdocTemplate { + doctype: string; + namespaces: { + namespace: string; + attributes: CredentialAttribute[]; + }[]; +} + +export interface CreateCredentialTemplate { + name: string; + description?: string; + signerOption?: SignerOption; + format: CredentialFormat; + canBeRevoked: boolean; + appearance?: Prisma.JsonValue; + issuerId: string; + template: SdJwtTemplate | MdocTemplate; +} + +export interface UpdateCredentialTemplate extends Partial {} + +export interface ClaimDisplay { + name: string; + locale?: string; +} + +export interface Claim { + path?: string[]; + display?: ClaimDisplay[]; + mandatory?: boolean; +} + +export interface CredentialAttribute { + key: string; + mandatory?: boolean; + value_type: AttributeType; + disclose?: boolean; + children?: CredentialAttribute[]; + display?: ClaimDisplay[]; +} diff --git a/apps/oid4vc-issuance/interfaces/oid4vc-wh-interfaces.ts b/apps/oid4vc-issuance/interfaces/oid4vc-wh-interfaces.ts new file mode 100644 index 000000000..2c0033b2f --- /dev/null +++ b/apps/oid4vc-issuance/interfaces/oid4vc-wh-interfaces.ts @@ -0,0 +1,48 @@ +export interface Oid4vcCredentialOfferWebhookPayload { + oidcIssueCredentialDto: Oid4vcCredentialOfferWebhookDto; + id: string; +} + +export interface Oid4vcCredentialOfferWebhookDto { + id: string; + credentialOfferId?: string; + issuedCredentials?: Record[]; + createdAt?: string; + updatedAt?: string; + credentialOfferPayload?: { + credential_configuration_ids?: string[]; + }; + state?: string; + contextCorrelationId?: string; + issuerId?: string; +} + +export interface CredentialPayload { + orgId: string; + schemaId?: string; + connectionId?: string; + credDefid?: string; + threadId: string; + createdBy: string; + lastChangedBy: string; + state: string; + credentialExchangeId: string; +} + +export interface OidcIssueCredentialPayload { + oidcIssueCredentialDto: { + id: string; + credentialOfferId?: string; + state?: string; + contextCorrelationId?: string; + credentialConfigurationIds?: string[]; + issuedCredentials?: string[]; + issuerId?: string; + credentialOfferPayload?: { + credential_issuer?: string; + credential_configuration_ids?: string[]; + grants?: Record; + credentials?: string[]; + }; + }; +} diff --git a/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts b/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts new file mode 100644 index 000000000..dbe75ad97 --- /dev/null +++ b/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts @@ -0,0 +1,484 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/naming-convention, @typescript-eslint/explicit-function-return-type, @typescript-eslint/explicit-module-boundary-types, camelcase */ +import { credential_templates } from '@prisma/client'; +import { GetAllCredentialOffer, SignerOption } from '../../interfaces/oid4vc-issuer-sessions.interfaces'; +import { CredentialFormat } from '@credebl/enum/enum'; +import { + CredentialAttribute, + MdocTemplate, + SdJwtTemplate +} from 'apps/oid4vc-issuance/interfaces/oid4vc-template.interfaces'; +import { UnprocessableEntityException } from '@nestjs/common'; +import { ResponseMessages } from '@credebl/common/response-messages'; +import { X509CertificateRecord } from '@credebl/common/interfaces/x509.interface'; +import { dateToSeconds } from '@credebl/common/date-only'; + +/* ============================================================================ + Domain Types +============================================================================ */ + +// type ValueType = 'string' | 'date' | 'number' | 'boolean' | 'integer' | string; + +// interface TemplateAttribute { +// display?: { name: string; locale: string }[]; +// mandatory?: boolean; +// value_type?: ValueType; +// } +// type TemplateAttributes = Record; + +export enum SignerMethodOption { + DID = 'did', + X5C = 'x5c' +} + +export type DisclosureFrame = Record>; + +export interface validityInfo { + validFrom: Date; + validUntil: Date; +} + +export interface CredentialRequestDtoLike { + templateId: string; + payload: Record; + validityInfo?: validityInfo; + // disclosureFrame?: DisclosureFrame; +} + +export interface CreateOidcCredentialOfferDtoLike { + credentials: CredentialRequestDtoLike[]; + preAuthorizedCodeFlowConfig?: { + txCode: { description?: string; length: number; input_mode: 'numeric' | 'text' | 'alphanumeric' }; + authorizationServerUrl: string; + }; + authorizationCodeFlowConfig?: { + authorizationServerUrl: string; + }; + publicIssuerId?: string; +} + +export interface ResolvedSignerOption { + method: 'did' | 'x5c'; + did?: string; + x5c?: string[]; +} + +/* ============================================================================ + Strong return types +============================================================================ */ + +export interface BuiltCredential { + credentialSupportedId: string; + signerOptions?: ResolvedSignerOption; + format: CredentialFormat; + payload: Record; + disclosureFrame?: DisclosureFrame; +} + +export interface BuiltCredentialOfferBase { + signerOption?: ResolvedSignerOption; + credentials: BuiltCredential[]; + publicIssuerId?: string; +} + +export type CredentialOfferPayload = BuiltCredentialOfferBase & + ( + | { + preAuthorizedCodeFlowConfig: { + txCode: { description?: string; length: number; input_mode: 'numeric' | 'text' | 'alphanumeric' }; + authorizationServerUrl: string; + }; + authorizationCodeFlowConfig?: never; + } + | { + authorizationCodeFlowConfig: { + authorizationServerUrl: string; + }; + preAuthorizedCodeFlowConfig?: never; + } + ); + +/* ============================================================================ + Constants +============================================================================ */ + +/** + * Default txCode constant requested (used for pre-authorized flow). + * The user requested this as a constant to be used by the builder. + */ +export const DEFAULT_TXCODE = { + description: 'test abc', + length: 4, + input_mode: 'numeric' as const +}; + +/* ============================================================================ + Small Utilities +============================================================================ */ + +// const isNil = (value: unknown): value is null | undefined => null == value; +// const isEmptyString = (value: unknown): boolean => 'string' === typeof value && '' === value.trim(); +// const isPlainRecord = (value: unknown): value is Record => +// Boolean(value) && 'object' === typeof value && !Array.isArray(value); + +/** Map DB format string -> API enum */ +function mapDbFormatToApiFormat(dbFormat: string): CredentialFormat { + const normalized = (dbFormat ?? '').toLowerCase(); + if (['sd-jwt', 'vc+sd-jwt', 'sdjwt', 'sd+jwt-vc'].includes(normalized)) { + return CredentialFormat.SdJwtVc; + } + if ('mso_mdoc' === normalized || 'mso-mdoc' === normalized || 'mdoc' === normalized) { + return CredentialFormat.Mdoc; + } + throw new Error(`Unsupported template format: ${dbFormat}`); +} + +function formatSuffix(apiFormat: CredentialFormat): 'sdjwt' | 'mdoc' { + return apiFormat === CredentialFormat.SdJwtVc ? 'sdjwt' : 'mdoc'; +} + +export function buildCredentialOfferUrl(baseUrl: string, getAllCredentialOffer: GetAllCredentialOffer): string { + const criteriaParams: string[] = []; + + if (getAllCredentialOffer.publicIssuerId) { + criteriaParams.push(`publicIssuerId=${encodeURIComponent(getAllCredentialOffer.publicIssuerId)}`); + } + + if (getAllCredentialOffer.preAuthorizedCode) { + criteriaParams.push(`preAuthorizedCode=${encodeURIComponent(getAllCredentialOffer.preAuthorizedCode)}`); + } + + if (getAllCredentialOffer.state) { + criteriaParams.push(`state=${encodeURIComponent(getAllCredentialOffer.state)}`); + } + + if (getAllCredentialOffer.credentialOfferUri) { + criteriaParams.push(`credentialOfferUri=${encodeURIComponent(getAllCredentialOffer.credentialOfferUri)}`); + } + + if (getAllCredentialOffer.authorizationCode) { + criteriaParams.push(`authorizationCode=${encodeURIComponent(getAllCredentialOffer.authorizationCode)}`); + } + + // Append query string if any params exist + return 0 < criteriaParams.length ? `${baseUrl}?${criteriaParams.join('&')}` : baseUrl; +} + +export function validatePayloadAgainstTemplate(template: any, payload: any): { valid: boolean; errors: string[] } { + const errors: string[] = []; + + const validateAttributes = (attributes: CredentialAttribute[], data: any, path = '') => { + for (const attr of attributes) { + const currentPath = path ? `${path}.${attr.key}` : attr.key; + const value = data?.[attr.key]; + + // Check for missing mandatory value + const isEmpty = + value === undefined || + null === value || + ('string' === typeof value && '' === value.trim()) || + ('object' === typeof value && !Array.isArray(value) && 0 === Object.keys(value).length); + + if (attr.mandatory && isEmpty) { + errors.push(`Missing mandatory attribute: ${currentPath}`); + } + + // Recurse for nested attributes + if (attr.children && 'object' === typeof value && null !== value) { + validateAttributes(attr.children, value, currentPath); + } + } + }; + + if (CredentialFormat.SdJwtVc === template.format) { + validateAttributes((template.attributes as SdJwtTemplate).attributes ?? [], payload); + } else if (CredentialFormat.Mdoc === template.format) { + const namespaces = payload?.namespaces; + if (!namespaces) { + errors.push('Missing namespaces object in mdoc payload.'); + } else { + const templateNamespaces = (template.attributes as MdocTemplate).namespaces; + for (const ns of templateNamespaces ?? []) { + const nsData = namespaces[ns.namespace]; + if (!nsData) { + errors.push(`Missing namespace: ${ns.namespace}`); + continue; + } + validateAttributes(ns.attributes, nsData, ns.namespace); + } + } + } + + return { valid: 0 === errors.length, errors }; +} + +function buildDisclosureFrameFromTemplate(template: { attributes: CredentialAttribute[] }) { + const disclosureFrame: DisclosureFrame = {}; + + const buildFrame = (attributes: CredentialAttribute[]) => { + const frame: Record = {}; + + for (const attr of attributes) { + if (attr.children?.length) { + // Handle nested attributes recursively + const subFrame = buildFrame(attr.children); + // Include parent only if disclose is true or it has children with disclosure + if (attr.disclose || 0 < Object.keys(subFrame).length) { + frame[attr.key] = subFrame; + } + } else if (attr.disclose !== undefined) { + frame[attr.key] = Boolean(attr.disclose); + } + } + + return frame; + }; + + Object.assign(disclosureFrame, buildFrame(template.attributes)); + + return disclosureFrame; +} + +function validateCredentialDatesInCertificateWindow(credentialValidityInfo: validityInfo, certificate) { + // Extract dates from credential + const credentialValidFrom = new Date(credentialValidityInfo.validFrom); + const credentialValidTo = new Date(credentialValidityInfo.validUntil); + + // Extract dates from certificate + const certNotBefore = new Date(certificate.validFrom); + const certNotAfter = new Date(certificate.expiry); + + // Validate that credential dates are within certificate validity period + const isCredentialStartValid = credentialValidFrom >= certNotBefore; + const isCredentialEndValid = credentialValidTo <= certNotAfter; + const isCredentialDurationValid = credentialValidFrom <= credentialValidTo; + + return { + isValid: isCredentialStartValid && isCredentialEndValid && isCredentialDurationValid, + details: { + credentialStartValid: isCredentialStartValid, + credentialEndValid: isCredentialEndValid, + credentialDurationValid: isCredentialDurationValid, + credentialValidFrom: credentialValidFrom.toISOString(), + credentialValidTo: credentialValidTo.toISOString(), + certificateNotBefore: certNotBefore.toISOString(), + certificateNotAfter: certNotAfter.toISOString() + } + }; +} + +function buildSdJwtCredential( + credentialRequest: CredentialRequestDtoLike, + templateRecord: any, + signerOptions: SignerOption[], + activeCertificateDetails?: X509CertificateRecord[] +): BuiltCredential { + // For SD-JWT format we expect payload to be a flat map of claims (no namespaces) + let payloadCopy = { ...(credentialRequest.payload as Record) }; + + // // strip vct if present per requirement + // delete payloadCopy.vct; + + const templateSignerOption: SignerOption = signerOptions.find( + (x) => templateRecord.signerOption.toLowerCase() === x.method + ); + if (!templateSignerOption) { + throw new UnprocessableEntityException( + `Signer option "${templateRecord.signerOption}" is not configured for template ${templateRecord.id}` + ); + } + + if (templateRecord.signerOption === SignerMethodOption.X5C && credentialRequest.validityInfo) { + if (!activeCertificateDetails?.length) { + throw new UnprocessableEntityException('Active x.509 certificate details are required for x5c signer templates.'); + } + const certificateDetail = activeCertificateDetails.find( + (x) => x.certificateBase64 === templateSignerOption.x5c?.[0] + ); + if (!certificateDetail) { + throw new UnprocessableEntityException('No active x.509 certificate matches the configured signer option.'); + } + + const validationResult = validateCredentialDatesInCertificateWindow( + credentialRequest.validityInfo, + certificateDetail + ); + if (!validationResult.isValid) { + throw new UnprocessableEntityException(`${JSON.stringify(validationResult.details)}`); + } + } + + if (credentialRequest.validityInfo) { + const credentialValidFrom = new Date(credentialRequest.validityInfo.validFrom); + const credentialValidTo = new Date(credentialRequest.validityInfo.validUntil); + const isCredentialDurationValid = credentialValidFrom <= credentialValidTo; + if (!isCredentialDurationValid) { + const errorDetails = { + credentialDurationValid: isCredentialDurationValid, + credentialValidFrom: credentialValidFrom.toISOString(), + credentialValidTo: credentialValidTo.toISOString() + }; + throw new UnprocessableEntityException(`${JSON.stringify(errorDetails)}`); + } + payloadCopy = { + ...payloadCopy, + nbf: dateToSeconds(credentialValidFrom), + exp: dateToSeconds(credentialValidTo) + }; + } + + const sdJwtTemplate = templateRecord.attributes as SdJwtTemplate; + payloadCopy.vct = sdJwtTemplate.vct; + + const apiFormat = mapDbFormatToApiFormat(templateRecord.format); + const idSuffix = formatSuffix(apiFormat); + const credentialSupportedId = `${templateRecord.name}-${idSuffix}`; + const disclosureFrame = buildDisclosureFrameFromTemplate({ attributes: sdJwtTemplate.attributes }); + + return { + credentialSupportedId, + signerOptions: templateSignerOption ? templateSignerOption : undefined, + format: apiFormat, + payload: payloadCopy, + ...(disclosureFrame ? { disclosureFrame } : {}) + }; +} + +/** Build an MSO mdoc credential object + * - For mdocs we expect the payload to include a `namespaces` map (draft-15 style) + */ +function buildMdocCredential( + credentialRequest: CredentialRequestDtoLike, + templateRecord: any, + signerOptions: SignerOption[], + activeCertificateDetails: X509CertificateRecord[] +): BuiltCredential { + let incomingPayload = { ...(credentialRequest.payload as Record) }; + + if ( + !credentialRequest.validityInfo || + !credentialRequest.validityInfo.validFrom || + !credentialRequest.validityInfo.validUntil + ) { + throw new UnprocessableEntityException(`${ResponseMessages.oidcIssuerSession.error.missingValidityInfo}`); + } + + if (!signerOptions?.length || !signerOptions[0].x5c?.length) { + throw new UnprocessableEntityException('An x5c signer configuration is required for mdoc credentials.'); + } + if (!activeCertificateDetails?.length) { + throw new UnprocessableEntityException('Active x.509 certificate details are required for mdoc credentials.'); + } + const certificateDetail = activeCertificateDetails.find((x) => x.certificateBase64 === signerOptions[0].x5c[0]); + if (!certificateDetail) { + throw new UnprocessableEntityException('No active x.509 certificate matches the configured signer option.'); + } + const validationResult = validateCredentialDatesInCertificateWindow( + credentialRequest.validityInfo, + certificateDetail + ); + + if (!validationResult.isValid) { + throw new UnprocessableEntityException(`${JSON.stringify(validationResult.details)}`); + } + incomingPayload = { + ...incomingPayload, + validityInfo: credentialRequest.validityInfo + }; + + const apiFormat = mapDbFormatToApiFormat(templateRecord.format); + const idSuffix = formatSuffix(apiFormat); + const credentialSupportedId = `${templateRecord.name}-${idSuffix}`; + + return { + credentialSupportedId, + signerOptions: signerOptions ? signerOptions[0] : undefined, + format: apiFormat, + payload: incomingPayload + }; +} + +export function buildCredentialOfferPayload( + dto: CreateOidcCredentialOfferDtoLike, + templates: credential_templates[], + issuerDetails?: { + publicId: string; + authorizationServerUrl?: string; + }, + signerOptions?: SignerOption[], + activeCertificateDetails?: X509CertificateRecord[] +): CredentialOfferPayload { + // Index templates by id + const templatesById = new Map(templates.map((template) => [template.id, template])); + + // Validate template ids + const missingTemplateIds = dto.credentials.map((c) => c.templateId).filter((id) => !templatesById.has(id)); + if (missingTemplateIds.length) { + throw new Error(`Unknown template ids: ${missingTemplateIds.join(', ')}`); + } + + // Build each credential using the template's format + const builtCredentials: BuiltCredential[] = dto.credentials.map((credentialRequest) => { + const templateRecord = templatesById.get(credentialRequest.templateId)!; + + const validationError = validatePayloadAgainstTemplate(templateRecord, credentialRequest.payload); + if (!validationError.valid) { + throw new UnprocessableEntityException(`${validationError.errors.join(', ')}`); + } + + const templateFormat = (templateRecord as any).format ?? 'vc+sd-jwt'; + const apiFormat = mapDbFormatToApiFormat(templateFormat); + if (apiFormat === CredentialFormat.SdJwtVc) { + return buildSdJwtCredential(credentialRequest, templateRecord, signerOptions, activeCertificateDetails); + } + if (apiFormat === CredentialFormat.Mdoc) { + return buildMdocCredential(credentialRequest, templateRecord, signerOptions, activeCertificateDetails); + } + throw new Error(`Unsupported template format for ${templateFormat}`); + }); + + // Base envelope: allow explicit publicIssuerId from DTO or fallback to issuerDetails.publicId + const publicIssuerIdFromDto = dto.publicIssuerId; + const publicIssuerIdFromIssuerDetails = issuerDetails?.publicId; + const finalPublicIssuerId = publicIssuerIdFromDto ?? publicIssuerIdFromIssuerDetails; + + const baseEnvelope: BuiltCredentialOfferBase = { + credentials: builtCredentials, + ...(finalPublicIssuerId ? { publicIssuerId: finalPublicIssuerId } : {}) + }; + + // Determine which authorization flow to return: + // Priority: + // 1) If issuerDetails.authorizationServerUrl is provided, return preAuthorizedCodeFlowConfig using DEFAULT_TXCODE + // 2) Else fall back to flows present in DTO (still enforce XOR) + const overrideAuthorizationServerUrl = issuerDetails?.authorizationServerUrl; + if (overrideAuthorizationServerUrl) { + if ('string' !== typeof overrideAuthorizationServerUrl || '' === overrideAuthorizationServerUrl.trim()) { + throw new Error('issuerDetails.authorizationServerUrl must be a non-empty string when provided'); + } + return { + ...baseEnvelope, + preAuthorizedCodeFlowConfig: { + txCode: DEFAULT_TXCODE, + authorizationServerUrl: overrideAuthorizationServerUrl + } + }; + } + + // No override provided — use what DTO carries (must be XOR) + const hasPreAuthFromDto = Boolean(dto.preAuthorizedCodeFlowConfig); + const hasAuthCodeFromDto = Boolean(dto.authorizationCodeFlowConfig); + if (hasPreAuthFromDto === hasAuthCodeFromDto) { + throw new Error('Provide exactly one of preAuthorizedCodeFlowConfig or authorizationCodeFlowConfig.'); + } + if (hasPreAuthFromDto) { + return { + ...baseEnvelope, + preAuthorizedCodeFlowConfig: dto.preAuthorizedCodeFlowConfig! + }; + } + return { + ...baseEnvelope, + authorizationCodeFlowConfig: dto.authorizationCodeFlowConfig! + }; +} diff --git a/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts b/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts new file mode 100644 index 000000000..96f5282c0 --- /dev/null +++ b/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts @@ -0,0 +1,417 @@ +/* eslint-disable camelcase */ +import { oidc_issuer, Prisma } from '@prisma/client'; +import { batchCredentialIssuanceDefault } from '../../constant/issuance'; +import { CreateOidcCredentialOffer } from '../../interfaces/oid4vc-issuer-sessions.interfaces'; +import { IssuerResponse } from 'apps/oid4vc-issuance/interfaces/oid4vc-issuance.interfaces'; +import { + Claim, + CredentialAttribute, + MdocTemplate, + SdJwtTemplate +} from 'apps/oid4vc-issuance/interfaces/oid4vc-template.interfaces'; +import { CredentialFormat } from '@credebl/enum/enum'; + +type AttributeDisplay = { name: string; locale: string }; + +//TODO: Fix this eslint issue +// eslint-disable-next-line @typescript-eslint/no-unused-vars +type AttributeDef = { + display?: AttributeDisplay[]; + mandatory?: boolean; + value_type: 'string' | 'date' | 'number' | 'boolean' | string; +}; +// type AttributesMap = Record; + +type CredentialDisplayItem = { + logo?: { uri: string; alt_text?: string }; + name: string; + locale?: string; + description?: string; +}; +type Appearance = { + display: CredentialDisplayItem[]; +}; + +// type Claim = { +// mandatory?: boolean; +// // value_type: string; +// path: string[]; +// display?: AttributeDisplay[]; +// }; + +type CredentialConfig = { + format: string; + vct?: string; + scope: string; + doctype?: string; + claims: Claim[]; + credential_signing_alg_values_supported: string[]; + cryptographic_binding_methods_supported: string[]; + display: { name: string; description?: string; locale?: string }[]; +}; + +type CredentialConfigurationsSupported = { + credentialConfigurationsSupported: Record; +}; + +// ---- Static Lists (as requested) ---- +const STATIC_CREDENTIAL_ALGS = ['ES256', 'EdDSA'] as const; +const STATIC_BINDING_METHODS = ['did:key'] as const; + +// Safe coercion helpers +function coerceJsonObject(v: Prisma.JsonValue): T | null { + if (null == v) { + return null; + } + if ('string' === typeof v) { + try { + return JSON.parse(v) as T; + } catch { + return null; + } + } + return v as unknown as T; // already a JsonObject/JsonArray +} +// eslint-disable-next-line @typescript-eslint/no-explicit-any +// function isAttributesMap(x: any): x is AttributesMap { +// return x && 'object' === typeof x && Array.isArray(x); +// } +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// function isAppearance(x: any): x is Appearance { +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// return x && 'object' === typeof x && Array.isArray((x as any).display); +// } + +// // Prisma row shape +// type TemplateRowPrisma = { +// id: string; +// name: string; +// description?: string | null; +// format?: string | null; +// canBeRevoked?: boolean | null; +// attributes: Prisma.JsonValue; // JsonValue from DB +// appearance: Prisma.JsonValue; // JsonValue from DB +// issuerId: string; +// createdAt?: Date | string; +// updatedAt?: Date | string; +// }; + +// Prisma row shape +//TODO: Fix this eslint issue +// eslint-disable-next-line @typescript-eslint/no-unused-vars +type TemplateRowPrisma = { + id: string; + name: string; + description?: string | null; + format?: string | null; + canBeRevoked?: boolean | null; + attributes: SdJwtTemplate | MdocTemplate; // JsonValue from DB + appearance: Prisma.JsonValue; // JsonValue from DB + issuerId: string; + createdAt?: Date | string; + updatedAt?: Date | string; +}; + +// Default DPoP list for issuer-level metadata (match your example) +const ISSUER_DPOP_ALGS_DEFAULT = ['RS256', 'ES256'] as const; + +// ---------- Safe coercion ---------- +function coerceJson(v: Prisma.JsonValue): T | null { + if (null == v) { + return null; + } + if ('string' === typeof v) { + try { + return JSON.parse(v) as T; + } catch { + return null; + } + } + return v as unknown as T; +} + +type DisplayItem = { + name: string; + locale?: string; + description?: string; + logo?: { uri: string; alt_text?: string }; +}; + +function isDisplayArray(x: unknown): x is DisplayItem[] { + return ( + Array.isArray(x) && + x.every( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (i) => i && 'object' === typeof i && 'string' === typeof (i as any).name + ) + ); +} + +// ---------- Builder you asked for ---------- +/** + * Build issuer metadata payload from issuer row + credential configurations. + * + * @param credentialConfigurations Object with credentialConfigurationsSupported (from your template builder) + * @param oidcIssuer OID4VC issuer row (uses publicIssuerId and metadata -> display) + * @param opts Optional overrides: dpopAlgs[], accessTokenSignerKeyType + */ +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export function buildIssuerPayload( + credentialConfigurations: CredentialConfigurationsSupported, + oidcIssuer: oidc_issuer, + opts?: { + dpopAlgs?: string[]; + accessTokenSignerKeyType?: string; + } +) { + if (!oidcIssuer?.publicIssuerId || 'string' !== typeof oidcIssuer.publicIssuerId) { + throw new Error('Invalid issuer: missing publicIssuerId'); + } + + const rawDisplay = coerceJson(oidcIssuer.metadata); + const display: DisplayItem[] = isDisplayArray(rawDisplay) ? rawDisplay : []; + + return { + display, + dpopSigningAlgValuesSupported: opts?.dpopAlgs ?? [...ISSUER_DPOP_ALGS_DEFAULT], + credentialConfigurationsSupported: credentialConfigurations.credentialConfigurationsSupported ?? {}, + batchCredentialIssuance: { + batchSize: oidcIssuer?.batchCredentialIssuanceSize ?? batchCredentialIssuanceDefault + } + }; +} + +export function extractTemplateIds(offer: CreateOidcCredentialOffer): string[] { + if (!offer?.credentials || !Array.isArray(offer.credentials)) { + return []; + } + + return offer.credentials.map((c) => c.templateId).filter((id): id is string => Boolean(id)); +} + +export function normalizeJson(input: unknown): IssuerResponse { + if ('string' === typeof input) { + return JSON.parse(input) as IssuerResponse; + } + if (input && 'object' === typeof input) { + return input as IssuerResponse; + } + throw new Error('Expected a JSON object or JSON string'); +} + +export function encodeIssuerPublicId(publicIssuerId: string): string { + if (!publicIssuerId) { + throw new Error('issuerPublicId is required'); + } + return encodeURIComponent(publicIssuerId.trim()); +} + +///--------------------------------------------------------- + +// function buildClaimsFromAttributesWithPath(attributes: CredentialAttribute[], parentPath: string[] = []): Claim[] { +// const claims: Claim[] = []; + +// for (const attr of attributes) { +// const currentPath = [...parentPath, attr.key]; + +// // 1️⃣ Add the parent attribute itself if it has display or mandatory metadata +// if ((attr.display && 0 < attr.display.length) || attr.mandatory) { +// const parentClaim: Claim = { path: currentPath }; + +// if (attr.display?.length) { +// parentClaim.display = attr.display.map((d) => ({ +// name: d.name, +// locale: d.locale +// })); +// } + +// if (attr.mandatory) { +// parentClaim.mandatory = true; +// } + +// claims.push(parentClaim); +// } + +// // 2️⃣ If this attribute has nested children, recurse into them +// if (attr.children && 0 < attr.children.length) { +// claims.push(...buildClaimsFromAttributes(attr.children, currentPath)); +// } +// } +// return claims; +// } + +/** + * Recursively builds a nested claims object from a list of attributes. + */ +function buildNestedClaims(attributes: CredentialAttribute[]): Record { + const claims: Record = {}; + + for (const attr of attributes) { + const node: Claim = {}; + + // ✅ include display info + if (attr.display?.length) { + node.display = attr.display.map((d) => ({ + name: d.name, + locale: d.locale + })); + } + + // ✅ include mandatory flag + if (attr.mandatory) { + node.mandatory = true; + } + + // ✅ handle nested children recursively + if (attr.children?.length) { + const childClaims = buildNestedClaims(attr.children); + Object.assign(node, childClaims); // merge children into current node + } + + claims[attr.key] = node; + } + + return claims; +} + +/** + * Builds claims object for both SD-JWT and MDOC credential templates. + */ +//TODO: Remove any type +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function buildClaimsFromTemplate(template: SdJwtTemplate | MdocTemplate): Record { + // ✅ MDOC case — handle namespaces + if ((template as MdocTemplate).namespaces) { + const mdocTemplate = template as MdocTemplate; + + //TODO: Remove any type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const claims: Record = {}; + + for (const ns of mdocTemplate.namespaces) { + claims[ns.namespace] = buildNestedClaims(ns.attributes); + } + + return claims; + } + + // ✅ SD-JWT case — flat attributes + const sdjwtTemplate = template as SdJwtTemplate; + return buildNestedClaims(sdjwtTemplate.attributes); +} + +//TODO: Fix this eslint issue +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export function buildSdJwtCredentialConfig(name: string, template: SdJwtTemplate) { + const formatSuffix = 'sdjwt'; + + // Determine the unique key for this credential configuration + const configKey = `${name}-${formatSuffix}`; + const credentialScope = `openid4vc:${template.vct}-${formatSuffix}`; + + const claims = buildClaimsFromTemplate(template); + + return { + [configKey]: { + format: CredentialFormat.SdJwtVc, + scope: credentialScope, + vct: template.vct, + credential_signing_alg_values_supported: [...STATIC_CREDENTIAL_ALGS], + cryptographic_binding_methods_supported: [...STATIC_BINDING_METHODS], + // proof_types_supported: { + // jwt: { + // proof_signing_alg_values_supported: ['ES256'] + // } + // }, + claims + } + }; +} + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export function buildMdocCredentialConfig(name: string, template: MdocTemplate) { + //const claims: Claim[] = []; + + const formatSuffix = 'mdoc'; + + // Determine the unique key for this credential configuration + const configKey = `${name}-${formatSuffix}`; + const credentialScope = `openid4vc:${template.doctype}-${formatSuffix}`; + + const claims = buildClaimsFromTemplate(template); + + // for (const ns of template.namespaces) { + // claims.push(...buildClaimsFromAttributes(ns.attributes, [ns.namespace])); + // } + + return { + [configKey]: { + format: CredentialFormat.Mdoc, + scope: credentialScope, + doctype: template.doctype, + credential_signing_alg_values_supported: [...STATIC_CREDENTIAL_ALGS], + cryptographic_binding_methods_supported: [...STATIC_BINDING_METHODS], + claims + } + }; +} + +//TODO: Fix this eslint issue +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export function buildCredentialConfig(name: string, template: SdJwtTemplate | MdocTemplate, format: CredentialFormat) { + switch (format) { + case CredentialFormat.SdJwtVc: + return buildSdJwtCredentialConfig(name, template as SdJwtTemplate); + case CredentialFormat.Mdoc: + return buildMdocCredentialConfig(name, template as MdocTemplate); + default: + throw new Error(`Unsupported credential format: ${format}`); + } +} + +/** + * Build agent payload from Prisma rows (attributes/appearance are Prisma.JsonValue). + * Safely coerces JSON and then builds the same structure as Builder #2. + */ +//TODO: Fix this eslint issue +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function buildCredentialConfigurationsSupported(templateRows: any): Record { + const credentialConfigMap: Record = {}; + + for (const templateRow of templateRows) { + const { format } = templateRow; + const templateToBuild = templateRow.attributes; + + const credentialConfig = buildCredentialConfig( + templateRow.name, + templateToBuild, + format === CredentialFormat.Mdoc ? CredentialFormat.Mdoc : CredentialFormat.SdJwtVc + ); + + const appearanceJson = coerceJsonObject(templateRow.appearance); + + // Prepare the display configuration + const displayConfigurations = + (appearanceJson as Appearance).display?.map((displayEntry) => ({ + name: displayEntry.name, + description: displayEntry.description, + locale: displayEntry.locale, + logo: displayEntry.logo + ? { + uri: displayEntry.logo.uri, + alt_text: displayEntry.logo.alt_text + } + : undefined + })) ?? []; + + // eslint-disable-next-line prefer-destructuring + const dynamicKey = Object.keys(credentialConfig)[0]; + Object.assign(credentialConfig[dynamicKey], { + display: displayConfigurations + }); + + Object.assign(credentialConfigMap, credentialConfig); + } + + return credentialConfigMap; // ✅ Return flat map, not nested object +} diff --git a/apps/oid4vc-issuance/src/main.ts b/apps/oid4vc-issuance/src/main.ts new file mode 100644 index 000000000..be0701c3d --- /dev/null +++ b/apps/oid4vc-issuance/src/main.ts @@ -0,0 +1,23 @@ +import { NestFactory } from '@nestjs/core'; +import { HttpExceptionFilter } from 'libs/http-exception.filter'; +import { Logger } from '@nestjs/common'; +import { MicroserviceOptions, Transport } from '@nestjs/microservices'; +import { getNatsOptions } from '@credebl/common/nats.config'; +import { CommonConstants } from '@credebl/common/common.constant'; +import NestjsLoggerServiceAdapter from '@credebl/logger/nestjsLoggerServiceAdapter'; +import { Oid4vcIssuanceModule } from './oid4vc-issuance.module'; + +const logger = new Logger(); + +async function bootstrap(): Promise { + const app = await NestFactory.createMicroservice(Oid4vcIssuanceModule, { + transport: Transport.NATS, + options: getNatsOptions(CommonConstants.OIDC4VC_ISSUANCE_SERVICE, process.env.OIDC4VC_ISSUANCE_NKEY_SEED) + }); + app.useLogger(app.get(NestjsLoggerServiceAdapter)); + app.useGlobalFilters(new HttpExceptionFilter()); + + await app.listen(); + logger.log('OID4VC-Issuance-Service Microservice is listening to NATS '); +} +bootstrap(); diff --git a/apps/oid4vc-issuance/src/oid4vc-issuance.controller.ts b/apps/oid4vc-issuance/src/oid4vc-issuance.controller.ts new file mode 100644 index 000000000..01b69f5d6 --- /dev/null +++ b/apps/oid4vc-issuance/src/oid4vc-issuance.controller.ts @@ -0,0 +1,182 @@ +/* eslint-disable camelcase */ +import { Controller } from '@nestjs/common'; +import { Oid4vcIssuanceService } from './oid4vc-issuance.service'; +import { IssuerCreation, IssuerUpdation } from '../interfaces/oid4vc-issuance.interfaces'; +import { MessagePattern } from '@nestjs/microservices'; +import { user, oidc_issuer, credential_templates } from '@prisma/client'; +import { + CreateOidcCredentialOffer, + UpdateCredentialRequest, + GetAllCredentialOffer +} from '../interfaces/oid4vc-issuer-sessions.interfaces'; +import { CreateCredentialTemplate, UpdateCredentialTemplate } from '../interfaces/oid4vc-template.interfaces'; +import { Oid4vcCredentialOfferWebhookPayload } from '../interfaces/oid4vc-wh-interfaces'; + +@Controller() +export class Oid4vcIssuanceController { + constructor(private readonly oid4vcIssuanceService: Oid4vcIssuanceService) {} + + @MessagePattern({ cmd: 'oid4vc-issuer-create' }) + async oidcIssuerCreate(payload: { + issueCredentialDto: IssuerCreation; + orgId: string; + userDetails: user; + }): Promise { + const { issueCredentialDto, orgId, userDetails } = payload; + return this.oid4vcIssuanceService.oidcIssuerCreate(issueCredentialDto, orgId, userDetails); + } + + @MessagePattern({ cmd: 'oid4vc-issuer-update' }) + async oidcIssuerUpdate(payload: { + issueUpdationDto: IssuerUpdation; + orgId: string; + userDetails: user; + }): Promise { + const { issueUpdationDto, orgId, userDetails } = payload; + return this.oid4vcIssuanceService.oidcIssuerUpdate(issueUpdationDto, orgId, userDetails); + } + + @MessagePattern({ cmd: 'oid4vc-issuer-get-by-id' }) + async oidcGetIssuerById(payload: { + id: string; + orgId: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + }): Promise { + const { id, orgId } = payload; + return this.oid4vcIssuanceService.oidcIssuerGetById(id, orgId); + } + + @MessagePattern({ cmd: 'oid4vc-get-issuers-issuance' }) + async oidcGetIssuers(payload: { orgId: string }): Promise { + const { orgId } = payload; + return this.oid4vcIssuanceService.oidcIssuers(orgId); + } + + @MessagePattern({ cmd: 'oid4vc-delete-issuer' }) + async deleteOidcIssuer(payload: { + orgId: string; + userDetails: user; + id: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + }): Promise { + const { orgId, userDetails, id } = payload; + return this.oid4vcIssuanceService.deleteOidcIssuer(orgId, userDetails, id); + } + + @MessagePattern({ cmd: 'oid4vc-template-create' }) + async oidcTemplateCreate(payload: { + CredentialTemplate: CreateCredentialTemplate; + orgId: string; + issuerId: string; + }): Promise { + const { CredentialTemplate, orgId, issuerId } = payload; + return this.oid4vcIssuanceService.createTemplate(CredentialTemplate, orgId, issuerId); + } + + @MessagePattern({ cmd: 'oid4vc-template-update' }) + async oidcTemplateUpdate(payload: { + templateId: string; + dto: UpdateCredentialTemplate; + orgId: string; + issuerId: string; + }): Promise { + const { templateId, dto, orgId, issuerId } = payload; + return this.oid4vcIssuanceService.updateTemplate(templateId, dto, orgId, issuerId); + } + + @MessagePattern({ cmd: 'oid4vc-template-delete' }) + async oidcTemplateDelete(payload: { + templateId: string; + orgId: string; + userDetails: user; + issuerId: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + }): Promise { + const { templateId, orgId, userDetails, issuerId } = payload; + return this.oid4vcIssuanceService.deleteTemplate(templateId, orgId, userDetails, issuerId); + } + + @MessagePattern({ cmd: 'oid4vc-template-find-id' }) + async oidcTemplateFindById(payload: { templateId: string; orgId: string }): Promise { + const { templateId, orgId } = payload; + return this.oid4vcIssuanceService.findByIdTemplate(templateId, orgId); + } + + @MessagePattern({ cmd: 'oid4vc-template-find-all' }) + async oidcTemplateFindAll(payload: { orgId: string; issuerId: string }): Promise { + const { orgId, issuerId } = payload; + return this.oid4vcIssuanceService.findAllTemplate(orgId, issuerId); + } + + @MessagePattern({ cmd: 'oid4vc-create-credential-offer' }) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async createOidcCredentialOffer(payload: { + oidcCredentialPayload: CreateOidcCredentialOffer; + orgId: string; + userDetails: user; + issuerId: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + }): Promise { + const { oidcCredentialPayload, orgId, userDetails, issuerId } = payload; + return this.oid4vcIssuanceService.createOidcCredentialOffer(oidcCredentialPayload, orgId, userDetails, issuerId); + } + + @MessagePattern({ cmd: 'oid4vc-create-credential-offer-D2A' }) + async createOidcCredentialOfferD2A(payload: { + oidcCredentialD2APayload; + orgId: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + }): Promise { + const { oidcCredentialD2APayload, orgId } = payload; + return this.oid4vcIssuanceService.createOidcCredentialOfferD2A(oidcCredentialD2APayload, orgId); + } + + @MessagePattern({ cmd: 'oid4vc-update-credential-offer' }) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async updateOidcCredentialOffer(payload: { + oidcUpdateCredentialPayload: UpdateCredentialRequest; + orgId: string; + issuerId: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + }): Promise { + const { oidcUpdateCredentialPayload, orgId, issuerId } = payload; + return this.oid4vcIssuanceService.updateOidcCredentialOffer(oidcUpdateCredentialPayload, orgId, issuerId); + } + + @MessagePattern({ cmd: 'oid4vc-credential-offer-get-by-id' }) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async getCredentialOfferDetailsById(payload: { + offerId: string; + orgId: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + }): Promise { + const { offerId, orgId } = payload; + return this.oid4vcIssuanceService.getCredentialOfferDetailsById(offerId, orgId); + } + @MessagePattern({ cmd: 'oid4vc-credential-offer-get-all' }) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async getAllCredentialOffers(payload: { + orgId: string; + getAllCredentialOffer: GetAllCredentialOffer; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + }): Promise { + const { orgId, getAllCredentialOffer } = payload; + return this.oid4vcIssuanceService.getCredentialOffers(orgId, getAllCredentialOffer); + } + + @MessagePattern({ cmd: 'oid4vc-credential-offer-delete' }) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async deleteCredentialOffers(payload: { + orgId: string; + credentialId: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + }): Promise { + const { orgId, credentialId } = payload; + return this.oid4vcIssuanceService.deleteCredentialOffers(orgId, credentialId); + } + + @MessagePattern({ cmd: 'webhook-oid4vc-issue-credential' }) + async oidcIssueCredentialWebhook(payload: Oid4vcCredentialOfferWebhookPayload): Promise { + return this.oid4vcIssuanceService.storeOidcCredentialWebhook(payload); + } +} diff --git a/apps/oid4vc-issuance/src/oid4vc-issuance.module.ts b/apps/oid4vc-issuance/src/oid4vc-issuance.module.ts new file mode 100644 index 000000000..b882032cb --- /dev/null +++ b/apps/oid4vc-issuance/src/oid4vc-issuance.module.ts @@ -0,0 +1,36 @@ +import { Logger, Module } from '@nestjs/common'; +import { Oid4vcIssuanceController } from './oid4vc-issuance.controller'; +import { Oid4vcIssuanceService } from './oid4vc-issuance.service'; +import { ClientsModule, Transport } from '@nestjs/microservices'; +import { getNatsOptions } from '@credebl/common/nats.config'; +import { CommonModule } from '@credebl/common'; +import { CommonConstants } from '@credebl/common/common.constant'; +import { GlobalConfigModule } from '@credebl/config'; +import { ContextInterceptorModule } from '@credebl/context'; +import { LoggerModule } from '@credebl/logger'; +import { CacheModule } from '@nestjs/cache-manager'; +import { ConfigModule as PlatformConfig } from '@credebl/config/config.module'; +import { Oid4vcIssuanceRepository } from './oid4vc-issuance.repository'; +import { NATSClient } from '@credebl/common/NATSClient'; +import { PrismaService } from '@credebl/prisma-service'; + +@Module({ + imports: [ + ClientsModule.register([ + { + name: 'NATS_CLIENT', + transport: Transport.NATS, + options: getNatsOptions(CommonConstants.OIDC4VC_ISSUANCE_SERVICE, process.env.OIDC4VC_ISSUANCE_NKEY_SEED) + } + ]), + CommonModule, + GlobalConfigModule, + LoggerModule, + PlatformConfig, + ContextInterceptorModule, + CacheModule.register() + ], + controllers: [Oid4vcIssuanceController], + providers: [Oid4vcIssuanceService, Oid4vcIssuanceRepository, PrismaService, Logger, NATSClient] +}) +export class Oid4vcIssuanceModule {} diff --git a/apps/oid4vc-issuance/src/oid4vc-issuance.repository.ts b/apps/oid4vc-issuance/src/oid4vc-issuance.repository.ts new file mode 100644 index 000000000..a55b71664 --- /dev/null +++ b/apps/oid4vc-issuance/src/oid4vc-issuance.repository.ts @@ -0,0 +1,356 @@ +/* eslint-disable camelcase */ +import { Injectable, Logger, NotFoundException } from '@nestjs/common'; +// eslint-disable-next-line camelcase +import { Prisma, credential_templates, oidc_issuer, org_agents } from '@prisma/client'; +import { PrismaService } from '@credebl/prisma-service'; +import { IssuerMetadata, IssuerUpdation, OrgAgent } from '../interfaces/oid4vc-issuance.interfaces'; +import { ResponseMessages } from '@credebl/common/response-messages'; +import { x5cKeyType, x5cRecordStatus } from '@credebl/enum/enum'; +import { X509CertificateRecord } from '@credebl/common/interfaces/x509.interface'; + +@Injectable() +export class Oid4vcIssuanceRepository { + constructor( + private readonly prisma: PrismaService, + private readonly logger: Logger + ) {} + + async getAgentEndPoint(orgId: string): Promise { + try { + const agentDetails = await this.prisma.org_agents.findFirst({ + where: { + orgId + }, + include: { + organisation: true + } + }); + + if (!agentDetails) { + throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); + } + + return agentDetails; + } catch (error) { + this.logger.error(`Error in get getAgentEndPoint: ${error.message} `); + throw error; + } + } + + async getOrgAgentType(orgAgentId: string): Promise { + try { + const { agent } = await this.prisma.org_agents_type.findFirst({ + where: { + id: orgAgentId + } + }); + + return agent; + } catch (error) { + this.logger.error(`[getOrgAgentType] - error: ${JSON.stringify(error)}`); + throw error; + } + } + + async getOrganizationByTenantId(tenantId: string): Promise { + try { + return this.prisma.org_agents.findFirst({ + where: { + tenantId + } + }); + } catch (error) { + this.logger.error(`Error in getOrganization in issuance repository: ${error.message} `); + throw error; + } + } + + async storeOidcCredentialDetails(credentialPayload, orgId: string): Promise { + try { + const payload = credentialPayload.oidcIssueCredentialDto; + const { + credentialOfferId, + state, + id: issuanceSessionId, + contextCorrelationId, + credentialOfferPayload, + issuedCredentials, + issuerId + } = payload; + + const credentialDetails = await this.prisma.oid4vc_credentials.upsert({ + where: { + issuanceSessionId + }, + update: { + lastChangedBy: orgId, + state, + credentialConfigurationIds: credentialOfferPayload.credential_configuration_ids ?? [], + ...(issuedCredentials !== undefined ? { issuedCredentials } : {}) + }, + create: { + lastChangedBy: orgId, + createdBy: orgId, + state, + orgId, + credentialOfferId, + contextCorrelationId, + issuanceSessionId, + publicIssuerId: issuerId, + credentialConfigurationIds: credentialOfferPayload.credential_configuration_ids ?? [], + ...(issuedCredentials !== undefined ? { issuedCredentials } : {}) + } + }); + + return credentialDetails; + } catch (error) { + this.logger.error(`Error in storeOidcCredentialDetails in issuance repository: ${error.message} `); + throw error; + } + } + + async getOidcIssuerByOrg(orgId: string): Promise { + try { + return await this.prisma.oidc_issuer.findMany({ + where: { createdBy: orgId }, + include: { + templates: true + }, + orderBy: { + createDateTime: 'desc' + } + }); + } catch (error) { + this.logger.error(`Error in getOidcIssuerByOrg: ${error.message}`); + throw error; + } + } + + async getAllOidcIssuersByOrg(orgId: string): Promise { + try { + return await this.prisma.oidc_issuer.findMany({ + where: { + orgAgent: { + orgId + } + }, + // include: { + // templates: true + // }, + orderBy: { + createDateTime: 'desc' + } + }); + } catch (error) { + this.logger.error(`Error in getOidcIssuerByOrg: ${error.message}`); + throw error; + } + } + + async getOidcIssuerDetailsById(issuerId: string): Promise { + try { + return await this.prisma.oidc_issuer.findFirstOrThrow({ + where: { id: issuerId } + }); + } catch (error) { + this.logger.error(`Error in getOidcIssuerDetailsById: ${error.message}`); + throw error; + } + } + + async getCurrentActiveCertificate(orgId: string, keyType: x5cKeyType): Promise { + try { + const now = new Date(); + + const certificate = await this.prisma.x509_certificates.findFirst({ + where: { + org_agents: { + orgId + }, + status: x5cRecordStatus.Active, + keyType, + validFrom: { + lte: now + }, + expiry: { + gte: now + } + }, + orderBy: { + createdAt: 'desc' + } + }); + return certificate; + } catch (error) { + this.logger.error(`Error in getCurrentActiveCertificate: ${error.message}`); + throw error; + } + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async addOidcIssuerDetails(issuerMetadata: IssuerMetadata, issuerProfileJson): Promise { + try { + const { publicIssuerId, createdById, orgAgentId, batchCredentialIssuanceSize, authorizationServerUrl } = + issuerMetadata; + const oidcIssuerDetails = await this.prisma.oidc_issuer.create({ + data: { + metadata: issuerProfileJson, + publicIssuerId, + createdBy: createdById, + orgAgentId, + batchCredentialIssuanceSize, + authorizationServerUrl + } + }); + + return oidcIssuerDetails; + } catch (error) { + this.logger.error(`[addOidcIssuerDetails] - error: ${JSON.stringify(error)}`); + throw error; + } + } + + async updateOidcIssuerDetails(createdById: string, issuerConfig: IssuerUpdation): Promise { + try { + const { issuerId, display, batchCredentialIssuanceSize } = issuerConfig; + const oidcIssuerDetails = await this.prisma.oidc_issuer.update({ + where: { id: issuerId }, + data: { + metadata: display as unknown as Prisma.InputJsonValue, + createdBy: createdById, + ...(batchCredentialIssuanceSize !== undefined ? { batchCredentialIssuanceSize } : {}) + } + }); + + return oidcIssuerDetails; + } catch (error) { + this.logger.error(`[addOidcIssuerDetails] - error: ${JSON.stringify(error)}`); + throw error; + } + } + + async deleteOidcIssuer(issuerId: string): Promise { + try { + return await this.prisma.oidc_issuer.delete({ + where: { id: issuerId } + }); + } catch (error) { + this.logger.error(`[deleteOidcIssuer] - error: ${JSON.stringify(error)}`); + throw error; + } + } + + async createTemplate( + issuerId: string, + data: Omit + ): Promise { + try { + return await this.prisma.credential_templates.create({ + data: { + ...data, + issuerId + } + }); + } catch (error) { + this.logger.error(`Error in createTemplate: ${error.message}`); + throw error; + } + } + + async getTemplateById(templateId: string): Promise { + try { + return await this.prisma.credential_templates.findUnique({ + where: { id: templateId } + }); + } catch (error) { + this.logger.error(`Error in getTemplateById: ${error.message}`); + throw error; + } + } + + async getTemplateByIds(templateIds: string[], issuerId: string): Promise { + try { + // Early return if empty input (avoids full table scan if someone passes []) + if (!Array.isArray(templateIds) || 0 === templateIds.length) { + return []; + } + + this.logger.debug(`getTemplateByIds templateIds=${JSON.stringify(templateIds)} issuerId=${issuerId}`); + + return await this.prisma.credential_templates.findMany({ + where: { + id: { in: templateIds }, + issuerId + } + }); + } catch (error) { + this.logger.error(`Error in getTemplateByIds: ${error?.message}`, error?.stack); + throw error; + } + } + + async getTemplateByNameForIssuer(name: string, issuerId: string): Promise { + try { + return await this.prisma.credential_templates.findMany({ + where: { + issuerId, + name: { + equals: name, + mode: 'insensitive' + } + } + }); + } catch (error) { + this.logger.error(`Error in getTemplateByNameForIssuer: ${error.message}`); + throw error; + } + } + + async getTemplatesByIssuerId(issuerId: string): Promise { + try { + return await this.prisma.credential_templates.findMany({ + where: { issuerId }, + orderBy: { + createdAt: 'desc' + } + }); + } catch (error) { + this.logger.error(`Error in getTemplatesByIssuer: ${error.message}`); + throw error; + } + } + + async getIssuerDetailsByIssuerId(issuerId: string): Promise { + try { + return await this.prisma.oidc_issuer.findUnique({ + where: { id: issuerId } + }); + } catch (error) { + this.logger.error(`Error in getIssuerDetailsByIssuerId: ${error.message}`); + throw error; + } + } + + async updateTemplate(templateId: string, data: Partial): Promise { + try { + return await this.prisma.credential_templates.update({ + where: { id: templateId }, + data + }); + } catch (error) { + this.logger.error(`Error in updateTemplate: ${error.message}`); + throw error; + } + } + + async deleteTemplate(templateId: string): Promise { + try { + return await this.prisma.credential_templates.delete({ + where: { id: templateId } + }); + } catch (error) { + this.logger.error(`Error in deleteTemplate: ${error.message}`); + throw error; + } + } +} diff --git a/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts b/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts new file mode 100644 index 000000000..1dd4b8584 --- /dev/null +++ b/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts @@ -0,0 +1,1013 @@ +/* eslint-disable quotes */ +/* eslint-disable no-useless-catch */ +/* eslint-disable camelcase */ +/* eslint-disable no-console */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/naming-convention, @typescript-eslint/explicit-function-return-type, @typescript-eslint/explicit-module-boundary-types, camelcase */ + +import { + BadRequestException, + ConflictException, + HttpException, + Inject, + Injectable, + InternalServerErrorException, + Logger, + NotFoundException, + Scope +} from '@nestjs/common'; +import { Oid4vcIssuanceRepository } from './oid4vc-issuance.repository'; +import { CommonConstants } from '@credebl/common/common.constant'; +import { ResponseMessages } from '@credebl/common/response-messages'; +import { ClientProxy, RpcException } from '@nestjs/microservices'; +import { map } from 'rxjs'; +import { getAgentUrl } from '@credebl/common/common.utils'; +import { credential_templates, oidc_issuer, Prisma, SignerOption, user } from '@prisma/client'; +import { + IAgentOIDCIssuerCreate, + IssuerCreation, + IssuerInitialConfig, + IssuerMetadata, + IssuerResponse, + IssuerUpdation +} from '../interfaces/oid4vc-issuance.interfaces'; +import { CreateCredentialTemplate, UpdateCredentialTemplate } from '../interfaces/oid4vc-template.interfaces'; +import { + accessTokenSignerKeyType, + batchCredentialIssuanceDefault, + credentialConfigurationsSupported, + dpopSigningAlgValuesSupported +} from '../constant/issuance'; +import { + buildCredentialConfigurationsSupported, + buildIssuerPayload, + encodeIssuerPublicId, + extractTemplateIds, + normalizeJson +} from '../libs/helpers/issuer.metadata'; +import { + CreateOidcCredentialOffer, + GetAllCredentialOffer, + SignerMethodOption, + UpdateCredentialRequest +} from '../interfaces/oid4vc-issuer-sessions.interfaces'; +import { + buildCredentialOfferPayload, + buildCredentialOfferUrl, + CredentialOfferPayload +} from '../libs/helpers/credential-sessions.builder'; +import { x5cKeyType } from '@credebl/enum/enum'; +import { instanceToPlain, plainToInstance } from 'class-transformer'; +import { X509CertificateRecord } from '@credebl/common/interfaces/x509.interface'; +import { Oid4vcCredentialOfferWebhookPayload } from '../interfaces/oid4vc-wh-interfaces'; + +type CredentialDisplayItem = { + logo?: { uri: string; alt_text?: string }; + name: string; + locale?: string; + description?: string; +}; + +type Appearance = { + display: CredentialDisplayItem[]; +}; +@Injectable() +export class Oid4vcIssuanceService { + private readonly logger = new Logger('IssueCredentialService'); + constructor( + @Inject('NATS_CLIENT') private readonly issuanceServiceProxy: ClientProxy, + private readonly oid4vcIssuanceRepository: Oid4vcIssuanceRepository + ) {} + + async oidcIssuerCreate(issuerCreation: IssuerCreation, orgId: string, userDetails: user): Promise { + try { + const { issuerId, batchCredentialIssuanceSize } = issuerCreation; + const agentDetails = await this.oid4vcIssuanceRepository.getAgentEndPoint(orgId); + if (!agentDetails) { + throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); + } + const { agentEndPoint, id: orgAgentId, orgAgentTypeId } = agentDetails; + const orgAgentType = await this.oid4vcIssuanceRepository.getOrgAgentType(orgAgentTypeId); + if (!orgAgentType) { + throw new NotFoundException(ResponseMessages.issuance.error.orgAgentTypeNotFound); + } + const url = await getAgentUrl(agentEndPoint, CommonConstants.OIDC_ISSUER_CREATE); + const issuerInitialConfig: IssuerInitialConfig = { + issuerId, + display: issuerCreation?.display || {}, + authorizationServerConfigs: issuerCreation?.authorizationServerConfigs || undefined, + accessTokenSignerKeyType, + dpopSigningAlgValuesSupported, + credentialConfigurationsSupported, + ...(batchCredentialIssuanceSize && 0 < batchCredentialIssuanceSize + ? { + batchCredentialIssuance: { + batchSize: batchCredentialIssuanceSize + } + } + : {}) + }; + let createdIssuer; + try { + createdIssuer = await this._createOIDCIssuer(issuerInitialConfig, url, orgId); + } catch (error) { + this.logger.error(`[oidcIssuerCreate] - error in oidcIssuerCreate issuance records: ${JSON.stringify(error)}`); + const status409 = + 409 === error?.status?.message?.statusCode || 409 === error?.response?.status || 409 === error?.statusCode; + + if (status409) { + throw new ConflictException(`Issuer with id '${issuerCreation.issuerId}' already exists`); + } + throw error; + } + const issuerConfigJson = createdIssuer?.response ?? createdIssuer ?? {}; + const issuerIdFromAgent = issuerConfigJson?.issuerId; + + if (!issuerIdFromAgent) { + throw new InternalServerErrorException('Issuer ID missing from agent response'); + } + const issuerMetadata: IssuerMetadata = { + authorizationServerUrl: issuerCreation.authorizationServerUrl, + publicIssuerId: issuerIdFromAgent, + createdById: userDetails.id, + orgAgentId, + batchCredentialIssuanceSize: issuerCreation?.batchCredentialIssuanceSize + }; + const addOidcIssuerDetails = await this.oid4vcIssuanceRepository.addOidcIssuerDetails( + issuerMetadata, + issuerCreation?.display + ); + + if (!addOidcIssuerDetails) { + throw new InternalServerErrorException('Error in adding OID4VC Issuer details in DB'); + } + return addOidcIssuerDetails; + } catch (error) { + this.logger.error(`[oidcIssuerCreate] - error in oidcIssuerCreate issuance records: ${JSON.stringify(error)}`); + throw new RpcException(error?.response ?? error); + } + } + + async oidcIssuerUpdate(issuerUpdationConfig: IssuerUpdation, orgId: string, userDetails: user): Promise { + try { + const getIssuerDetails = await this.oid4vcIssuanceRepository.getOidcIssuerDetailsById( + issuerUpdationConfig.issuerId + ); + if (!getIssuerDetails) { + throw new NotFoundException(ResponseMessages.oidcIssuer.error.notFound); + } + const agentDetails = await this.oid4vcIssuanceRepository.getAgentEndPoint(orgId); + if (!agentDetails) { + throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); + } + const { agentEndPoint, orgAgentTypeId } = agentDetails; + const orgAgentType = await this.oid4vcIssuanceRepository.getOrgAgentType(orgAgentTypeId); + if (!orgAgentType) { + throw new NotFoundException(ResponseMessages.issuance.error.orgAgentTypeNotFound); + } + + const addOidcIssuerDetails = await this.oid4vcIssuanceRepository.updateOidcIssuerDetails( + userDetails.id, + issuerUpdationConfig + ); + + if (!addOidcIssuerDetails) { + throw new InternalServerErrorException('Error in updating OID4VC Issuer details in DB'); + } + + const url = await getAgentUrl( + agentEndPoint, + CommonConstants.OIDC_ISSUER_TEMPLATE, + getIssuerDetails.publicIssuerId + ); + const issuerConfig = await this.buildOidcIssuerConfig(issuerUpdationConfig.issuerId); + console.log('This is the issuerConfig:', JSON.stringify(issuerConfig, null, 2)); + const updatedIssuer = await this._createOIDCTemplate(issuerConfig, url, orgId); + if (updatedIssuer?.response?.statusCode && 200 !== updatedIssuer?.response?.statusCode) { + throw new InternalServerErrorException( + `Error from agent while updating issuer: ${updatedIssuer?.response?.message ?? 'Unknown error'}` + ); + } + + return addOidcIssuerDetails; + } catch (error) { + this.logger.error(`[oidcIssuerUpdate] - error in oidcIssuerUpdate issuance records: ${JSON.stringify(error)}`); + throw new RpcException(error?.response ?? error); + } + } + + async oidcIssuerGetById(id: string, orgId: string): Promise { + try { + const getIssuerDetails = await this.oid4vcIssuanceRepository.getOidcIssuerDetailsById(id); + if (!getIssuerDetails && !getIssuerDetails.publicIssuerId) { + throw new NotFoundException(ResponseMessages.oidcIssuer.error.notFound); + } + const agentDetails = await this.oid4vcIssuanceRepository.getAgentEndPoint(orgId); + if (!agentDetails) { + throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); + } + const encodedId = encodeIssuerPublicId(getIssuerDetails?.publicIssuerId); + const url = await getAgentUrl(agentDetails?.agentEndPoint, CommonConstants.OIDC_ISSUER_BY_ID, encodedId); + const issuerDetailsRaw = await this._oidcGetIssuerById(url, orgId); + if (!issuerDetailsRaw) { + throw new InternalServerErrorException(`Error from agent while getting issuer`); + } + const issuerDetails = { + response: normalizeJson(issuerDetailsRaw.response) + }; + + return issuerDetails.response; + } catch (error) { + const errorStack = error?.status?.message?.error; + if (errorStack) { + throw new RpcException({ + message: errorStack?.reason ? errorStack?.reason : errorStack?.message, + statusCode: error?.status?.code + }); + } else { + throw new RpcException(error.response ? error.response : error); + } + } + } + + async oidcIssuers(orgId: string): Promise { + //Promise { + try { + const agentDetails = await this.oid4vcIssuanceRepository.getAgentEndPoint(orgId); + if (!agentDetails?.agentEndPoint) { + throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); + } + const getIssuers = await this.oid4vcIssuanceRepository.getAllOidcIssuersByOrg(orgId); + + // const url = await getAgentUrl(agentDetails.agentEndPoint, CommonConstants.OIDC_GET_ALL_ISSUERS); + // const issuersDetails = await this._oidcGetIssuers(url, orgId); + // if (!issuersDetails || null == issuersDetails.response) { + // throw new InternalServerErrorException('Error from agent while oidcIssuers'); + // } + // //TODO: Fix the response type from agent + // const raw = issuersDetails.response as unknown; + // const response: IssuerResponse[] = + // 'string' === typeof raw ? (JSON.parse(raw) as IssuerResponse[]) : (raw as IssuerResponse[]); + + // if (!Array.isArray(response)) { + // throw new InternalServerErrorException('Invalid issuer payload from agent'); + // } + // return response; + return getIssuers; + } catch (error: any) { + const msg = error?.message ?? 'unknown error'; + this.logger.error(`[oidcIssuers] - error in oidcIssuers: ${msg}`); + throw new RpcException(error?.response ?? error); + } + } + + async deleteOidcIssuer(orgId: string, userDetails: user, id: string) { + try { + const issuerRecordId = await this.oidcIssuerGetById(id, orgId); + if (!issuerRecordId.id) { + throw new NotFoundException(ResponseMessages.oidcIssuer.error.notFound); + } + const agentDetails = await this.oid4vcIssuanceRepository.getAgentEndPoint(orgId); + if (!agentDetails) { + throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); + } + const { agentEndPoint } = agentDetails; + const url = await getAgentUrl(agentEndPoint, CommonConstants.OIDC_ISSUER_DELETE, issuerRecordId.id); + const createTemplateOnAgent = await this._deleteOidcIssuer(url, orgId); + if (!createTemplateOnAgent) { + throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); + } + const deleteOidcIssuer = await this.oid4vcIssuanceRepository.deleteOidcIssuer(id); + if (!deleteOidcIssuer) { + throw new NotFoundException(ResponseMessages.oidcIssuer.error.deleteFailed); + } + return deleteOidcIssuer; + } catch (error) { + if ('PrismaClientKnownRequestError' === error.name) { + throw new BadRequestException(error.meta?.cause ?? ResponseMessages.oidcIssuer.error.deleteFailed); + } + throw new Error(error.response ? error.response : error); + } + } + + async createTemplate( + credentialTemplate: CreateCredentialTemplate, + orgId: string, + issuerId: string + ): Promise { + try { + //TODO: add revert mechanism if agent call fails + const { name, description, format, canBeRevoked, appearance, signerOption } = credentialTemplate; + + const checkNameExist = await this.oid4vcIssuanceRepository.getTemplateByNameForIssuer(name, issuerId); + if (0 < checkNameExist.length) { + throw new ConflictException(ResponseMessages.oidcTemplate.error.templateNameAlreadyExist); + } + const metadata = { + name, + description, + format: format.toString(), + canBeRevoked, + attributes: instanceToPlain(credentialTemplate.template), + appearance: appearance ?? {}, + issuerId, + signerOption + }; + // Persist in DB + const createdTemplate = await this.oid4vcIssuanceRepository.createTemplate(issuerId, metadata); + if (!createdTemplate) { + throw new InternalServerErrorException(ResponseMessages.oidcTemplate.error.createFailed); + } + + let createTemplateOnAgent; + try { + const issuerTemplateConfig = await this.buildOidcIssuerConfig(issuerId); + console.log(`service - createTemplate: `, JSON.stringify(issuerTemplateConfig)); + const agentDetails = await this.oid4vcIssuanceRepository.getAgentEndPoint(orgId); + if (!agentDetails) { + throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); + } + const { agentEndPoint } = agentDetails; + const issuerDetails = await this.oid4vcIssuanceRepository.getOidcIssuerDetailsById(issuerId); + if (!issuerDetails) { + throw new NotFoundException(ResponseMessages.oidcTemplate.error.issuerDetailsNotFound); + } + const url = await getAgentUrl( + agentEndPoint, + CommonConstants.OIDC_ISSUER_TEMPLATE, + issuerDetails.publicIssuerId + ); + createTemplateOnAgent = await this._createOIDCTemplate(issuerTemplateConfig, url, orgId); + } catch (agentError) { + try { + await this.oid4vcIssuanceRepository.deleteTemplate(createdTemplate.id); + this.logger.log(`${ResponseMessages.oidcTemplate.success.deleteTemplate}${createdTemplate.id}`); + throw new RpcException(agentError?.response ?? agentError); + } catch (cleanupError) { + this.logger.error( + `${ResponseMessages.oidcTemplate.error.failedDeleteTemplate}${createdTemplate.id} deleteError=${JSON.stringify( + cleanupError + )} originalAgentError=${JSON.stringify(agentError)}` + ); + throw new RpcException('Template creation failed and cleanup also failed'); + } + } + console.log('createTemplateOnAgent::::::::::::::', createTemplateOnAgent); + if (!createTemplateOnAgent) { + throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); + } + return createdTemplate; + } catch (error) { + this.logger.error(`[createTemplate] - error: ${JSON.stringify(error)}`); + throw new RpcException(error.response ?? error); + } + } + + async updateTemplate( + templateId: string, + updateCredentialTemplate: UpdateCredentialTemplate, + orgId: string, + issuerId: string + ): Promise { + try { + const template = await this.oid4vcIssuanceRepository.getTemplateById(templateId); + if (!template) { + throw new NotFoundException(ResponseMessages.oidcTemplate.error.notFound); + } + if (updateCredentialTemplate.name) { + const checkNameExist = await this.oid4vcIssuanceRepository.getTemplateByNameForIssuer( + updateCredentialTemplate.name, + issuerId + ); + if (0 < checkNameExist.length && !checkNameExist.some((item) => item.id === templateId)) { + throw new ConflictException(ResponseMessages.oidcTemplate.error.templateNameAlreadyExist); + } + } + const normalized = { + ...updateCredentialTemplate, + ...(issuerId ? { issuerId } : {}) + }; + const { name, description, format, canBeRevoked, appearance } = normalized; + const attributes = instanceToPlain(normalized.template); + + const payload = { + ...(name !== undefined ? { name } : {}), + ...(description !== undefined ? { description } : {}), + ...(format !== undefined ? { format } : {}), + ...(canBeRevoked !== undefined ? { canBeRevoked } : {}), + ...(attributes !== undefined ? { attributes } : {}), + ...(appearance !== undefined ? { appearance } : {}), + ...(issuerId ? { issuerId } : {}) + }; + + const updatedTemplate = await this.oid4vcIssuanceRepository.updateTemplate(templateId, payload); + + try { + const templates = await this.oid4vcIssuanceRepository.getTemplatesByIssuerId(issuerId); + if (!templates || 0 === templates.length) { + throw new NotFoundException(ResponseMessages.issuance.error.notFound); + } + const issuerTemplateConfig = await this.buildOidcIssuerConfig(issuerId); + const agentDetails = await this.oid4vcIssuanceRepository.getAgentEndPoint(orgId); + if (!agentDetails) { + throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); + } + const { agentEndPoint } = agentDetails; + const issuerDetails = await this.oid4vcIssuanceRepository.getOidcIssuerDetailsById(issuerId); + if (!issuerDetails) { + throw new NotFoundException(ResponseMessages.oidcTemplate.error.issuerDetailsNotFound); + } + const url = await getAgentUrl( + agentEndPoint, + CommonConstants.OIDC_ISSUER_TEMPLATE, + issuerDetails.publicIssuerId + ); + + const createTemplateOnAgent = await this._createOIDCTemplate(issuerTemplateConfig, url, orgId); + if (!createTemplateOnAgent) { + throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); + } + } catch (agentError) { + this.logger.error(`[updateTemplate] - error updating template on agent: ${JSON.stringify(agentError)}`); + try { + const rollbackPayload = { + name: template.name, + description: template.description, + format: template.format, + canBeRevoked: template.canBeRevoked, + attributes: template.attributes, + appearance: template.appearance, + issuerId: template.issuerId + }; + await this.oid4vcIssuanceRepository.updateTemplate(templateId, rollbackPayload); + this.logger.log(`Rolled back template ${templateId} to previous state after agent error`); + throw new RpcException(agentError?.response ?? agentError); + } catch (revertError) { + this.logger.error( + `[updateTemplate] - rollback failed for template ${templateId}: ${JSON.stringify(revertError)} originalAgentError=${JSON.stringify( + agentError + )}` + ); + const wrappedError = { + message: 'Template update failed and rollback also failed', + agentError: agentError?.response ?? agentError, + rollbackError: revertError?.response ?? revertError + }; + throw new RpcException(wrappedError); + } + } + + return updatedTemplate; + } catch (error) { + this.logger.error(`[updateTemplate] - error: ${JSON.stringify(error)}`); + throw new RpcException(error.response ?? error); + } + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async deleteTemplate(templateId: string, orgId: string, userDetails: user, issuerId: string): Promise { + try { + const template = await this.oid4vcIssuanceRepository.getTemplateById(templateId); + if (!template) { + throw new NotFoundException(ResponseMessages.oidcTemplate.error.notFound); + } + const deleteTemplate = await this.oid4vcIssuanceRepository.deleteTemplate(templateId); + if (!deleteTemplate) { + throw new NotFoundException(ResponseMessages.oidcTemplate.error.deleteTemplate); + } + + const issuerTemplateConfig = await this.buildOidcIssuerConfig(issuerId); + const agentDetails = await this.oid4vcIssuanceRepository.getAgentEndPoint(orgId); + if (!agentDetails) { + throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); + } + const { agentEndPoint } = agentDetails; + const issuerDetails = await this.oid4vcIssuanceRepository.getOidcIssuerDetailsById(issuerId); + if (!issuerDetails) { + throw new NotFoundException(ResponseMessages.oidcTemplate.error.issuerDetailsNotFound); + } + const url = await getAgentUrl(agentEndPoint, CommonConstants.OIDC_ISSUER_TEMPLATE, issuerDetails.publicIssuerId); + + const createTemplateOnAgent = await this._createOIDCTemplate(issuerTemplateConfig, url, orgId); + if (!createTemplateOnAgent) { + throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); + } + return deleteTemplate; + } catch (error) { + this.logger.error(`[deleteTemplate] - error: ${JSON.stringify(error)}`); + throw new RpcException(error.response ?? error); + } + } + + async findByIdTemplate(templateId: string, orgId: string): Promise { + try { + if (!orgId || !templateId) { + throw new BadRequestException(ResponseMessages.oidcTemplate.error.invalidId); + } + const template = await this.oid4vcIssuanceRepository.getTemplateById(templateId); + if (!template) { + throw new NotFoundException(ResponseMessages.oidcTemplate.error.notFound); + } + return template; + } catch (error) { + this.logger.error(`[findByIdTemplate] - error: ${JSON.stringify(error)}`); + throw new RpcException(error.response ?? error); + } + } + + async createOidcCredentialOffer( + createOidcCredentialOffer: CreateOidcCredentialOffer, + orgId: string, + userDetails: user, + issuerId: string + ): Promise { + try { + const filterTemplateIds = extractTemplateIds(createOidcCredentialOffer); + if (!filterTemplateIds) { + throw new BadRequestException('Please provide a valid id'); + } + const getAllOfferTemplates = await this.oid4vcIssuanceRepository.getTemplateByIds(filterTemplateIds, issuerId); + if (!getAllOfferTemplates) { + throw new NotFoundException('No templates found for the issuer'); + } + + const agentDetails = await this.oid4vcIssuanceRepository.getAgentEndPoint(orgId); + if (!agentDetails) { + throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); + } + //TDOD: signerOption should be under credentials change this with x509 support + + //TDOD: signerOption should be under credentials change this with x509 support + const signerOptions = []; + const activeCertificateDetails: X509CertificateRecord[] = []; + for (const template of getAllOfferTemplates) { + if (template.signerOption === SignerOption.DID) { + signerOptions.push({ + method: SignerMethodOption.DID, + did: agentDetails.orgDid + }); + } + + if (template.signerOption == SignerOption.X509_P256) { + const activeCertificate = await this.oid4vcIssuanceRepository.getCurrentActiveCertificate( + orgId, + x5cKeyType.P256 + ); + + if (!activeCertificate) { + throw new NotFoundException('No active certificate(p256) found for issuer'); + } + signerOptions.push({ + method: SignerMethodOption.X5C, + x5c: [activeCertificate.certificateBase64] + }); + activeCertificateDetails.push(activeCertificate); + } + + if (template.signerOption == SignerOption.X509_ED25519) { + const activeCertificate = await this.oid4vcIssuanceRepository.getCurrentActiveCertificate( + orgId, + x5cKeyType.Ed25519 + ); + + if (!activeCertificate) { + throw new NotFoundException('No active certificate(ed25519) found for issuer'); + } + signerOptions.push({ + method: SignerMethodOption.X5C, + x5c: [activeCertificate.certificateBase64] + }); + activeCertificateDetails.push(activeCertificate); + } + } + //TODO: Implement x509 support and discuss with team + //TODO: add logic to pass the issuer info + const issuerDetailsFromDb = await this.oid4vcIssuanceRepository.getOidcIssuerDetailsById(issuerId); + const { publicIssuerId, authorizationServerUrl } = issuerDetailsFromDb || {}; + const buildOidcCredentialOffer: CredentialOfferPayload = buildCredentialOfferPayload( + createOidcCredentialOffer, + // + getAllOfferTemplates, + { + publicId: publicIssuerId, + authorizationServerUrl: `${authorizationServerUrl}/oid4vci/${publicIssuerId}` + }, + signerOptions as any, + activeCertificateDetails + ); + console.log('This is the buildOidcCredentialOffer:', JSON.stringify(buildOidcCredentialOffer, null, 2)); + + if (!buildOidcCredentialOffer) { + throw new BadRequestException('Error while creating oid4vc credential offer'); + } + const issuerDetails = await this.oid4vcIssuanceRepository.getOidcIssuerDetailsById(issuerId); + if (!issuerDetails) { + throw new NotFoundException(ResponseMessages.oidcTemplate.error.issuerDetailsNotFound); + } + buildOidcCredentialOffer.publicIssuerId = issuerDetails.publicIssuerId; + const url = await getAgentUrl( + await this.getAgentEndpoint(orgId), + CommonConstants.OIDC_ISSUER_SESSIONS_CREDENTIAL_OFFER, + issuerDetails.publicIssuerId + ); + + const createCredentialOfferOnAgent = await this._oidcCreateCredentialOffer(buildOidcCredentialOffer, url, orgId); + if (!createCredentialOfferOnAgent) { + throw new NotFoundException(ResponseMessages.oidcIssuerSession.error.errorCreateOffer); + } + + return createCredentialOfferOnAgent.response; + } catch (error) { + this.logger.error(`[createOidcCredentialOffer] - error: ${JSON.stringify(error)}`); + throw new RpcException(error.response ?? error); + } + } + + async createOidcCredentialOfferD2A(oidcCredentialD2APayload, orgId: string): Promise { + try { + for (const credential of oidcCredentialD2APayload.credentials) { + const { signerOptions } = credential; + + if (!signerOptions?.method) { + throw new BadRequestException(`signerOptions.method is required`); + } + if (signerOptions.method === SignerMethodOption.X5C) { + if (!signerOptions.x5c || 0 === signerOptions.x5c.length) { + // const x5cFromDb = await this.oid4vcIssuanceRepository.getIssuerX5c( + const x5cFromDb = 'Test'; + // If you want to use the actual DB call, uncomment and use: + // const x5cFromDb = await this.oid4vcIssuanceRepository.getIssuerX5c( + // oidcCredentialD2APayload.publicIssuerId, + // orgId + // ); + if (!x5cFromDb || 0 === x5cFromDb.length) { + throw new BadRequestException(`No x5c found for issuer`); + } + signerOptions.x5c = x5cFromDb; + } + } + + if (signerOptions.method === SignerMethodOption.DID) { + if (!signerOptions.did) { + const agentDetails = await this.oid4vcIssuanceRepository.getAgentEndPoint(orgId); + if (!agentDetails) { + throw new BadRequestException(`No DID found for issuer`); + } + signerOptions.did = agentDetails.orgDid; + } + } + } + + const url = await getAgentUrl( + await this.getAgentEndpoint(orgId), + CommonConstants.OIDC_ISSUER_SESSIONS_CREDENTIAL_OFFER + ); + const createCredentialOfferOnAgent = await this._oidcCreateCredentialOffer(oidcCredentialD2APayload, url, orgId); + if (!createCredentialOfferOnAgent) { + throw new NotFoundException(ResponseMessages.oidcIssuerSession.error.errorCreateOffer); + } + console.log('This is the createCredentialOfferOnAgent:', JSON.stringify(createCredentialOfferOnAgent, null, 2)); + + return createCredentialOfferOnAgent.response; + } catch (error) { + this.logger.error(`[createOidcCredentialOffer] - error: ${JSON.stringify(error)}`); + throw new RpcException(error.response ?? error); + } + } + + async updateOidcCredentialOffer( + updateOidcCredentialOffer: UpdateCredentialRequest, + orgId: string, + issuerId: string + ): Promise { + try { + if (!updateOidcCredentialOffer.issuerMetadata) { + throw new BadRequestException('Please provide a valid issuerMetadata'); + } + const url = await getAgentUrl( + await this.getAgentEndpoint(orgId), + CommonConstants.OIDC_ISSUER_SESSIONS_UPDATE_OFFER, + updateOidcCredentialOffer.credentialOfferId + ); + const updateCredentialOfferOnAgent = await this._oidcUpdateCredentialOffer( + updateOidcCredentialOffer.issuerMetadata, + url, + orgId + ); + if (!updateCredentialOfferOnAgent) { + throw new NotFoundException(ResponseMessages.oidcIssuerSession.error.errorUpdateOffer); + } + + return updateCredentialOfferOnAgent.response; + } catch (error) { + throw new RpcException(error.response ?? error); + } + } + + async getCredentialOfferDetailsById(offerId: string, orgId: string): Promise { + try { + const url = await getAgentUrl( + await this.getAgentEndpoint(orgId), + CommonConstants.OIDC_ISSUER_SESSIONS_BY_ID, + offerId + ); + const offer = await this._oidcGetCredentialOfferById(url, orgId); + if ('string' === typeof offer.response) { + offer.response = JSON.parse(offer.response); + } + return offer.response; + } catch (error) { + this.logger.error(`[getCredentialOfferDetailsById] - error: ${JSON.stringify(error)}`); + throw new RpcException(error.response ?? error); + } + } + + async getCredentialOffers(orgId: string, getAllCredentialOffer: GetAllCredentialOffer): Promise { + try { + const url = await getAgentUrl(await this.getAgentEndpoint(orgId), CommonConstants.OIDC_ISSUER_SESSIONS); + const credentialOfferUrl = buildCredentialOfferUrl(url, getAllCredentialOffer); + const offers = await this._oidcGetCredentialOffers(credentialOfferUrl, orgId); + if ('string' === typeof offers.response) { + offers.response = JSON.parse(offers.response); + } + return offers.response; + } catch (error) { + this.logger.error(`[getCredentialOffers] - error: ${JSON.stringify(error)}`); + throw new RpcException(error.response ?? error); + } + } + async deleteCredentialOffers(orgId: string, credentialId: string): Promise { + try { + if (!credentialId) { + throw new BadRequestException('Please provide a valid credentialId'); + } + const url = await getAgentUrl( + await this.getAgentEndpoint(orgId), + CommonConstants.OIDC_DELETE_CREDENTIAL_OFFER, + credentialId + ); + const deletedCredentialOffer = await this._oidcDeleteCredentialOffer(url, orgId); + if (!deletedCredentialOffer) { + throw new NotFoundException(ResponseMessages.oidcIssuerSession.error.deleteFailed); + } + if ('string' === typeof deletedCredentialOffer.response) { + deletedCredentialOffer.response = JSON.parse(deletedCredentialOffer.response); + } + return deletedCredentialOffer.response; + } catch (error) { + this.logger.error(`[getCredentialOffers] - error: ${JSON.stringify(error)}`); + throw new RpcException(error.response ?? error); + } + } + + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + async buildOidcIssuerConfig(issuerId: string) { + try { + const issuerDetails = await this.oid4vcIssuanceRepository.getOidcIssuerDetailsById(issuerId); + const templates = await this.oid4vcIssuanceRepository.getTemplatesByIssuerId(issuerId); + + const credentialConfigurationsSupported = buildCredentialConfigurationsSupported(templates); + + return buildIssuerPayload({ credentialConfigurationsSupported }, issuerDetails); + } catch (error) { + this.logger.error(`[buildOidcIssuerPayload] - error: ${JSON.stringify(error)}`); + throw new RpcException(error.response ?? error); + } + } + + async findAllTemplate(orgId: string, issuerId: string): Promise { + try { + if (!orgId || !issuerId) { + throw new BadRequestException(ResponseMessages.oidcTemplate.error.invalidId); + } + return this.oid4vcIssuanceRepository.getTemplatesByIssuerId(issuerId); + } catch (error) { + this.logger.error(`[findAllTemplate] - error: ${JSON.stringify(error)}`); + throw new RpcException(error.response ?? error); + } + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async _createOIDCIssuer(issuerCreation, url: string, orgId: string): Promise { + try { + const pattern = { cmd: 'agent-create-oid4vc-issuer' }; + const payload: IAgentOIDCIssuerCreate = { issuerCreation, url, orgId }; + return this.natsCall(pattern, payload); + } catch (error) { + this.logger.error(`[_createOIDCIssuer] [NATS call]- error in create OID4VC Issuer : ${JSON.stringify(error)}`); + throw error; + } + } + + async _createOIDCTemplate(templatePayload, url: string, orgId: string): Promise { + try { + const pattern = { cmd: 'agent-create-oid4vc-template' }; + const payload = { templatePayload, url, orgId }; + return this.natsCall(pattern, payload); + } catch (error) { + this.logger.error( + `[_createOIDCTemplate] [NATS call]- error in create OID4VC Template : ${JSON.stringify(error)}` + ); + throw error; + } + } + + async _deleteOidcIssuer(url: string, orgId: string): Promise { + try { + const pattern = { cmd: 'delete-oid4vc-issuer' }; + const payload = { url, orgId }; + return this.natsCall(pattern, payload); + } catch (error) { + this.logger.error( + `[_createOIDCTemplate] [NATS call]- error in create OID4VC Template : ${JSON.stringify(error)}` + ); + throw error; + } + } + + async _oidcGetIssuerById(url: string, orgId: string) { + try { + const pattern = { cmd: 'oid4vc-get-issuer-by-id' }; + const payload = { url, orgId }; + return this.natsCall(pattern, payload); + } catch (error) { + this.logger.error( + `[_oidcGetIssuerById] [NATS call]- error in oid4vc get issuer by id : ${JSON.stringify(error)}` + ); + throw error; + } + } + + async _oidcGetIssuers(url: string, orgId: string) { + try { + const pattern = { cmd: 'oid4vc-get-issuers-agent-service' }; + const payload = { url, orgId }; + return this.natsCall(pattern, payload); + } catch (error) { + this.logger.error(`[_oidcGetIssuers] [NATS call]- error in oid4vc get issuers : ${JSON.stringify(error)}`); + throw error; + } + } + + async _oidcCreateCredentialOffer(credentialPayload: CredentialOfferPayload, url: string, orgId: string) { + try { + const pattern = { cmd: 'agent-service-oid4vc-create-credential-offer' }; + const payload = { credentialPayload, url, orgId }; + return this.natsCall(pattern, payload); + } catch (error) { + this.logger.error( + `[_oidcCreateCredentialOffer] [NATS call]- error in oid4vc create credential offer : ${JSON.stringify(error)}` + ); + throw error; + } + } + + async _oidcUpdateCredentialOffer(issuanceMetadata, url: string, orgId: string) { + try { + const pattern = { cmd: 'agent-service-oid4vc-update-credential-offer' }; + const payload = { issuanceMetadata, url, orgId }; + return this.natsCall(pattern, payload); + } catch (error) { + this.logger.error( + `[_oidcUpdateCredentialOffer] [NATS call]- error in oid4vc update credential offer : ${JSON.stringify(error)}` + ); + throw error; + } + } + + async _oidcGetCredentialOfferById(url: string, orgId: string) { + try { + const pattern = { cmd: 'agent-service-oid4vc-get-credential-offer-by-id' }; + const payload = { url, orgId }; + return this.natsCall(pattern, payload); + } catch (error) { + this.logger.error( + `[_oidcGetCredentialOfferById] [NATS call]- error in oid4vc get credential offer by id : ${JSON.stringify(error)}` + ); + throw error; + } + } + + async _oidcGetCredentialOffers(url: string, orgId: string) { + try { + const pattern = { cmd: 'agent-service-oid4vc-get-credential-offers' }; + const payload = { url, orgId }; + return this.natsCall(pattern, payload); + } catch (error) { + this.logger.error( + `[_oidcGetCredentialOffers] [NATS call]- error in oid4vc get credential offers : ${JSON.stringify(error)}` + ); + throw error; + } + } + + async _oidcDeleteCredentialOffer(url: string, orgId: string) { + try { + const pattern = { cmd: 'agent-service-oid4vc-delete-credential-offer' }; + const payload = { url, orgId }; + return this.natsCall(pattern, payload); + } catch (error) { + this.logger.error( + `[_oidcDeleteCredentialOffer] [NATS call]- error in oid4vc delete credential offer : ${JSON.stringify(error)}` + ); + throw error; + } + } + + async natsCall( + pattern: object, + payload: object + ): Promise<{ + response: string; + }> { + try { + return this.issuanceServiceProxy + .send(pattern, payload) + .pipe( + map((response) => ({ + response + })) + ) + .toPromise() + .catch((error) => { + this.logger.error(`catch: ${JSON.stringify(error)}`); + throw new HttpException( + { + status: error.statusCode, + error: error.message + }, + error.error + ); + }); + } catch (error) { + this.logger.error(`[natsCall] - error in nats call : ${JSON.stringify(error)}`); + throw error; + } + } + + async getAgentEndpoint(orgId: string): Promise { + const agentDetails = await this.oid4vcIssuanceRepository.getAgentEndPoint(orgId); + + if (!agentDetails) { + throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); + } + + if (!agentDetails.agentEndPoint || '' === agentDetails.agentEndPoint.trim()) { + throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); + } + + return agentDetails.agentEndPoint; + } + + async storeOidcCredentialWebhook( + CredentialOfferWebhookPayload: Oid4vcCredentialOfferWebhookPayload + ): Promise { + try { + // pick fields + let organisationId: string; + const { oidcIssueCredentialDto, id } = CredentialOfferWebhookPayload; + + if ('default' !== oidcIssueCredentialDto?.contextCorrelationId) { + const getOrganizationId = await this.oid4vcIssuanceRepository.getOrganizationByTenantId( + oidcIssueCredentialDto?.contextCorrelationId + ); + organisationId = getOrganizationId?.orgId; + } else { + organisationId = id; + } + + const { + contextCorrelationId, + credentialOfferPayload, + issuedCredentials, + id: issuanceSessionId + } = oidcIssueCredentialDto ?? {}; + const cfgIds: string[] = Array.isArray(credentialOfferPayload?.credential_configuration_ids) + ? credentialOfferPayload.credential_configuration_ids + : []; + const issuedCredentialsArr: string[] | undefined = + Array.isArray(issuedCredentials) && 0 < issuedCredentials.length + ? issuedCredentials.map((c: any) => ('string' === typeof c ? c : JSON.stringify(c))) + : issuedCredentials && Array.isArray(issuedCredentials) && 0 === issuedCredentials.length + ? [] + : undefined; + + const sanitized = { + ...CredentialOfferWebhookPayload, + credentialOfferPayload: { + credential_configuration_ids: cfgIds + } + }; + + const agentDetails = await this.oid4vcIssuanceRepository.storeOidcCredentialDetails( + CredentialOfferWebhookPayload, + organisationId + ); + return agentDetails; + } catch (error) { + this.logger.error(`[storeOidcCredentialWebhook] - error: ${JSON.stringify(error)}`); + throw error; + } + } +} diff --git a/apps/oid4vc-issuance/test/app.e2e-spec.ts b/apps/oid4vc-issuance/test/app.e2e-spec.ts new file mode 100644 index 000000000..37bf09beb --- /dev/null +++ b/apps/oid4vc-issuance/test/app.e2e-spec.ts @@ -0,0 +1,19 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { INestApplication } from '@nestjs/common'; +import * as request from 'supertest'; +import { Oid4vcIssuanceModule } from './../src/oid4vc-issuance.module'; + +describe('Oid4vcIssuanceController (e2e)', () => { + let app: INestApplication; + + beforeEach(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [Oid4vcIssuanceModule] + }).compile(); + + app = moduleFixture.createNestApplication(); + await app.init(); + }); + + it('/ (GET)', () => request(app.getHttpServer()).get('/').expect(200).expect('Hello World!')); +}); diff --git a/apps/oid4vc-issuance/test/jest-e2e.json b/apps/oid4vc-issuance/test/jest-e2e.json new file mode 100644 index 000000000..e9d912f3e --- /dev/null +++ b/apps/oid4vc-issuance/test/jest-e2e.json @@ -0,0 +1,9 @@ +{ + "moduleFileExtensions": ["js", "json", "ts"], + "rootDir": ".", + "testEnvironment": "node", + "testRegex": ".e2e-spec.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + } +} diff --git a/apps/oid4vc-issuance/tsconfig.app.json b/apps/oid4vc-issuance/tsconfig.app.json new file mode 100644 index 000000000..b5f8c422e --- /dev/null +++ b/apps/oid4vc-issuance/tsconfig.app.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": false, + "outDir": "../../dist/apps/oid4vc-issuance" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] +} diff --git a/apps/oid4vc-verification/interfaces/oid4vp-verification-sessions.interfaces.ts b/apps/oid4vc-verification/interfaces/oid4vp-verification-sessions.interfaces.ts new file mode 100644 index 000000000..b6db519a3 --- /dev/null +++ b/apps/oid4vc-verification/interfaces/oid4vp-verification-sessions.interfaces.ts @@ -0,0 +1,23 @@ +import { SignerMethodOption } from '@credebl/enum/enum'; + +export interface Oid4vpPresentationWh { + id: string; + state: string; + createdAt: string; + updatedAt: string; + contextCorrelationId: string; + authorizationRequestId: string; + verifierId: string; +} + +export interface DidSigner { + method: SignerMethodOption.DID; + didUrl: string; +} + +export interface X5cSigner { + method: SignerMethodOption.X5C; + x5c: string[]; +} + +export type RequestSigner = DidSigner | X5cSigner; diff --git a/apps/oid4vc-verification/interfaces/oid4vp-verifier.interfaces.ts b/apps/oid4vc-verification/interfaces/oid4vp-verifier.interfaces.ts new file mode 100644 index 000000000..c39d7ae26 --- /dev/null +++ b/apps/oid4vc-verification/interfaces/oid4vp-verifier.interfaces.ts @@ -0,0 +1,27 @@ +import { OpenId4VcVerificationPresentationState } from '@credebl/common/interfaces/oid4vp-verification'; +import { organisation } from '@prisma/client'; +export interface OrgAgent { + organisation: organisation; + id: string; + createDateTime: Date; + createdBy: string; + lastChangedDateTime: Date; + lastChangedBy: string; + orgDid: string; + verkey: string; + agentEndPoint: string; + agentId: string; + isDidPublic: boolean; + ledgerId: string; + orgAgentTypeId: string; + tenantId: string; +} + +export interface VerificationSessionQuery { + publicVerifierId?: string; + payloadState?: string; + state?: OpenId4VcVerificationPresentationState; + authorizationRequestUri?: string; + nonce?: string; + id?: string; +} diff --git a/apps/oid4vc-verification/src/main.ts b/apps/oid4vc-verification/src/main.ts new file mode 100644 index 000000000..0aec6f280 --- /dev/null +++ b/apps/oid4vc-verification/src/main.ts @@ -0,0 +1,23 @@ +import { NestFactory } from '@nestjs/core'; +import { Logger } from '@nestjs/common'; +import { MicroserviceOptions, Transport } from '@nestjs/microservices'; +import { getNatsOptions } from '@credebl/common/nats.config'; +import { CommonConstants } from '@credebl/common/common.constant'; +import NestjsLoggerServiceAdapter from '@credebl/logger/nestjsLoggerServiceAdapter'; +import { Oid4vpModule } from './oid4vc-verification.module'; + +const logger = new Logger(); + +async function bootstrap(): Promise { + const app = await NestFactory.createMicroservice(Oid4vpModule, { + transport: Transport.NATS, + options: getNatsOptions(CommonConstants.OIDC4VC_VERIFICATION_SERVICE, process.env.OIDC4VC_VERIFICATION_NKEY_SEED) + }); + app.useLogger(app.get(NestjsLoggerServiceAdapter)); + // TODO: Not sure if we want the below + // app.useGlobalFilters(new HttpExceptionFilter()); + + await app.listen(); + logger.log('OID4VC-Verification-Service Microservice is listening to NATS '); +} +bootstrap(); diff --git a/apps/oid4vc-verification/src/oid4vc-verification.controller.ts b/apps/oid4vc-verification/src/oid4vc-verification.controller.ts new file mode 100644 index 000000000..8f949a41e --- /dev/null +++ b/apps/oid4vc-verification/src/oid4vc-verification.controller.ts @@ -0,0 +1,105 @@ +import { Controller, Logger } from '@nestjs/common'; +import { Oid4vpVerificationService } from './oid4vc-verification.service'; +import { user } from '@prisma/client'; +import { CreateVerifier, UpdateVerifier } from '@credebl/common/interfaces/oid4vp-verification'; +import { MessagePattern } from '@nestjs/microservices'; +import { VerificationSessionQuery } from '../interfaces/oid4vp-verifier.interfaces'; +import { Oid4vpPresentationWh } from '../interfaces/oid4vp-verification-sessions.interfaces'; + +@Controller() +export class Oid4vpVerificationController { + constructor( + private readonly oid4vpVerificationService: Oid4vpVerificationService, + private logger: Logger + ) {} + + @MessagePattern({ cmd: 'oid4vp-verifier-create' }) + async oid4vpCreateVerifier(payload: { + createVerifier: CreateVerifier; + orgId: string; + userDetails: user; + }): Promise { + const { createVerifier, orgId, userDetails } = payload; + this.logger.debug( + `[oid4vpCreateVerifier] Received 'oid4vp-verifier-create' request for orgId=${orgId}, user=${userDetails?.id}` + ); + return this.oid4vpVerificationService.oid4vpCreateVerifier(createVerifier, orgId, userDetails); + } + + @MessagePattern({ cmd: 'oid4vp-verifier-update' }) + async oid4vpUpdateVerifier(payload: { + updateVerifier: UpdateVerifier; + orgId: string; + verifierId: string; + userDetails: user; + }): Promise { + const { updateVerifier, orgId, verifierId, userDetails } = payload; + this.logger.debug( + `[oid4vpUpdateVerifier] Received 'oid4vp-verifier-update' for orgId=${orgId}, verifierId=${verifierId}, user=${userDetails?.id ?? 'unknown'}` + ); + return this.oid4vpVerificationService.oid4vpUpdateVerifier(updateVerifier, orgId, verifierId, userDetails); + } + + @MessagePattern({ cmd: 'oid4vp-verifier-get' }) + async oid4vpGetVerifier(payload: { orgId: string; verifierId?: string }): Promise { + const { orgId, verifierId } = payload; + this.logger.debug( + `[oid4vpGetVerifier] Received 'oid4vp-verifier-get' for orgId=${orgId}, verifierId=${verifierId ?? 'all'}` + ); + return this.oid4vpVerificationService.getVerifierById(orgId, verifierId); + } + + @MessagePattern({ cmd: 'oid4vp-verifier-delete' }) + async oid4vpDeleteVerifier(payload: { orgId: string; verifierId: string }): Promise { + const { orgId, verifierId } = payload; + this.logger.debug( + `[oid4vpDeleteVerifier]Received 'oid4vp-verifier-delete' for orgId=${orgId}, verifierId=${verifierId}` + ); + return this.oid4vpVerificationService.deleteVerifierById(orgId, verifierId); + } + + @MessagePattern({ cmd: 'oid4vp-verifier-session-get' }) + async oid4vpGetVerifierSession(payload: { orgId: string; query?: VerificationSessionQuery }): Promise { + const { orgId, query } = payload; + this.logger.debug(`[oid4vpGetVerifierSession] Received 'oid4vp-verifier-session-get' for orgId=${orgId}`); + return this.oid4vpVerificationService.getVerifierSession(orgId, query); + } + + @MessagePattern({ cmd: 'oid4vp-verifier-session-response-get' }) + async getVerificationSessionResponse(payload: { orgId: string; verificationSessionId: string }): Promise { + const { orgId, verificationSessionId } = payload; + this.logger.debug( + `[getVerificationSessionResponse] Received 'oid4vp-verifier-session-response-get' for orgId=${orgId}, verificationSessionId=${verificationSessionId}` + ); + return this.oid4vpVerificationService.getVerificationSessionResponse(orgId, verificationSessionId); + } + + @MessagePattern({ cmd: 'oid4vp-verification-session-create' }) + // TODO: change name + async oid4vpCreateVerificationSession(payload: { + orgId: string; + verifierId: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + sessionRequest: any; + userDetails: user; + }): Promise { + const { orgId, verifierId, sessionRequest, userDetails } = payload; + this.logger.debug( + `[oid4vpCreateVerificationSession] Received 'oid4vp-verification-session-create' for orgId=${orgId}, verifierId=${verifierId}, user=${userDetails?.id ?? 'unknown'}` + ); + return this.oid4vpVerificationService.oid4vpCreateVerificationSession( + orgId, + verifierId, + sessionRequest, + userDetails + ); + } + + @MessagePattern({ cmd: 'webhook-oid4vp-presentation' }) + async oid4vpPresentationWebhook(payload: { + oid4vpPresentationWhDto: Oid4vpPresentationWh; + id: string; + }): Promise { + return this.oid4vpVerificationService.oid4vpPresentationWebhook(payload.oid4vpPresentationWhDto, payload.id); + } +} diff --git a/apps/oid4vc-verification/src/oid4vc-verification.module.ts b/apps/oid4vc-verification/src/oid4vc-verification.module.ts new file mode 100644 index 000000000..398b3482e --- /dev/null +++ b/apps/oid4vc-verification/src/oid4vc-verification.module.ts @@ -0,0 +1,52 @@ +import { Logger, Module } from '@nestjs/common'; +import { Oid4vpVerificationController } from './oid4vc-verification.controller'; +import { Oid4vpVerificationService } from './oid4vc-verification.service'; +import { ClientsModule, Transport } from '@nestjs/microservices'; +import { getNatsOptions } from '@credebl/common/nats.config'; +import { CommonModule } from '@credebl/common'; +import { CommonConstants, MICRO_SERVICE_NAME } from '@credebl/common/common.constant'; +import { GlobalConfigModule } from '@credebl/config'; +import { ContextInterceptorModule } from '@credebl/context'; +import { LoggerModule } from '@credebl/logger/logger.module'; +import { CacheModule } from '@nestjs/cache-manager'; +import { ConfigModule as PlatformConfig } from '@credebl/config/config.module'; +import { NATSClient } from '@credebl/common/NATSClient'; +import { PrismaService, PrismaServiceModule } from '@credebl/prisma-service'; +import { Oid4vpRepository } from './oid4vc-verification.repository'; +import { ConfigModule } from '@nestjs/config'; + +@Module({ + imports: [ + ConfigModule.forRoot(), + ContextInterceptorModule, + PlatformConfig, + LoggerModule, + CacheModule.register(), + ClientsModule.register([ + { + name: 'NATS_CLIENT', + transport: Transport.NATS, + options: getNatsOptions( + CommonConstants.OIDC4VC_VERIFICATION_SERVICE, + process.env.OIDC4VC_VERIFICATION_NKEY_SEED + ) + } + ]), + CommonModule, + GlobalConfigModule, + PrismaServiceModule + ], + controllers: [Oid4vpVerificationController], + providers: [ + Oid4vpVerificationService, + Oid4vpRepository, + PrismaService, + Logger, + NATSClient, + { + provide: MICRO_SERVICE_NAME, + useValue: 'Oid4vc-verification-service' + } + ] +}) +export class Oid4vpModule {} diff --git a/apps/oid4vc-verification/src/oid4vc-verification.repository.ts b/apps/oid4vc-verification/src/oid4vc-verification.repository.ts new file mode 100644 index 000000000..d5d4fa4ca --- /dev/null +++ b/apps/oid4vc-verification/src/oid4vc-verification.repository.ts @@ -0,0 +1,262 @@ +/* eslint-disable camelcase */ +import { Injectable, Logger, NotFoundException } from '@nestjs/common'; +// eslint-disable-next-line camelcase +import { oid4vp_verifier, org_agents } from '@prisma/client'; +import { PrismaService } from '@credebl/prisma-service'; +import { ResponseMessages } from '@credebl/common/response-messages'; +import { OrgAgent } from '../interfaces/oid4vp-verifier.interfaces'; +import { Oid4vpPresentationWh } from '../interfaces/oid4vp-verification-sessions.interfaces'; +import { x5cKeyType, x5cRecordStatus } from '@credebl/enum/enum'; +import { X509CertificateRecord } from '@credebl/common/interfaces/x509.interface'; + +@Injectable() +export class Oid4vpRepository { + private readonly logger = new Logger('Oid4vpRepository'); + constructor(private readonly prisma: PrismaService) {} + + async getAgentEndPoint(orgId: string): Promise { + this.logger.debug(`[getAgentEndPoint] called with orgId=${orgId}`); + try { + const agentDetails = await this.prisma.org_agents.findFirst({ + where: { + orgId + }, + include: { + organisation: true + } + }); + + if (!agentDetails) { + this.logger.warn(`[getAgentEndPoint] No agent endpoint found for orgId=${orgId}`); + throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); + } + + this.logger.debug(`[getAgentEndPoint] Found agent endpoint with id=${agentDetails.id}`); + return agentDetails; + } catch (error) { + this.logger.error(`[getAgentEndPoint] Error in get getAgentEndPoint: ${error.message} `); + throw error; + } + } + + async getOrgAgentType(orgAgentId: string): Promise { + this.logger.debug(`[getOrgAgentType] called with orgAgentId=${orgAgentId}`); + try { + const { agent } = await this.prisma.org_agents_type.findFirst({ + where: { + id: orgAgentId + } + }); + + this.logger.debug(`[getOrgAgentType] Found type=${agent}`); + return agent; + } catch (error) { + this.logger.error(`[getOrgAgentType] - error: ${JSON.stringify(error)}`); + throw error; + } + } + + async getOrganizationByTenantId(tenantId: string): Promise { + this.logger.debug(`[getOrganizationByTenantId] called with tenantId=${tenantId}`); + try { + const record = await this.prisma.org_agents.findFirst({ + where: { + tenantId + } + }); + this.logger.debug(`[getOrganizationByTenantId] Found orgAgent id=${record?.id ?? 'none'}`); + return record; + } catch (error) { + this.logger.error( + `[getOrganizationByTenantId] Error in getOrganization in issuance repository: ${error.message} ` + ); + throw error; + } + } + + async createOid4vpVerifier(verifierDetails, orgId: string, userId: string): Promise { + this.logger.debug( + `[createOid4vpVerifier] called for orgId=${orgId}, userId=${userId}, verifierId=${verifierDetails?.verifierId}` + ); + try { + const { id, clientMetadata, verifierId } = verifierDetails; + const created = await this.prisma.oid4vp_verifier.create({ + data: { + metadata: clientMetadata, + publicVerifierId: verifierId, + verifierId: id, + createdBy: userId, + lastChangedBy: userId, + orgAgentId: orgId + } + }); + this.logger.debug(`[createOid4vpVerifier] Created verifier record with id=${created.id}`); + return created; + } catch (error) { + this.logger.error(`[createOid4vpVerifier] Error in createOid4vpVerifier: ${error?.message ?? error}`); + throw error; + } + } + + async updateOid4vpVerifier(verifierDetails, userId: string, verifierId: string): Promise { + this.logger.debug(`[updateOid4vpVerifier] called with verifierId=${verifierId}, userId=${userId}`); + try { + const { clientMetadata } = verifierDetails; + const updated = await this.prisma.oid4vp_verifier.update({ + data: { + metadata: clientMetadata, + lastChangedBy: userId + }, + where: { + id: verifierId + } + }); + this.logger.debug(`[updateOid4vpVerifier] Updated verifier id=${updated.id}`); + return updated; + } catch (error) { + this.logger.error(`[updateOid4vpVerifier] Error in createOid4vpVerifier: ${error.message}`); + throw error; + } + } + + async getVerifiersByPublicVerifierId(publicVerifierId: string): Promise { + this.logger.debug(`[getVerifiersByPublicVerifierId] called with publicVerifierId=${publicVerifierId}`); + try { + const result = await this.prisma.oid4vp_verifier.findMany({ + where: { + publicVerifierId + } + }); + this.logger.debug(`[getVerifiersByPublicVerifierId] Found ${result.length} records`); + return result; + } catch (error) { + this.logger.error(`[getVerifiersByPublicVerifierId] Error in getVerifiersByPublicVerifierId: ${error.message}`); + throw error; + } + } + + async getVerifiersByVerifierId(orgId: string, verifierId?: string): Promise { + this.logger.debug(`[getVerifiersByVerifierId] called with orgId=${orgId}, verifierId=${verifierId ?? 'N/A'}`); + try { + const result = await this.prisma.oid4vp_verifier.findMany({ + where: { + id: verifierId, + orgAgent: { + orgId + } + } + }); + this.logger.debug(`[getVerifiersByVerifierId] Found ${result.length} records`); + return result; + } catch (error) { + this.logger.error(`[getVerifiersByVerifierId] Error in getVerifiersByPublicVerifierId: ${error.message}`); + throw error; + } + } + + async getVerifierById(orgId: string, verifierId?: string): Promise { + this.logger.debug(`[getVerifierById] called with orgId=${orgId}, verifierId=${verifierId ?? 'N/A'}`); + try { + const result = await this.prisma.oid4vp_verifier.findUnique({ + where: { + id: verifierId, + orgAgent: { + orgId + } + } + }); + this.logger.debug(`[getVerifierById] Found record id=${result?.id ?? 'none'}`); + return result; + } catch (error) { + this.logger.error(`[getVerifierById] Error in getVerifiersByPublicVerifierId: ${error.message}`); + throw error; + } + } + + async deleteVerifierByVerifierId(orgId: string, verifierId?: string): Promise { + this.logger.debug(`[deleteVerifierByVerifierId] called with orgId=${orgId}, verifierId=${verifierId ?? 'N/A'}`); + try { + const deleted = await this.prisma.oid4vp_verifier.delete({ + where: { + id: verifierId, + orgAgent: { + orgId + } + } + }); + this.logger.debug(`[deleteVerifierByVerifierId] Deleted verifier id=${deleted?.id ?? 'none'}`); + return deleted; + } catch (error) { + this.logger.error(`[deleteVerifierByVerifierId] Error in deleteVerifierByVerifierId: ${error.message}`); + throw error; + } + } + + async storeOid4vpPresentationDetails( + oid4vpPresentationPayload: Oid4vpPresentationWh, + orgId: string + ): Promise { + try { + const { + state, + id: verificationSessionId, + contextCorrelationId, + authorizationRequestId, + verifierId + } = oid4vpPresentationPayload; + const credentialDetails = await this.prisma.oid4vp_presentations.upsert({ + where: { + verificationSessionId + }, + update: { + lastChangedBy: orgId, + state + }, + create: { + lastChangedBy: orgId, + createdBy: orgId, + state, + orgId, + contextCorrelationId, + verificationSessionId, + presentationId: authorizationRequestId, + publicVerifierId: verifierId + } + }); + + return credentialDetails; + } catch (error) { + this.logger.error(`Error in storeOid4vpPresentationDetails in oid4vp-presentation repository: ${error.message} `); + throw error; + } + } + + async getCurrentActiveCertificate(orgId: string, keyType: x5cKeyType): Promise { + try { + const now = new Date(); + + const certificate = await this.prisma.x509_certificates.findFirst({ + where: { + org_agents: { + orgId + }, + status: x5cRecordStatus.Active, + keyType, + validFrom: { + lte: now + }, + expiry: { + gte: now + } + }, + orderBy: { + createdAt: 'desc' + } + }); + return certificate; + } catch (error) { + this.logger.error(`Error in getCurrentActiveCertificate: ${error.message}`); + throw error; + } + } +} diff --git a/apps/oid4vc-verification/src/oid4vc-verification.service.ts b/apps/oid4vc-verification/src/oid4vc-verification.service.ts new file mode 100644 index 000000000..0aa2d631b --- /dev/null +++ b/apps/oid4vc-verification/src/oid4vc-verification.service.ts @@ -0,0 +1,477 @@ +/* eslint-disable quotes */ +/* eslint-disable no-useless-catch */ +/* eslint-disable camelcase */ +/* eslint-disable no-console */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/naming-convention, @typescript-eslint/explicit-function-return-type, @typescript-eslint/explicit-module-boundary-types, camelcase */ + +import { + BadRequestException, + ConflictException, + Inject, + Injectable, + InternalServerErrorException, + NotFoundException +} from '@nestjs/common'; +import { Oid4vpRepository } from './oid4vc-verification.repository'; +import { CommonConstants } from '@credebl/common/common.constant'; +import { ResponseMessages } from '@credebl/common/response-messages'; +import { ClientProxy, RpcException } from '@nestjs/microservices'; +import { getAgentUrl } from '@credebl/common/common.utils'; +import { SignerOption, user } from '@prisma/client'; +import { map } from 'rxjs'; +import { CreateVerifier, UpdateVerifier, VerifierRecord } from '@credebl/common/interfaces/oid4vp-verification'; +import { buildUrlWithQuery } from '@credebl/common/cast.helper'; +import { VerificationSessionQuery } from '../interfaces/oid4vp-verifier.interfaces'; +import { BaseService } from 'libs/service/base.service'; +import { NATSClient } from '@credebl/common/NATSClient'; + +import { Oid4vpPresentationWh, RequestSigner } from '../interfaces/oid4vp-verification-sessions.interfaces'; +import { X509CertificateRecord } from '@credebl/common/interfaces/x509.interface'; +import { SignerMethodOption } from '@credebl/enum/enum'; +@Injectable() +export class Oid4vpVerificationService extends BaseService { + constructor( + @Inject('NATS_CLIENT') private readonly oid4vpVerificationServiceProxy: ClientProxy, + private readonly natsClient: NATSClient, + private readonly oid4vpRepository: Oid4vpRepository + ) { + super('Oid4vpVerificationService'); + } + + async oid4vpCreateVerifier(createVerifier: CreateVerifier, orgId: string, userDetails: user): Promise { + this.logger.debug(`[oid4vpCreateVerifier] called for orgId=${orgId}, user=${userDetails?.id ?? 'unknown'}`); + try { + let createdVerifierDetails; + const { verifierId } = createVerifier; + this.logger.debug(`[oid4vpCreateVerifier] checking if verifierId=${verifierId} already exists`); + const checkIdExist = await this.oid4vpRepository.getVerifiersByPublicVerifierId(verifierId); + if (0 < checkIdExist.length) { + throw new ConflictException(ResponseMessages.oid4vp.error.verifierIdAlreadyExists); + } + + this.logger.debug(`[oid4vpCreateVerifier] fetching agent endpoint for orgId=${orgId}`); + const agentDetails = await this.oid4vpRepository.getAgentEndPoint(orgId); + if (!agentDetails) { + throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); + } + const { agentEndPoint, id } = agentDetails; + const url = await getAgentUrl(agentEndPoint, CommonConstants.OIDC_VERIFIER_CREATE); + this.logger.debug(`[oid4vpCreateVerifier] calling agent URL=${url}`); + + try { + createdVerifierDetails = await this._createOid4vpVerifier(createVerifier, url, orgId); + if (!createdVerifierDetails) { + throw new InternalServerErrorException(ResponseMessages.oid4vp.error.createFailed); + } + createdVerifierDetails = createdVerifierDetails as VerifierRecord; + this.logger.debug('[oid4vpCreateVerifier] verifier creation response received successfully from agent'); + } catch (error) { + const status409 = + 409 === error?.status?.message?.statusCode || 409 === error?.response?.status || 409 === error?.statusCode; + + if (status409) { + throw new ConflictException(`Verifier with id '${createdVerifierDetails.verifierId}' already exists`); + } + throw error; + } + + this.logger.debug(`[oid4vpCreateVerifier] saving verifier details for orgId=${orgId}`); + const saveVerifierDetails = await this.oid4vpRepository.createOid4vpVerifier( + createdVerifierDetails, + id, + userDetails.id + ); + if (!saveVerifierDetails) { + throw new InternalServerErrorException(ResponseMessages.oid4vp.error.createFailed); + } + + this.logger.debug(`[oid4vpCreateVerifier] verifier created successfully for orgId=${orgId}`); + return saveVerifierDetails; + } catch (error) { + this.logger.error( + `[oid4vpCreateVerifier] - error in oid4vpCreateVerifier issuance records: ${error?.response?.message ?? JSON.stringify(error?.response ?? error)}` + ); + throw new RpcException(error?.response ?? error); + } + } + + async oid4vpUpdateVerifier( + updateVerifier: UpdateVerifier, + orgId: string, + verifierId: string, + userDetails: user + ): Promise { + try { + let updatedVerifierDetails; + const existingVerifiers = await this.oid4vpRepository.getVerifiersByVerifierId(orgId, verifierId); + if (0 > existingVerifiers.length) { + throw new NotFoundException(ResponseMessages.oid4vp.error.notFound); + } + // updateVerifier['verifierId'] = existingVerifiers[0].publicVerifierId + const agentDetails = await this.oid4vpRepository.getAgentEndPoint(orgId); + if (!agentDetails) { + throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); + } + const { agentEndPoint, id } = agentDetails; + const url = await getAgentUrl( + agentEndPoint, + CommonConstants.OIDC_VERIFIER_UPDATE, + existingVerifiers[0].publicVerifierId + ); + this.logger.debug(`[oid4vpUpdateVerifier] calling agent URL=${url}`); + + try { + updatedVerifierDetails = await this._updateOid4vpVerifier(updateVerifier, url, orgId); + if (!updatedVerifierDetails) { + throw new InternalServerErrorException(ResponseMessages.oid4vp.error.updateFailed); + } + updatedVerifierDetails = updatedVerifierDetails.data as VerifierRecord; + } catch (error) { + // We'll not need this + const status409 = + 409 === error?.status?.message?.statusCode || 409 === error?.response?.status || 409 === error?.statusCode; + + if (status409) { + throw new ConflictException(`Verifier with id '${updatedVerifierDetails.verifierId}' already exists`); + } + throw error; + } + const updateVerifierDetails = await this.oid4vpRepository.updateOid4vpVerifier( + updatedVerifierDetails, + userDetails.id, + verifierId + ); + if (!updateVerifierDetails) { + throw new InternalServerErrorException(ResponseMessages.oid4vp.error.updateFailed); + } + + this.logger.debug( + `[oid4vpUpdateVerifier] verifier updated successfully for orgId=${orgId}, verifierId=${verifierId}` + ); + return updateVerifierDetails; + } catch (error) { + this.logger.error(`[oid4vpUpdateVerifier] - error in oid4vpUpdateVerifier records: ${JSON.stringify(error)}`); + throw new RpcException(error?.response ?? error); + } + } + + async getVerifierById(orgId: string, verifierId?: string): Promise { + this.logger.debug(`[getVerifierById] fetching verifier(s) for orgId=${orgId}, verifierId=${verifierId ?? 'all'}`); + try { + const verifiers = await this.oid4vpRepository.getVerifiersByVerifierId(orgId, verifierId); + if (!verifiers || 0 === verifiers.length) { + throw new NotFoundException(ResponseMessages.oid4vp.error.notFound); + } + this.logger.debug(`[getVerifierById] ${verifiers.length} record(s) found`); + return verifiers; + } catch (error) { + this.logger.error(`[getVerifierById] - error: ${error?.response ?? error?.message ?? JSON.stringify(error)}`); + throw new RpcException(error?.response ?? error); + } + } + + async deleteVerifierById(orgId: string, verifierId: string): Promise { + this.logger.debug(`[deleteVerifierById] called for orgId=${orgId}, verifierId=${verifierId}`); + try { + const checkIdExist = await this.oid4vpRepository.getVerifiersByVerifierId(orgId, verifierId); + if (0 == checkIdExist.length) { + throw new NotFoundException(ResponseMessages.oid4vp.error.notFound); + } + + const agentDetails = await this.oid4vpRepository.getAgentEndPoint(orgId); + if (!agentDetails) { + throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); + } + const { agentEndPoint, id } = agentDetails; + const url = await getAgentUrl(agentEndPoint, CommonConstants.OIDC_VERIFIER_DELETE, checkIdExist[0].verifierId); + this.logger.debug(`[deleteVerifierById] calling agent URL=${url}`); + + await this._deleteOid4vpVerifier(url, orgId); + + const verifier = await this.oid4vpRepository.deleteVerifierByVerifierId(orgId, verifierId); + + this.logger.debug( + `[deleteVerifierById] verifier deleted successfully for orgId=${orgId}, verifierId=${verifierId}` + ); + return verifier; + } catch (error) { + this.logger.error( + `[deleteVerifierById] - error: ${JSON.stringify(error?.response ?? error?.error ?? error ?? 'Something went wrong')}` + ); + throw new RpcException(error?.response ?? error.error ?? error); + } + } + + async oid4vpCreateVerificationSession(orgId, verifierId, sessionRequest, userDetails: user): Promise { + this.logger.debug( + `[oid4vpCreateVerificationSession] called for orgId=${orgId}, verifierId=${verifierId}, user=${userDetails?.id ?? 'unknown'}` + ); + try { + const activeCertificateDetails: X509CertificateRecord[] = []; + const agentDetails = await this.oid4vpRepository.getAgentEndPoint(orgId); + if (!agentDetails) { + throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); + } + const { agentEndPoint, orgDid } = agentDetails; + + const verifier = await this.oid4vpRepository.getVerifierById(orgId, verifierId); + if (!verifier) { + throw new NotFoundException(ResponseMessages.oid4vp.error.notFound); + } + + sessionRequest.verifierId = verifier.publicVerifierId; + + let requestSigner: RequestSigner | undefined; + + if (sessionRequest.requestSigner.method === SignerOption.DID) { + requestSigner = { + method: SignerMethodOption.DID, + didUrl: orgDid + }; + } else if ( + sessionRequest.requestSigner.method === SignerOption.X509_P256 || + sessionRequest.requestSigner.method === SignerOption.X509_ED25519 + ) { + this.logger.debug('X5C based request signer method selected'); + + const activeCertificate = await this.oid4vpRepository.getCurrentActiveCertificate( + orgId, + sessionRequest.requestSigner.methodv + ); + this.logger.debug(`activeCertificate=${JSON.stringify(activeCertificate)}`); + + if (!activeCertificate) { + throw new NotFoundException( + `No active certificate(${sessionRequest.requestSigner.method}}) found for issuer` + ); + } + + requestSigner = { + method: SignerMethodOption.X5C, // "x5c" + x5c: [activeCertificate.certificateBase64] // array with PEM/DER base64 + }; + + activeCertificateDetails.push(activeCertificate); + } else { + throw new BadRequestException(`Unsupported requestSigner method: ${sessionRequest.requestSigner.method}`); + } + + // assign the single object (not an array) + sessionRequest.requestSigner = requestSigner; + + console.log(`[oid4vpCreateVerificationSession] sessionRequest=${JSON.stringify(sessionRequest)}`); + + const url = await getAgentUrl(agentEndPoint, CommonConstants.OID4VP_VERIFICATION_SESSION); + console.log(`[oid4vpCreateVerificationSession] calling agent URL=${url}`); + + const createdSession = await this._createVerificationSession(sessionRequest, url, orgId); + if (!createdSession) { + throw new InternalServerErrorException(ResponseMessages.oid4vp.error.createFailed); + } + + this.logger.debug( + `[oid4vpCreateVerificationSession] verification session created successfully for orgId=${orgId}` + ); + return createdSession; + } catch (error) { + this.logger.error( + `[oid4vpCreateVerificationSession] - error creating verification session: ${JSON.stringify(error?.response ?? error)}` + ); + throw new RpcException(error?.response ?? error); + } + } + + async getVerifierSession(orgId: string, query: VerificationSessionQuery): Promise { + this.logger.debug(`[getVerifierSession] called for orgId=${orgId}, potentially with a query`); + try { + const agentDetails = await this.oid4vpRepository.getAgentEndPoint(orgId); + if (!agentDetails) { + throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); + } + const { agentEndPoint, id } = agentDetails; + + let url = query.id + ? getAgentUrl(agentEndPoint, CommonConstants.OIDC_VERIFIER_SESSION_GET_BY_ID, query.id) + : getAgentUrl(agentEndPoint, CommonConstants.OIDC_VERIFIER_SESSION_GET_BY_QUERY); + + if (!query.id) { + url = buildUrlWithQuery(url, query); + } + this.logger.debug(`[getVerifierSession] calling agent URL=${url}`); + + const verifiers = await await this._getOid4vpVerifierSession(url, orgId); + if (!verifiers || 0 === verifiers.length) { + throw new NotFoundException(ResponseMessages.oid4vp.error.notFound); + } + this.logger.debug(`[getVerifierSession] ${verifiers.length} verifier session(s) found for orgId=${orgId}`); + return verifiers; + } catch (error) { + this.logger.error(`[getVerifierSession] - error: ${JSON.stringify(error)}`); + throw new RpcException(error?.response ?? error); + } + } + + async getVerificationSessionResponse(orgId: string, verificationSessionId: string): Promise { + this.logger.debug( + `[getVerificationSessionResponse] called for orgId=${orgId}, verificationSessionId=${verificationSessionId}` + ); + try { + const agentDetails = await this.oid4vpRepository.getAgentEndPoint(orgId); + if (!agentDetails) { + throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); + } + const { agentEndPoint, id } = agentDetails; + const url = getAgentUrl( + agentEndPoint, + CommonConstants.OIDC_VERIFIER_SESSION_RESPONSE_GET_BY_ID, + verificationSessionId + ); + const verifiers = await await this._getOid4vpVerifierSession(url, orgId); + if (!verifiers || 0 === verifiers.length) { + throw new NotFoundException(ResponseMessages.oid4vpSession.error.responseNotFound); + } + this.logger.debug(`[getVerificationSessionResponse] response fetched successfully for orgId=${orgId}`); + return verifiers; + } catch (error) { + this.logger.error(`[getVerificationSessionResponse] - error: ${JSON.stringify(error?.response ?? error)}`); + throw new RpcException(error?.response ?? error); + } + } + + async oid4vpPresentationWebhook(oid4vpPresentation: Oid4vpPresentationWh, id: string): Promise { + try { + const { contextCorrelationId } = oid4vpPresentation ?? {}; + let orgId: string; + if ('default' !== contextCorrelationId) { + const getOrganizationId = await this.oid4vpRepository.getOrganizationByTenantId(contextCorrelationId); + if (!getOrganizationId) { + throw new NotFoundException(ResponseMessages.organisation.error.notFound); + } + orgId = getOrganizationId?.orgId; + } else { + orgId = id; + } + const agentDetails = await this.oid4vpRepository.storeOid4vpPresentationDetails(oid4vpPresentation, orgId); + return agentDetails; + } catch (error) { + this.logger.error(`[storeOid4vpPresentationWebhook] - error: ${JSON.stringify(error)}`); + throw error; + } + } + + async _createOid4vpVerifier(verifierDetails: CreateVerifier, url: string, orgId: string): Promise { + this.logger.debug(`[_createOid4vpVerifier] sending NATS message for orgId=${orgId}`); + try { + const payload = { verifierDetails, url, orgId }; + const response = await this.natsClient.sendNatsMessage( + this.oid4vpVerificationServiceProxy, + 'agent-create-oid4vp-verifier', + payload + ); + this.logger.debug(`[_createOid4vpVerifier] NATS response received`); + return response; + } catch (error) { + this.logger.error( + `[_createOID4VPVerifier] [NATS call]- error in create OID4VP Verifier : ${JSON.stringify(error)}` + ); + throw error; + } + } + + async _deleteOid4vpVerifier(url: string, orgId: string): Promise { + this.logger.debug(`[_deleteOid4vpVerifier] sending NATS message for orgId=${orgId}`); + try { + const payload = { url, orgId }; + const response = await this.natsClient.sendNatsMessage( + this.oid4vpVerificationServiceProxy, + 'agent-delete-oid4vp-verifier', + payload + ); + this.logger.debug(`[_deleteOid4vpVerifier] NATS response received`); + return response; + } catch (error) { + this.logger.error( + `[_deleteOid4vpVerifier] [NATS call]- error in delete OID4VP Verifier : ${JSON.stringify(error)}` + ); + throw error; + } + } + + async _updateOid4vpVerifier(verifierDetails: UpdateVerifier, url: string, orgId: string): Promise { + this.logger.debug(`[_updateOid4vpVerifier] sending NATS message for orgId=${orgId}`); + try { + const payload = { verifierDetails, url, orgId }; + const response = await this.natsClient.sendNatsMessage( + this.oid4vpVerificationServiceProxy, + 'agent-update-oid4vp-verifier', + payload + ); + this.logger.debug(`[_updateOid4vpVerifier] NATS response received`); + return response; + } catch (error) { + this.logger.error( + `[_updateOid4vpVerifier] [NATS call]- error in update OID4VP Verifier : ${JSON.stringify(error)}` + ); + throw error; + } + } + + async _createVerificationSession(sessionRequest: any, url: string, orgId: string): Promise { + this.logger.debug(`[_createVerificationSession] sending NATS message for orgId=${orgId}`); + try { + const payload = { sessionRequest, url, orgId }; + const response = await this.natsClient.sendNatsMessage( + this.oid4vpVerificationServiceProxy, + 'agent-create-oid4vp-verification-session', + payload + ); + this.logger.debug(`[_createVerificationSession] NATS response received`); + return response; + } catch (error) { + this.logger.error( + `[_createVerificationSession] [NATS call]- error in create verification session : ${JSON.stringify(error)}` + ); + throw error; + } + } + + async _getOid4vpVerifierSession(url: string, orgId: string): Promise { + this.logger.debug(`[_getOid4vpVerifierSession] sending NATS message for orgId=${orgId}`); + try { + const payload = { url, orgId }; + const response = await this.natsClient.sendNatsMessage( + this.oid4vpVerificationServiceProxy, + 'agent-get-oid4vp-verifier-session', + payload + ); + this.logger.debug(`[_getOid4vpVerifierSession] NATS response received`); + return response; + } catch (error) { + this.logger.error( + `[_getOid4vpVerifierSession] [NATS call]- error in get OID4VP Verifier Session : ${JSON.stringify(error)}` + ); + throw error; + } + } + + async _getVerificationSessionResponse(url: string, orgId: string): Promise { + this.logger.debug(`[_getVerificationSessionResponse] sending NATS message for orgId=${orgId}`); + try { + const payload = { url, orgId }; + const response = await this.natsClient.sendNatsMessage( + this.oid4vpVerificationServiceProxy, + 'agent-get-oid4vp-verifier-session', + payload + ); + this.logger.debug(`[_getVerificationSessionResponse] NATS response received`); + return response; + } catch (error) { + this.logger.error( + `[_getVerificationSessionResponse] [NATS call]- error in get OID4VP Verifier Session : ${JSON.stringify(error)}` + ); + throw error; + } + } +} diff --git a/apps/oid4vc-verification/test/app.e2e-spec.ts b/apps/oid4vc-verification/test/app.e2e-spec.ts new file mode 100644 index 000000000..a011c0ceb --- /dev/null +++ b/apps/oid4vc-verification/test/app.e2e-spec.ts @@ -0,0 +1,16 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { INestApplication } from '@nestjs/common'; +import { Oid4vcVerificationModule } from './../src/oid4vc-verification.module'; + +describe('Oid4vcVerificationController (e2e)', () => { + let app: INestApplication; + + beforeEach(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [Oid4vcVerificationModule] + }).compile(); + + app = moduleFixture.createNestApplication(); + await app.init(); + }); +}); diff --git a/apps/oid4vc-verification/test/jest-e2e.json b/apps/oid4vc-verification/test/jest-e2e.json new file mode 100644 index 000000000..e9d912f3e --- /dev/null +++ b/apps/oid4vc-verification/test/jest-e2e.json @@ -0,0 +1,9 @@ +{ + "moduleFileExtensions": ["js", "json", "ts"], + "rootDir": ".", + "testEnvironment": "node", + "testRegex": ".e2e-spec.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + } +} diff --git a/apps/oid4vc-verification/tsconfig.app.json b/apps/oid4vc-verification/tsconfig.app.json new file mode 100644 index 000000000..2a2de5a9b --- /dev/null +++ b/apps/oid4vc-verification/tsconfig.app.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": false, + "outDir": "../../dist/apps/oid4vc-verification" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] +} diff --git a/apps/user/test/app.e2e-spec.ts b/apps/user/test/app.e2e-spec.ts index ab51d4a94..ae537da6c 100644 --- a/apps/user/test/app.e2e-spec.ts +++ b/apps/user/test/app.e2e-spec.ts @@ -15,10 +15,5 @@ describe('UserController (e2e)', () => { await app.init(); }); - it('/ (GET)', () => { - return request(app.getHttpServer()) - .get('/') - .expect(200) - .expect('Hello World!'); - }); + it('/ (GET)', () => request(app.getHttpServer()).get('/').expect(200).expect('Hello World!')); }); diff --git a/apps/utility/src/utilities.repository.ts b/apps/utility/src/utilities.repository.ts index 7727d1b8e..4703af487 100644 --- a/apps/utility/src/utilities.repository.ts +++ b/apps/utility/src/utilities.repository.ts @@ -1,51 +1,46 @@ -import { PrismaService } from "@credebl/prisma-service"; -import { Injectable, Logger } from "@nestjs/common"; +import { PrismaService } from '@credebl/prisma-service'; +import { Injectable, Logger } from '@nestjs/common'; // eslint-disable-next-line camelcase -import { shortening_url } from "@prisma/client"; +import { shortening_url } from '@prisma/client'; @Injectable() export class UtilitiesRepository { - constructor( - private readonly prisma: PrismaService, - private readonly logger: Logger - ) { } - - async saveShorteningUrl( - payload - ): Promise { - - try { - - const { referenceId, invitationPayload } = payload; - const storeShorteningUrl = await this.prisma.shortening_url.upsert({ - where: { referenceId }, - update: { invitationPayload }, - create: { referenceId, invitationPayload } - }); - - this.logger.log(`[saveShorteningUrl] - shortening url details ${referenceId}`); - return storeShorteningUrl; - } catch (error) { - this.logger.error(`Error in saveShorteningUrl: ${error} `); - throw error; - } + constructor( + private readonly prisma: PrismaService, + private readonly logger: Logger + ) {} + + async saveShorteningUrl(payload): Promise { + try { + const { referenceId, invitationPayload } = payload; + const storeShorteningUrl = await this.prisma.shortening_url.upsert({ + where: { referenceId }, + update: { invitationPayload }, + create: { referenceId, invitationPayload } + }); + + this.logger.log(`[saveShorteningUrl] - shortening url details ${referenceId}`); + return storeShorteningUrl; + } catch (error) { + this.logger.error(`Error in saveShorteningUrl: ${error} `); + throw error; } - - // eslint-disable-next-line camelcase - async getShorteningUrl(referenceId): Promise { - try { - - const storeShorteningUrl = await this.prisma.shortening_url.findUnique({ - where: { - referenceId - } - }); - - this.logger.log(`[getShorteningUrl] - shortening url details ${referenceId}`); - return storeShorteningUrl; - } catch (error) { - this.logger.error(`Error in getShorteningUrl: ${error} `); - throw error; + } + + // eslint-disable-next-line camelcase + async getShorteningUrl(referenceId): Promise { + try { + const storeShorteningUrl = await this.prisma.shortening_url.findUnique({ + where: { + referenceId } + }); + + this.logger.log(`[getShorteningUrl] - shortening url details ${referenceId}`); + return storeShorteningUrl; + } catch (error) { + this.logger.error(`Error in getShorteningUrl: ${error} `); + throw error; } -} \ No newline at end of file + } +} diff --git a/apps/verification/src/interfaces/verification.interface.ts b/apps/verification/src/interfaces/verification.interface.ts index 2e259a785..909b72c06 100644 --- a/apps/verification/src/interfaces/verification.interface.ts +++ b/apps/verification/src/interfaces/verification.interface.ts @@ -236,6 +236,11 @@ export interface IProofRequestSearchCriteria { } export interface IInvitation { + outOfBandRecord?: { + id: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } & Record; + proofRecordThId?: string; invitationUrl?: string; deepLinkURL?: string; } diff --git a/apps/verification/src/verification.controller.ts b/apps/verification/src/verification.controller.ts index 5b795d419..907c00a77 100644 --- a/apps/verification/src/verification.controller.ts +++ b/apps/verification/src/verification.controller.ts @@ -45,7 +45,7 @@ export class VerificationController { * @returns Proof presentation details by proofId */ @MessagePattern({ cmd: 'get-proof-presentations-by-proofId' }) - async getProofPresentationById(payload: { proofId: string; orgId: string; user: IUserRequest }): Promise { + async getProofPresentationById(payload: { proofId: string; orgId: string; user: IUserRequest }): Promise { return this.verificationService.getProofPresentationById(payload.proofId, payload.orgId); } @@ -68,7 +68,7 @@ export class VerificationController { async sendProofRequest(payload: { requestProofDto: IProofRequestData; user: IUserRequest; - }): Promise { + }): Promise { return this.verificationService.sendProofRequest(payload.requestProofDto); } @@ -79,7 +79,7 @@ export class VerificationController { * @returns Verified proof presentation details */ @MessagePattern({ cmd: 'verify-presentation' }) - async verifyPresentation(payload: { proofId: string; orgId: string; user: IUserRequest }): Promise { + async verifyPresentation(payload: { proofId: string; orgId: string; user: IUserRequest }): Promise { return this.verificationService.verifyPresentation(payload.proofId, payload.orgId); } diff --git a/apps/verification/src/verification.service.ts b/apps/verification/src/verification.service.ts index 1a4cb77d1..09013b019 100644 --- a/apps/verification/src/verification.service.ts +++ b/apps/verification/src/verification.service.ts @@ -9,7 +9,6 @@ import { NotFoundException } from '@nestjs/common'; import { ClientProxy, RpcException } from '@nestjs/microservices'; -import { map } from 'rxjs/operators'; import { IGetAllProofPresentations, IProofRequestSearchCriteria, @@ -52,7 +51,6 @@ import { convertUrlToDeepLinkUrl, getAgentUrl } from '@credebl/common/common.uti import { UserActivityRepository } from 'libs/user-activity/repositories'; import { ISchemaDetail } from '@credebl/common/interfaces/schema.interface'; import { NATSClient } from '@credebl/common/NATSClient'; -import { from } from 'rxjs'; import { EmailService } from '@credebl/common/email.service'; @Injectable() @@ -184,12 +182,11 @@ export class VerificationService { * @param payload * @returns Get all proof presentation */ - async _getProofPresentations(payload: IGetAllProofPresentations): Promise<{ - response: string; - }> { + async _getProofPresentations(payload: IGetAllProofPresentations): Promise { try { const pattern = { cmd: 'agent-get-proof-presentations' }; - return await this.natsCall(pattern, payload); + const result = await this.natsClient.send(this.verificationServiceProxy, pattern, payload); + return result; } catch (error) { this.logger.error(`[_getProofPresentations] - error in get proof presentations : ${JSON.stringify(error)}`); throw error; @@ -225,7 +222,7 @@ export class VerificationService { * @param orgId * @returns Proof presentation details by proofId */ - async getProofPresentationById(proofId: string, orgId: string): Promise { + async getProofPresentationById(proofId: string, orgId: string): Promise { try { const getAgentDetails = await this.verificationRepository.getAgentEndPoint(orgId); const url = await getAgentUrl( @@ -237,18 +234,18 @@ export class VerificationService { const payload = { orgId, url }; const getProofPresentationById = await this._getProofPresentationById(payload); - return getProofPresentationById?.response; + return getProofPresentationById; } catch (error) { this.logger.error( `[getProofPresentationById] - error in get proof presentation by proofId : ${JSON.stringify(error)}` ); - const errorMessage = error?.response?.error?.reason || error?.message; + const errorMessage = error?.error?.reason || error?.message; if (errorMessage?.includes('not found')) { throw new NotFoundException(errorMessage); } - throw new RpcException(error.response ? error.response : error); + throw new RpcException(error); } } @@ -257,15 +254,14 @@ export class VerificationService { * @param payload * @returns Get proof presentation details */ - async _getProofPresentationById(payload: IGetProofPresentationById): Promise<{ - response: string; - }> { + async _getProofPresentationById(payload: IGetProofPresentationById): Promise { try { const pattern = { cmd: 'agent-get-proof-presentation-by-id' }; - return await this.natsCall(pattern, payload); + const result = await this.natsClient.send(this.verificationServiceProxy, pattern, payload); + return result; } catch (error) { this.logger.error( `[_getProofPresentationById] - error in get proof presentation by id : ${JSON.stringify(error)}` @@ -280,7 +276,7 @@ export class VerificationService { * @returns Requested proof presentation details */ - async sendProofRequest(requestProof: IProofRequestData): Promise { + async sendProofRequest(requestProof: IProofRequestData): Promise { try { const comment = requestProof.comment ? requestProof.comment : ''; const getAgentDetails = await this.verificationRepository.getAgentEndPoint(requestProof.orgId); @@ -338,11 +334,11 @@ export class VerificationService { ? requestProof.connectionId : [requestProof.connectionId]; - const responses: string[] = []; + const responses: object[] = []; for (const connectionId of connectionIds) { const payload = await createPayload(connectionId); const getProofPresentationById = await this._sendProofRequest(payload); - responses.push(getProofPresentationById?.response); + responses.push(getProofPresentationById); } return responses; } else { @@ -352,7 +348,7 @@ export class VerificationService { const payload = await createPayload(connectionId); const getProofPresentationById = await this._sendProofRequest(payload); - return getProofPresentationById?.response; + return getProofPresentationById; } } catch (error) { // Handle cases where identical attributes are used in both predicates and non-predicates. @@ -381,17 +377,16 @@ export class VerificationService { * @param orgId * @returns Get requested proof presentation details */ - async _sendProofRequest(payload: IProofRequestPayload): Promise<{ - response: string; - }> { + async _sendProofRequest(payload: IProofRequestPayload): Promise { try { const pattern = { cmd: 'agent-send-proof-request' }; - return await this.natsCall(pattern, payload); + const result = await this.natsClient.send(this.verificationServiceProxy, pattern, payload); + return result; } catch (error) { - this.logger.error(`[_sendProofRequest] - error in verify presentation : ${JSON.stringify(error)}`); + this.logger.error(`[_sendProofRequest] - nats error in sending proof request : ${JSON.stringify(error)}`); throw error; } } @@ -402,28 +397,28 @@ export class VerificationService { * @param orgId * @returns Verified proof presentation details */ - async verifyPresentation(proofId: string, orgId: string): Promise { + async verifyPresentation(proofId: string, orgId: string): Promise { try { const getAgentData = await this.verificationRepository.getAgentEndPoint(orgId); const url = await getAgentUrl(getAgentData?.agentEndPoint, CommonConstants.ACCEPT_PRESENTATION, proofId); const payload = { orgId, url }; const getProofPresentationById = await this._verifyPresentation(payload); - return getProofPresentationById?.response; + return getProofPresentationById; } catch (error) { this.logger.error( `[getProofPresentationById] - error in get proof presentation by proofId : ${JSON.stringify(error)}` ); - const errorStack = error?.response?.error?.reason; + const errorStack = error?.error?.reason; if (errorStack) { throw new RpcException({ message: ResponseMessages.verification.error.proofNotFound, - statusCode: error?.response?.status, + statusCode: error?.status, error: errorStack }); } else { - throw new RpcException(error.response ? error.response : error); + throw new RpcException(error || 'Something went wrong'); } } } @@ -433,15 +428,14 @@ export class VerificationService { * @param payload * @returns Get verified proof presentation details */ - async _verifyPresentation(payload: IVerifyPresentation): Promise<{ - response: string; - }> { + async _verifyPresentation(payload: IVerifyPresentation): Promise { try { const pattern = { cmd: 'agent-verify-presentation' }; - return await this.natsCall(pattern, payload); + const result = await this.natsClient.send(this.verificationServiceProxy, pattern, payload); + return result; } catch (error) { this.logger.error(`[_verifyPresentation] - error in verify presentation : ${JSON.stringify(error)}`); throw error; @@ -571,17 +565,17 @@ export class VerificationService { //nats call in agent-service to create an invitation url const pattern = { cmd: 'store-object-return-url' }; const payload = { persistent, storeObj }; - const message = await this.natsCall(pattern, payload); - return message.response; + const message = await this.natsClient.send(this.verificationServiceProxy, pattern, payload); + return message; } - private async generateOOBProofReq(payload: IProofRequestPayload): Promise { + private async generateOOBProofReq(payload: IProofRequestPayload): Promise { const getProofPresentation = await this._sendOutOfBandProofRequest(payload); if (!getProofPresentation) { throw new Error(ResponseMessages.verification.error.proofPresentationNotFound); } - return getProofPresentation.response; + return getProofPresentation; } // Currently batch size is not used, as length of emails sent is restricted to '10' @@ -626,13 +620,13 @@ export class VerificationService { getAgentDetails: org_agents, organizationDetails: organisation ): Promise { - const getProofPresentation = await this._sendOutOfBandProofRequest(payload); + const getProofPresentation: IInvitation = await this._sendOutOfBandProofRequest(payload); if (!getProofPresentation) { throw new Error(ResponseMessages.verification.error.proofPresentationNotFound); } - const invitationUrl = getProofPresentation?.response?.invitationUrl; + const invitationUrl = getProofPresentation?.invitationUrl; // Currently have shortenedUrl to store only for 30 days const persist: boolean = false; const shortenedUrl = await this.storeVerificationObjectAndReturnUrl(invitationUrl, persist); @@ -670,8 +664,8 @@ export class VerificationService { return { isEmailSent, - outOfBandRecordId: getProofPresentation?.response?.outOfBandRecord?.id, - proofRecordThId: getProofPresentation?.response?.proofRecordThId + outOfBandRecordId: getProofPresentation?.outOfBandRecord?.id, + proofRecordThId: getProofPresentation?.proofRecordThId }; } @@ -680,15 +674,14 @@ export class VerificationService { * @param payload * @returns Get requested proof presentation details */ - async _sendOutOfBandProofRequest(payload: IProofRequestPayload): Promise<{ - response; - }> { + async _sendOutOfBandProofRequest(payload: IProofRequestPayload): Promise { try { const pattern = { cmd: 'agent-send-out-of-band-proof-request' }; - return await this.natsCall(pattern, payload); + const result = await this.natsClient.send(this.verificationServiceProxy, pattern, payload); + return result; } catch (error) { this.logger.error(`[_sendOutOfBandProofRequest] - error in Out Of Band Presentation : ${JSON.stringify(error)}`); throw error; @@ -771,7 +764,7 @@ export class VerificationService { const getProofPresentationById = await this._getVerifiedProofDetails(payload); - if (!getProofPresentationById?.response?.presentation) { + if (!getProofPresentationById?.presentation) { throw new NotFoundException(ResponseMessages.verification.error.proofPresentationNotFound, { cause: new Error(), description: ResponseMessages.errorMessages.notFound @@ -781,11 +774,10 @@ export class VerificationService { const extractedDataArray: IProofPresentationDetails[] = []; // For Presentation Exchange format - if (getProofPresentationById?.response?.request?.presentationExchange) { - const presentationDefinition = - getProofPresentationById?.response?.request?.presentationExchange?.presentation_definition; + if (getProofPresentationById?.request?.presentationExchange) { + const presentationDefinition = getProofPresentationById?.request?.presentationExchange?.presentation_definition; const verifiableCredentials = - getProofPresentationById?.response?.presentation?.presentationExchange?.verifiableCredential; + getProofPresentationById?.presentation?.presentationExchange?.verifiableCredential; presentationDefinition?.input_descriptors.forEach((descriptor, index) => { const schemaId = descriptor?.schema[0]?.uri; @@ -795,7 +787,7 @@ export class VerificationService { if (getProofPresentationById?.response) { certificate = - getProofPresentationById?.response?.presentation?.presentationExchange?.verifiableCredential[0].prettyVc + getProofPresentationById?.presentation?.presentationExchange?.verifiableCredential[0].prettyVc ?.certificate; } @@ -822,10 +814,10 @@ export class VerificationService { }); } // For Indy format - if (getProofPresentationById?.response?.request?.indy) { - const requestedAttributes = getProofPresentationById?.response?.request?.indy?.requested_attributes; - const requestedPredicates = getProofPresentationById?.response?.request?.indy?.requested_predicates; - const revealedAttrs = getProofPresentationById?.response?.presentation?.indy?.requested_proof?.revealed_attrs; + if (getProofPresentationById?.request?.indy) { + const requestedAttributes = getProofPresentationById?.request?.indy?.requested_attributes; + const requestedPredicates = getProofPresentationById?.request?.indy?.requested_predicates; + const revealedAttrs = getProofPresentationById?.presentation?.indy?.requested_proof?.revealed_attrs; if (0 !== Object.keys(requestedAttributes).length && 0 !== Object.keys(requestedPredicates).length) { for (const key in requestedAttributes) { @@ -836,9 +828,9 @@ export class VerificationService { if (requestedAttributeKey?.restrictions) { credDefId = requestedAttributeKey?.restrictions[0]?.cred_def_id; schemaId = requestedAttributeKey?.restrictions[0]?.schema_id; - } else if (getProofPresentationById?.response?.presentation?.indy?.identifiers) { - credDefId = getProofPresentationById?.response?.presentation?.indy?.identifiers[0].cred_def_id; - schemaId = getProofPresentationById?.response?.presentation?.indy?.identifiers[0].schema_id; + } else if (getProofPresentationById?.presentation?.indy?.identifiers) { + credDefId = getProofPresentationById?.presentation?.indy?.identifiers[0].cred_def_id; + schemaId = getProofPresentationById?.presentation?.indy?.identifiers[0].schema_id; } if (revealedAttrs.hasOwnProperty(key)) { @@ -915,16 +907,16 @@ export class VerificationService { return extractedDataArray; } catch (error) { this.logger.error(`[getVerifiedProofDetails] - error in get verified proof details : ${JSON.stringify(error)}`); - const errorStack = error?.response?.error?.reason; + const errorStack = error?.error?.reason; if (errorStack) { throw new RpcException({ message: ResponseMessages.verification.error.verifiedProofNotFound, - statusCode: error?.response?.status, + statusCode: error?.status, error: errorStack }); } else { - throw new RpcException(error.response ? error.response : error); + throw new RpcException(error); } } } @@ -936,24 +928,24 @@ export class VerificationService { if (attribute?.restrictions) { credDefId = attribute?.restrictions[0]?.cred_def_id; schemaId = attribute?.restrictions[0]?.schema_id; - } else if (getProofPresentationById?.response?.presentation?.indy?.identifiers) { - credDefId = getProofPresentationById?.response?.presentation?.indy?.identifiers[0].cred_def_id; - schemaId = getProofPresentationById?.response?.presentation?.indy?.identifiers[0].schema_id; + } else if (getProofPresentationById?.presentation?.indy?.identifiers) { + credDefId = getProofPresentationById?.presentation?.indy?.identifiers[0].cred_def_id; + schemaId = getProofPresentationById?.presentation?.indy?.identifiers[0].schema_id; } return [credDefId, schemaId]; } - async _getVerifiedProofDetails(payload: IVerifiedProofData): Promise<{ - response; - }> { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async _getVerifiedProofDetails(payload: IVerifiedProofData): Promise { try { //nats call in agent for fetch verified proof details const pattern = { cmd: 'get-agent-verified-proof-details' }; - return await this.natsCall(pattern, payload); + const result = await this.natsClient.send(this.verificationServiceProxy, pattern, payload); + return result; } catch (error) { this.logger.error(`[_getVerifiedProofDetails] - error in verified proof details : ${JSON.stringify(error)}`); throw error; @@ -993,32 +985,6 @@ export class VerificationService { } } - async natsCall( - pattern: object, - payload: object - ): Promise<{ - response: string; - }> { - return from(this.natsClient.send(this.verificationServiceProxy, pattern, payload)) - .pipe( - map((response) => ({ - response - })) - ) - .toPromise() - .catch((error) => { - this.logger.error(`catch: ${JSON.stringify(error)}`); - throw new HttpException( - { - status: error.statusCode, - error: error.error, - message: error.message - }, - error.error - ); - }); - } - async delay(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } diff --git a/apps/webhook/src/webhook.service.ts b/apps/webhook/src/webhook.service.ts index 3d3402bd2..f0e899261 100644 --- a/apps/webhook/src/webhook.service.ts +++ b/apps/webhook/src/webhook.service.ts @@ -1,27 +1,16 @@ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ -import { CommonService } from '@credebl/common'; import { WebhookRepository } from './webhook.repository'; import { ResponseMessages } from '@credebl/common/response-messages'; -import { ClientProxy, RpcException } from '@nestjs/microservices'; +import { RpcException } from '@nestjs/microservices'; import AsyncRetry = require('async-retry'); import { ICreateWebhookUrl, IGetWebhookUrl, IWebhookDto } from '../interfaces/webhook.interfaces'; -import { - Inject, - Injectable, - InternalServerErrorException, - Logger, - NotFoundException -} from '@nestjs/common'; +import { Injectable, InternalServerErrorException, Logger, NotFoundException } from '@nestjs/common'; import { IWebhookUrl } from '@credebl/common/interfaces/webhook.interface'; @Injectable() export class WebhookService { private readonly logger = new Logger('WebhookService'); - constructor( - @Inject('NATS_CLIENT') private readonly webhookProxy: ClientProxy, - private readonly commonService: CommonService, - private readonly webhookRepository: WebhookRepository - ) {} + constructor(private readonly webhookRepository: WebhookRepository) {} // eslint-disable-next-line @typescript-eslint/explicit-function-return-type retryOptions(logger: Logger) { diff --git a/apps/x509/src/interfaces/x509.interface.ts b/apps/x509/src/interfaces/x509.interface.ts new file mode 100644 index 000000000..bfae58ad9 --- /dev/null +++ b/apps/x509/src/interfaces/x509.interface.ts @@ -0,0 +1,53 @@ +import { X509CertificateRecord } from '@credebl/common/interfaces/x509.interface'; +import { x5cKeyType, x5cRecordStatus } from '@credebl/enum/enum'; + +export interface CreateX509CertificateEntity { + orgId: string; // We'll accept orgId and find orgAgent internally + keyType: x5cKeyType; + status: string; + validFrom: Date; + expiry: Date; + certificateBase64: string; + createdBy: string; + lastChangedBy: string; +} + +export interface UpdateCertificateStatusDto { + status: x5cRecordStatus; + lastChangedBy: string; +} + +export interface CertificateDateCheckDto { + orgId: string; + validFrom: Date; + expiry: Date; + keyType: x5cKeyType; + status: x5cRecordStatus; + excludeCertificateId?: string; +} + +export interface OrgAgent { + id: string; + createDateTime: Date; + createdBy: string; + lastChangedDateTime: Date; + lastChangedBy: string; + orgDid: string; + verkey: string; + agentEndPoint: string; + agentId: string; + isDidPublic: boolean; + ledgerId: string; + orgAgentTypeId: string; + tenantId: string; +} + +export interface IX509ListCount { + total: number; + data: X509CertificateRecord[]; +} + +export interface IX509CollisionResult { + hasCollision: boolean; + collisions: X509CertificateRecord[]; +} diff --git a/apps/x509/src/main.ts b/apps/x509/src/main.ts new file mode 100644 index 000000000..b5a6d01e4 --- /dev/null +++ b/apps/x509/src/main.ts @@ -0,0 +1,21 @@ +import { NestFactory } from '@nestjs/core'; +import { HttpExceptionFilter } from 'libs/http-exception.filter'; +import { Logger } from '@nestjs/common'; +import { MicroserviceOptions, Transport } from '@nestjs/microservices'; +import { getNatsOptions } from '@credebl/common/nats.config'; +import { CommonConstants } from '@credebl/common/common.constant'; +import NestjsLoggerServiceAdapter from '@credebl/logger/nestjsLoggerServiceAdapter'; +import { X509Module } from './x509.module'; + +async function bootstrap(): Promise { + const app = await NestFactory.createMicroservice(X509Module, { + transport: Transport.NATS, + options: getNatsOptions(CommonConstants.X509_SERVICE, process.env.X509_NKEY_SEED) + }); + app.useLogger(app.get(NestjsLoggerServiceAdapter)); + app.useGlobalFilters(new HttpExceptionFilter()); + + await app.listen(); + Logger.log('X509 Microservice is listening to NATS '); +} +bootstrap(); diff --git a/apps/x509/src/repositories/x509.repository.ts b/apps/x509/src/repositories/x509.repository.ts new file mode 100644 index 000000000..037c2d4f0 --- /dev/null +++ b/apps/x509/src/repositories/x509.repository.ts @@ -0,0 +1,326 @@ +/* eslint-disable camelcase */ +// src/repositories/x509-certificate.repository.ts +import { Injectable, NotFoundException, Logger } from '@nestjs/common'; +import { PrismaService } from '@credebl/prisma-service'; +import { + CertificateDateCheckDto, + CreateX509CertificateEntity, + IX509CollisionResult, + IX509ListCount, + OrgAgent, + UpdateCertificateStatusDto +} from '../interfaces/x509.interface'; +import { x5cRecordStatus } from '@credebl/enum/enum'; +import { ResponseMessages } from '@credebl/common/response-messages'; +import { org_agents } from '@prisma/client'; +import { X509CertificateRecord } from '@credebl/common/interfaces/x509.interface'; + +@Injectable() +export class X509CertificateRepository { + constructor( + private readonly prisma: PrismaService, + private readonly logger: Logger + ) {} + + // Helper method to get orgAgent by orgId + private async getOrgAgentByOrgId(orgId: string): Promise { + try { + const orgAgent = await this.prisma.org_agents.findFirst({ + where: { + orgId + } + }); + + if (!orgAgent) { + throw new NotFoundException(`OrgAgent with orgId ${orgId} not found`); + } + + return orgAgent; + } catch (error) { + this.logger.error(`Error in getOrgAgentByOrgId: ${error.message}`); + throw error; + } + } + + // CREATE - Create new certificate using orgId + async create(createDto: CreateX509CertificateEntity): Promise { + try { + // Get orgAgent by orgId + const orgAgent = await this.getOrgAgentByOrgId(createDto.orgId); + + const certificate = await this.prisma.x509_certificates.create({ + data: { + orgAgentId: orgAgent.id, + keyType: createDto.keyType, + status: createDto.status, + validFrom: createDto.validFrom, + expiry: createDto.expiry, + certificateBase64: createDto.certificateBase64, + createdBy: createDto.createdBy, + lastChangedBy: createDto.lastChangedBy + } + }); + + return certificate; + } catch (error) { + this.logger.error(`Error in create certificate: ${error.message}`); + throw error; + } + } + + // READ - Find all certificates with optional filtering and pagination + async findAll(options?: { + orgId: string; + status?: string; + keyType?: string; + page?: number; + limit?: number; + }): Promise { + try { + const { orgId, status, keyType, page = 1, limit = 10 } = options || {}; + + const skip = (page - 1) * limit; + + const where: Parameters[0]['where'] = {}; + + // Build where conditions with joins + if (orgId || status || keyType) { + where.AND = []; + + where.AND.push({ + org_agents: { + orgId + } + }); + + if (status) { + where.AND.push({ status }); + } + + if (keyType) { + where.AND.push({ keyType }); + } + } + + const [data, total] = await Promise.all([ + this.prisma.x509_certificates.findMany({ + where, + skip, + take: limit, + orderBy: { + createdAt: 'desc' + } + }), + this.prisma.x509_certificates.count({ where }) + ]); + + return { data, total }; + } catch (error) { + this.logger.error(`Error in findAll certificates: ${error.message}`); + throw error; + } + } + + // READ - Find certificate by ID + async findById(orgId: string, id: string): Promise { + try { + const certificate = await this.prisma.x509_certificates.findUnique({ + where: { + id, + org_agents: { + orgId + } + } + }); + + if (!certificate) { + throw new NotFoundException(`Certificate with ID ${id} not found`); + } + + return certificate; + } catch (error) { + this.logger.error(`Error in findById: ${error.message}`); + throw error; + } + } + + // READ - Find certificates by organization ID + async findByOrgId(orgId: string): Promise { + try { + const certificates = await this.prisma.x509_certificates.findMany({ + where: { + org_agents: { + orgId + } + }, + orderBy: { + createdAt: 'desc' + } + }); + + return certificates; + } catch (error) { + this.logger.error(`Error in findByOrgId: ${error.message}`); + throw error; + } + } + + // UTILITY - Check date collision without throwing exception + async hasDateCollision(dateCheckDto: CertificateDateCheckDto): Promise { + try { + const { orgId, validFrom, expiry, excludeCertificateId } = dateCheckDto; + + const collisions = await this.prisma.x509_certificates.findMany({ + where: { + status: dateCheckDto.status, + keyType: dateCheckDto.keyType, + org_agents: { + orgId + }, + AND: [ + { + OR: [ + { + AND: [{ validFrom: { lte: expiry } }, { expiry: { gte: validFrom } }] + } + ] + }, + ...(excludeCertificateId ? [{ id: { not: excludeCertificateId } }] : []) + ] + } + }); + + return { + hasCollision: 0 < collisions.length, + collisions + }; + } catch (error) { + this.logger.error(`Error in hasDateCollision: ${error.message}`); + throw error; + } + } + + // UPDATE - Update certificate status + async updateStatus(id: string, statusDto: UpdateCertificateStatusDto): Promise { + try { + const certificate = await this.prisma.x509_certificates.update({ + where: { id }, + data: { + status: statusDto.status, + lastChangedBy: statusDto.lastChangedBy + } + }); + + return certificate; + } catch (error) { + this.logger.error(`Error in updateStatus: ${error.message}`); + throw error; + } + } + + // // DELETE - Delete certificate + // async delete(id: string) { + // try { + // await this.findById(id); // Check if certificate exists + + // await this.prisma.x509_certificates.delete({ + // where: { id } + // }); + // } catch (error) { + // this.logger.error(`Error in delete certificate: ${error.message}`); + // throw error; + // } + // } + + // UTILITY - Check if certificate exists + async exists(id: string): Promise { + try { + const count = await this.prisma.x509_certificates.count({ + where: { id } + }); + return 0 < count; + } catch (error) { + this.logger.error(`Error in exists check: ${error.message}`); + throw error; + } + } + + // UTILITY - Find expiring certificates for an org + async findExpiringCertificatesByOrg(orgId: string, days: number = 30): Promise { + try { + const expiryDate = new Date(); + expiryDate.setDate(expiryDate.getDate() + days); + + const certificates = await this.prisma.x509_certificates.findMany({ + where: { + org_agents: { + orgId + }, + expiry: { + lte: expiryDate + }, + status: 'Active' + }, + orderBy: { + expiry: 'asc' + } + }); + + return certificates; + } catch (error) { + this.logger.error(`Error in findExpiringCertificatesByOrg: ${error.message}`); + throw error; + } + } + + async getCurrentActiveCertificate(orgId: string): Promise { + try { + const now = new Date(); + + const certificate = await this.prisma.x509_certificates.findFirst({ + where: { + org_agents: { + orgId + }, + status: x5cRecordStatus.Active, + validFrom: { + lte: now + }, + expiry: { + gte: now + } + }, + orderBy: { + createdAt: 'desc' + } + }); + + return certificate; + } catch (error) { + this.logger.error(`Error in getCurrentActiveCertificate: ${error.message}`); + throw error; + } + } + + async getAgentEndPoint(orgId: string): Promise { + try { + const agentDetails = await this.prisma.org_agents.findFirst({ + where: { + orgId + }, + include: { + organisation: true + } + }); + + if (!agentDetails) { + throw new NotFoundException(ResponseMessages.x509.error.agentEndPointNotFound); + } + + return agentDetails; + } catch (error) { + this.logger.error(`Error in get getAgentEndPoint: ${error.message} `); + throw error; + } + } +} diff --git a/apps/x509/src/x509.controller.ts b/apps/x509/src/x509.controller.ts new file mode 100644 index 000000000..8227e88e4 --- /dev/null +++ b/apps/x509/src/x509.controller.ts @@ -0,0 +1,58 @@ +import { Controller } from '@nestjs/common'; +import { MessagePattern } from '@nestjs/microservices'; +import { X509CertificateService } from './x509.service'; +import { user } from '@prisma/client'; +import { IUserRequest } from '@credebl/user-request/user-request.interface'; +import { + IX509ImportCertificateOptionsDto, + IX509SearchCriteria, + X509CertificateRecord, + X509CreateCertificateOptions +} from '@credebl/common/interfaces/x509.interface'; + +@Controller() +export class X509CertificateController { + constructor(private readonly x509CertificateService: X509CertificateService) {} + + @MessagePattern({ cmd: 'create-x509-certificate' }) + async createCertificate(payload: { + orgId: string; + options: X509CreateCertificateOptions; + user: user; + }): Promise { + return this.x509CertificateService.createCertificate(payload); + } + + @MessagePattern({ cmd: 'activate-x509-certificate' }) + async activateCertificate(payload: { orgId: string; id: string; user: user }): Promise { + return this.x509CertificateService.activateCertificate(payload); + } + + @MessagePattern({ cmd: 'deActivate-x509-certificate' }) + async deActivateCertificate(payload: { orgId: string; id: string; user: user }): Promise { + return this.x509CertificateService.deActivateCertificate(payload); + } + + @MessagePattern({ cmd: 'get-all-certificates' }) + async getCertificateByOrgId(payload: { + orgId: string; + options: IX509SearchCriteria; + user: IUserRequest; + }): Promise<{ data: X509CertificateRecord[]; total: number }> { + return this.x509CertificateService.getCertificateByOrgId(payload.orgId, payload.options); + } + + @MessagePattern({ cmd: 'get-certificate' }) + async getCertificate(payload: { orgId: string; id: string; user: IUserRequest }): Promise { + return this.x509CertificateService.getCertificateById(payload.orgId, payload.id); + } + + @MessagePattern({ cmd: 'import-x509-certificate' }) + async importCertificate(payload: { + orgId: string; + options: IX509ImportCertificateOptionsDto; + user: user; + }): Promise { + return this.x509CertificateService.importCertificate(payload); + } +} diff --git a/apps/x509/src/x509.module.ts b/apps/x509/src/x509.module.ts new file mode 100644 index 000000000..8b2a9f0bb --- /dev/null +++ b/apps/x509/src/x509.module.ts @@ -0,0 +1,31 @@ +import { Logger, Module } from '@nestjs/common'; +import { X509CertificateService } from './x509.service'; +import { PrismaService } from '@credebl/prisma-service'; +import { ClientsModule, Transport } from '@nestjs/microservices'; +import { X509CertificateRepository } from './repositories/x509.repository'; +import { getNatsOptions } from '@credebl/common/nats.config'; +import { CommonConstants } from '@credebl/common/common.constant'; +import { ConfigModule as PlatformConfig } from '@credebl/config/config.module'; +import { GlobalConfigModule } from '@credebl/config/global-config.module'; +import { LoggerModule } from '@credebl/logger/logger.module'; +import { ContextInterceptorModule } from '@credebl/context/contextInterceptorModule'; +import { X509CertificateController } from './x509.controller'; + +@Module({ + imports: [ + GlobalConfigModule, + LoggerModule, + PlatformConfig, + ContextInterceptorModule, + ClientsModule.register([ + { + name: 'NATS_CLIENT', + transport: Transport.NATS, + options: getNatsOptions(CommonConstants.X509_SERVICE, process.env.X509_NKEY_SEED) + } + ]) + ], + controllers: [X509CertificateController], + providers: [X509CertificateService, PrismaService, X509CertificateRepository, Logger] +}) +export class X509Module {} diff --git a/apps/x509/src/x509.service.ts b/apps/x509/src/x509.service.ts new file mode 100644 index 000000000..b4a3c4659 --- /dev/null +++ b/apps/x509/src/x509.service.ts @@ -0,0 +1,346 @@ +// src/services/x509-certificate.service.ts +import { + ConflictException, + HttpException, + Inject, + Injectable, + InternalServerErrorException, + NotFoundException, + UnprocessableEntityException +} from '@nestjs/common'; +import { BaseService } from 'libs/service/base.service'; +import { X509CertificateRepository } from './repositories/x509.repository'; +import { user } from '@prisma/client'; +import { ClientProxy, RpcException } from '@nestjs/microservices'; +import { map } from 'rxjs'; +import { + IX509ImportCertificateOptionsDto, + IX509SearchCriteria, + x509CertificateDecodeDto, + X509CertificateRecord, + X509CreateCertificateOptions +} from '@credebl/common/interfaces/x509.interface'; +import { + CertificateDateCheckDto, + CreateX509CertificateEntity, + UpdateCertificateStatusDto +} from './interfaces/x509.interface'; +import { getAgentUrl } from '@credebl/common/common.utils'; +import { CommonConstants } from '@credebl/common/common.constant'; +import { ResponseMessages } from '@credebl/common/response-messages'; +import { x5cKeyType, x5cRecordStatus } from '@credebl/enum/enum'; + +@Injectable() +export class X509CertificateService extends BaseService { + constructor( + private readonly x509CertificateRepository: X509CertificateRepository, + @Inject('NATS_CLIENT') private readonly x509ServiceProxy: ClientProxy + ) { + super('x509Service'); + } + + async createCertificate(payload: { + orgId: string; + options: X509CreateCertificateOptions; + user: user; + }): Promise { + try { + this.logger.log(`Start creating x509 certificate`); + this.logger.debug(`Create x509 certificate with options`, payload); + const { options, user, orgId } = payload; + const url = await getAgentUrl(await this.getAgentEndpoint(orgId), CommonConstants.X509_CREATE_CERTIFICATE); + + const certificateDateCheckDto: CertificateDateCheckDto = { + orgId, + validFrom: options.validity.notBefore, + expiry: options.validity.notAfter, + keyType: options.authorityKey.keyType, + status: x5cRecordStatus.Active + }; + const collisionForActiveRecords = await this.x509CertificateRepository.hasDateCollision(certificateDateCheckDto); + + let certStatus: x5cRecordStatus; + if (collisionForActiveRecords.hasCollision) { + certificateDateCheckDto.status = x5cRecordStatus.PendingActivation; + const collisionForPendingRecords = + await this.x509CertificateRepository.hasDateCollision(certificateDateCheckDto); + + if (collisionForPendingRecords.hasCollision) { + this.logger.log(`Creating x509 certificate has collision`); + this.logger.error(`Collision records`, collisionForActiveRecords); + throw new ConflictException(ResponseMessages.x509.error.collision); + } + + certStatus = x5cRecordStatus.PendingActivation; + } else { + certStatus = x5cRecordStatus.Active; + } + + const certificate = await this._createX509CertificateForOrg(options, url, orgId); + if (!certificate) { + throw new NotFoundException(ResponseMessages.x509.error.errorCreate); + } + + const createDto: CreateX509CertificateEntity = { + orgId, + certificateBase64: certificate.response.publicCertificateBase64, + keyType: options.authorityKey.keyType, + status: certStatus, + validFrom: options.validity.notBefore, + expiry: options.validity.notAfter, + createdBy: user.id, + lastChangedBy: user.id + }; + + return await this.x509CertificateRepository.create(createDto); + } catch (error) { + this.logger.error(`Error in createCertificate: ${error}`); + throw new RpcException(error.response ? error.response : error); + } + } + + async activateCertificate(payload: { orgId: string; id: string; user: user }): Promise { + const { orgId, user, id } = payload; + const certificateRecord = await this.x509CertificateRepository.findById(orgId, id); + if (certificateRecord) { + const certificateDateCheckDto: CertificateDateCheckDto = { + orgId, + validFrom: certificateRecord.validFrom, + expiry: certificateRecord.expiry, + keyType: certificateRecord.keyType as x5cKeyType, + status: x5cRecordStatus.Active, + excludeCertificateId: id + }; + const collisionForActiveRecords = await this.x509CertificateRepository.hasDateCollision(certificateDateCheckDto); + if (collisionForActiveRecords.hasCollision) { + throw new ConflictException( + `${ResponseMessages.x509.error.collisionForActivatingX5c}. Conflict Records:[${collisionForActiveRecords.collisions.map((collision) => collision.id)}]` + ); + } + const statusDto: UpdateCertificateStatusDto = { + status: x5cRecordStatus.Active, + lastChangedBy: user.id + }; + + return this.x509CertificateRepository.updateStatus(id, statusDto); + } + + throw new NotFoundException(ResponseMessages.x509.error.notFound); + } + + async deActivateCertificate(payload: { orgId: string; id: string; user: user }): Promise { + const { orgId, user, id } = payload; + const certificateRecord = await this.x509CertificateRepository.findById(orgId, id); + if (certificateRecord) { + const statusDto: UpdateCertificateStatusDto = { + status: x5cRecordStatus.InActive, + lastChangedBy: user.id + }; + + return this.x509CertificateRepository.updateStatus(id, statusDto); + } + throw new NotFoundException(ResponseMessages.x509.error.notFound); + } + + async importCertificate(payload: { + orgId: string; + options: IX509ImportCertificateOptionsDto; + user: user; + }): Promise { + try { + const { options, user, orgId } = payload; + const url = await getAgentUrl(await this.getAgentEndpoint(orgId), CommonConstants.X509_DECODE_CERTIFICATE); + + this.logger.log(`Decoding certificate to import`); + const decodedResult = await this._decodeX509CertificateForOrg({ certificate: options.certificate }, url, orgId); + if (!decodedResult || !decodedResult.response) { + this.logger.error(`Failed to decode certificate`); + throw new NotFoundException(ResponseMessages.x509.error.errorDecode); + } + + this.logger.log(`Decoded certificate`); + this.logger.debug(`certificate data:`, JSON.stringify(decodedResult)); + + const { publicKey } = decodedResult.response; + const decodedCert = decodedResult.response.x509Certificate; + + this.logger.log(`Start validating certificate`); + const isValidKeyType = Object.values(x5cKeyType).includes(publicKey.keyType as x5cKeyType); + + if (!isValidKeyType) { + this.logger.error(`keyType is not valid for importing certificate`); + throw new InternalServerErrorException(ResponseMessages.x509.error.import); + } + + const validFrom = new Date(decodedCert.notBefore); + const expiry = new Date(decodedCert.notAfter); + const certificateDateCheckDto: CertificateDateCheckDto = { + orgId, + validFrom, + expiry, + keyType: publicKey.keyType, + status: x5cRecordStatus.Active + }; + const collisionForActiveRecords = await this.x509CertificateRepository.hasDateCollision(certificateDateCheckDto); + + let certStatus: x5cRecordStatus; + if (collisionForActiveRecords.hasCollision) { + certificateDateCheckDto.status = x5cRecordStatus.PendingActivation; + const collisionForPendingRecords = + await this.x509CertificateRepository.hasDateCollision(certificateDateCheckDto); + + if (collisionForPendingRecords.hasCollision) { + this.logger.log(`Importing x509 certificate has collision`); + this.logger.error(`Collision records`, collisionForPendingRecords); + throw new UnprocessableEntityException(ResponseMessages.x509.error.collision); + } + certStatus = x5cRecordStatus.PendingActivation; + } else { + certStatus = x5cRecordStatus.Active; + } + const importurl = await getAgentUrl(await this.getAgentEndpoint(orgId), CommonConstants.X509_IMPORT_CERTIFICATE); + + this.logger.log(`Certificate validation done`); + const certificate = await this._importX509CertificateForOrg(options, importurl, orgId); + if (!certificate) { + throw new NotFoundException(ResponseMessages.x509.error.errorCreate); + } + this.logger.log(`Successfully imported certificate in wallet `); + const createDto: CreateX509CertificateEntity = { + orgId, + certificateBase64: certificate.response.issuerCertficicate, + keyType: publicKey.keyType, + status: certStatus, + validFrom, + expiry, + createdBy: user.id, + lastChangedBy: user.id + }; + this.logger.log(`Now adding certificate in platform for org : ${orgId} `); + + return await this.x509CertificateRepository.create(createDto); + } catch (error) { + this.logger.error(`Error in importing certificate: ${error}`); + throw new RpcException(error.response ? error.response : error); + } + } + + async getCertificateByOrgId( + orgId: string, + options: IX509SearchCriteria + ): Promise<{ data: X509CertificateRecord[]; total: number }> { + return this.x509CertificateRepository.findAll({ + orgId, + keyType: options.keyType, + status: options.status, + limit: options.pageSize, + page: options.pageNumber + }); + } + + async getCertificateById(orgId: string, id: string): Promise { + return this.x509CertificateRepository.findById(orgId, id); + } + + async _createX509CertificateForOrg( + options: X509CreateCertificateOptions, + url: string, + orgId: string + ): Promise<{ + response; + }> { + try { + const pattern = { cmd: 'agent-create-x509-certificate' }; + const payload = { options, url, orgId }; + this.logger.log(`Requesing agent service for create x509 certificate`); + this.logger.debug(`agent service payload - _createX509CertificateForOrg : `, payload); + return await this.natsCall(pattern, payload); + } catch (error) { + this.logger.error(`[_createX509CertificateForOrg] [NATS call]- : ${JSON.stringify(error)}`); + throw error; + } + } + + async _decodeX509CertificateForOrg( + options: x509CertificateDecodeDto, + url: string, + orgId: string + ): Promise<{ + response; + }> { + try { + const pattern = { cmd: 'agent-decode-x509-certificate' }; + const payload = { options, url, orgId }; + this.logger.log(`Requesing agent service for decode x509 certificate`); + this.logger.debug(`agent service payload - _decodeX509CertificateForOrg : `, payload); + return await this.natsCall(pattern, payload); + } catch (error) { + this.logger.error(`[_decodeX509CertificateForOrg] [NATS call]- : ${JSON.stringify(error)}`); + throw error; + } + } + + async _importX509CertificateForOrg( + options: IX509ImportCertificateOptionsDto, + url: string, + orgId: string + ): Promise<{ + response; + }> { + try { + const pattern = { cmd: 'agent-import-x509-certificate' }; + const payload = { options, url, orgId }; + this.logger.log(`Requesing agent service for importing x509 certificate`); + this.logger.debug(`agent service payload - _importX509CertificateForOrg : `, payload); + return await this.natsCall(pattern, payload); + } catch (error) { + this.logger.error(`[_importX509CertificateForOrg] [NATS call]- : ${JSON.stringify(error)}`); + throw error; + } + } + + async natsCall( + pattern: object, + payload: object + ): Promise<{ + response: string; + }> { + try { + return this.x509ServiceProxy + .send(pattern, payload) + .pipe( + map((response) => ({ + response + })) + ) + .toPromise() + .catch((error) => { + this.logger.error(`catch: ${JSON.stringify(error)}`); + throw new HttpException( + { + status: error.statusCode, + error: error.message + }, + error.statusCode + ); + }); + } catch (error) { + this.logger.error(`[natsCall] - error in nats call : ${JSON.stringify(error)}`); + throw error; + } + } + + async getAgentEndpoint(orgId: string): Promise { + const agentDetails = await this.x509CertificateRepository.getAgentEndPoint(orgId); + + if (!agentDetails) { + throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); + } + + if (!agentDetails.agentEndPoint || '' === agentDetails.agentEndPoint.trim()) { + throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); + } + + return agentDetails.agentEndPoint; + } +} diff --git a/apps/x509/tsconfig.app.json b/apps/x509/tsconfig.app.json new file mode 100644 index 000000000..812e0f82c --- /dev/null +++ b/apps/x509/tsconfig.app.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": false, + "outDir": "../../dist/apps/x509" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] +} diff --git a/libs/common/src/cast.helper.ts b/libs/common/src/cast.helper.ts index 7d030a6c9..a4da5a2bb 100644 --- a/libs/common/src/cast.helper.ts +++ b/libs/common/src/cast.helper.ts @@ -523,3 +523,20 @@ export function ValidateNestedStructureFields(validationOptions?: ValidationOpti }); }; } + +export function buildUrlWithQuery>(baseUrl: string, queryParams: T): string { + const criteriaParams: string[] = []; + + if (!queryParams || (queryParams?.length >= 0)) { + return baseUrl + } + + for (const [key, value] of Object.entries(queryParams)) { + // Skip undefined or null values + if (value !== undefined && value !== null) { + criteriaParams.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`); + } + } + + return criteriaParams.length > 0 ? `${baseUrl}?${criteriaParams.join('&')}` : baseUrl; +} \ No newline at end of file diff --git a/libs/common/src/common.constant.ts b/libs/common/src/common.constant.ts index 7de2755a3..b75426178 100644 --- a/libs/common/src/common.constant.ts +++ b/libs/common/src/common.constant.ts @@ -117,6 +117,30 @@ export enum CommonConstants { // CREATE KEYS CREATE_POLYGON_SECP256k1_KEY = '/polygon/create-keys', + // OID4VC URLs + URL_OIDC_ISSUER_CREATE = '/openid4vc/issuer', + /* eslint-disable @typescript-eslint/naming-convention, @typescript-eslint/explicit-function-return-type, @typescript-eslint/explicit-module-boundary-types, camelcase, @typescript-eslint/no-duplicate-enum-values */ + URL_OIDC_GET_ISSUERS = '/openid4vc/issuer', + URL_OIDC_ISSUER_UPDATE = '/openid4vc/issuer/#', + URL_OIDC_ISSUER_SESSIONS_CREATE = '/openid4vc/issuance-sessions/create-credential-offer', + URL_OIDC_ISSUER_SESSIONS_GET = '/openid4vc/issuance-sessions/#', + URL_OIDC_ISSUER_SESSIONS_GET_ALL = '/openid4vc/issuance-sessions', + + // OID4Vp URLs + URL_OIDC_VERIFIER_CREATE = '/openid4vc/verifier', //TODO: correct this URL + URL_OIDC_VERIFIER_UPDATE = '/openid4vc/verifier/#', + URL_OIDC_VERIFIER_DELETE = '/openid4vc/verifier/#', + URL_OIDC_VERIFIER_GET = '/openid4vc/verifier/#', + URL_OIDC_VERIFIER_SESSION_GET_BY_ID = '/openid4vc/verification-sessions/#', + URL_OIDC_VERIFIER_SESSION_GET_BY_QUERY = '/openid4vc/verification-sessions', + URL_OIDC_VERIFIER_SESSION_RESPONSE_GET_BY_ID = '/openid4vc/verification-sessions/response/#', + URL_OID4VP_VERIFICATION_SESSION = '/openid4vc/verification-sessions/create-presentation-request', + + //X509 agent API URLs + URL_CREATE_X509_CERTIFICATE = '/x509', + URL_IMPORT_X509_CERTIFICATE = '/x509/import', + URL_DECODE_X509_CERTIFICATE = '/x509/decode', + // Nested attribute separator NESTED_ATTRIBUTE_SEPARATOR = '~', @@ -364,6 +388,10 @@ export enum CommonConstants { NOTIFICATION_SERVICE = 'notification', GEO_LOCATION_SERVICE = 'geo-location', CLOUD_WALLET_SERVICE = 'cloud-wallet', + OIDC4VC_ISSUANCE_SERVICE = 'oid4vc-issuance', + OIDC4VC_VERIFICATION_SERVICE = 'oid4vc-verification', + OID4VP_VERIFICATION_SESSION = 'oid4vp-verification-session', + X509_SERVICE = 'x509-service', ACCEPT_OFFER = '/didcomm/credentials/accept-offer', SEED_LENGTH = 32, @@ -383,7 +411,32 @@ export enum CommonConstants { GET_VERIFIED_PROOF = 'get-verified-proof', GET_QUESTION_ANSWER_RECORD = 'get-question-answer-record', SEND_QUESTION = 'send-question', - SEND_BASIC_MESSAGE = 'send-basic-message' + SEND_BASIC_MESSAGE = 'send-basic-message', + + // OID4VC + OIDC_ISSUER_CREATE = 'create-oid4vc-issuer', + OIDC_ISSUER_DELETE = 'delete-oid4vc-issuer', + OIDC_GET_ALL_ISSUERS = 'get-all-oid4vc-issuers', + OIDC_ISSUER_BY_ID = 'get-issuer-by-id', + OIDC_ISSUER_TEMPLATE = 'create-oid4vc-template', + OIDC_ISSUER_SESSIONS_CREDENTIAL_OFFER = 'create-oid4vc-credential-offer', + OIDC_ISSUER_SESSIONS_UPDATE_OFFER = 'update-oid4vc-credential-offer', + OIDC_ISSUER_SESSIONS_BY_ID = 'get-oid4vc-session-by-id', + OIDC_ISSUER_SESSIONS = 'get-oid4vc-sessions', + OIDC_DELETE_CREDENTIAL_OFFER = 'delete-oid4vc-credential-offer', + + // OID4VP + OIDC_VERIFIER_CREATE = 'create-oid4vp-verifier', + OIDC_VERIFIER_UPDATE = 'update-oid4vp-verifier', + OIDC_VERIFIER_DELETE = 'delete-oid4vp-verifier', + OIDC_VERIFIER_SESSION_GET_BY_ID = 'get-oid4vp-verifier-session-id', + OIDC_VERIFIER_SESSION_GET_BY_QUERY = 'get-oid4vp-verifier-session-query', + OIDC_VERIFIER_SESSION_RESPONSE_GET_BY_ID = 'get-oid4vp-verifier-session-response-id', + + //X509 + X509_CREATE_CERTIFICATE = 'create-x509-certificate', + X509_IMPORT_CERTIFICATE = 'import-x509-certificate', + X509_DECODE_CERTIFICATE = 'decode-x509-certificate' } export const MICRO_SERVICE_NAME = Symbol('MICRO_SERVICE_NAME'); export const ATTRIBUTE_NAME_REGEX = /\['(.*?)'\]/; diff --git a/libs/common/src/common.utils.ts b/libs/common/src/common.utils.ts index 938e795c3..204cd3df4 100644 --- a/libs/common/src/common.utils.ts +++ b/libs/common/src/common.utils.ts @@ -67,11 +67,10 @@ export const networkNamespace = (did: string): string => { return segments[1]; }; -export const getAgentUrl = async (agentEndPoint: string, urlFlag: string, paramId?: string): Promise => { +export const getAgentUrl = (agentEndPoint: string, urlFlag: string, paramId?: string): string => { if (!agentEndPoint) { throw new NotFoundException(ResponseMessages.common.error.invalidEndpoint); } - const agentUrlMap: Map = new Map([ [String(CommonConstants.CONNECTION_INVITATION), String(CommonConstants.URL_CONN_INVITE)], [String(CommonConstants.LEGACY_INVITATION), String(CommonConstants.URL_CONN_LEGACY_INVITE)], @@ -93,7 +92,39 @@ export const getAgentUrl = async (agentEndPoint: string, urlFlag: string, paramI [String(CommonConstants.GET_VERIFIED_PROOF), String(CommonConstants.URL_PROOF_FORM_DATA)], [String(CommonConstants.GET_QUESTION_ANSWER_RECORD), String(CommonConstants.URL_QUESTION_ANSWER_RECORD)], [String(CommonConstants.SEND_QUESTION), String(CommonConstants.URL_SEND_QUESTION)], - [String(CommonConstants.SEND_BASIC_MESSAGE), String(CommonConstants.URL_SEND_BASIC_MESSAGE)] + [String(CommonConstants.SEND_BASIC_MESSAGE), String(CommonConstants.URL_SEND_BASIC_MESSAGE)], + [String(CommonConstants.OIDC_ISSUER_CREATE), String(CommonConstants.URL_OIDC_ISSUER_CREATE)], + [String(CommonConstants.OIDC_GET_ALL_ISSUERS), String(CommonConstants.URL_OIDC_GET_ISSUERS)], + [String(CommonConstants.OIDC_ISSUER_DELETE), String(CommonConstants.URL_OIDC_ISSUER_UPDATE)], + [String(CommonConstants.OIDC_ISSUER_BY_ID), String(CommonConstants.URL_OIDC_ISSUER_UPDATE)], + [String(CommonConstants.OIDC_ISSUER_TEMPLATE), String(CommonConstants.URL_OIDC_ISSUER_UPDATE)], + [ + String(CommonConstants.OIDC_ISSUER_SESSIONS_CREDENTIAL_OFFER), + String(CommonConstants.URL_OIDC_ISSUER_SESSIONS_CREATE) + ], + [String(CommonConstants.OIDC_ISSUER_SESSIONS_UPDATE_OFFER), String(CommonConstants.URL_OIDC_ISSUER_SESSIONS_GET)], + [String(CommonConstants.OIDC_ISSUER_SESSIONS_BY_ID), String(CommonConstants.URL_OIDC_ISSUER_SESSIONS_GET)], + [String(CommonConstants.OIDC_ISSUER_SESSIONS), String(CommonConstants.URL_OIDC_ISSUER_SESSIONS_GET_ALL)], + [String(CommonConstants.OIDC_DELETE_CREDENTIAL_OFFER), String(CommonConstants.URL_OIDC_ISSUER_SESSIONS_GET_ALL)], + [String(CommonConstants.X509_CREATE_CERTIFICATE), String(CommonConstants.URL_CREATE_X509_CERTIFICATE)], + [String(CommonConstants.X509_DECODE_CERTIFICATE), String(CommonConstants.URL_DECODE_X509_CERTIFICATE)], + [String(CommonConstants.X509_IMPORT_CERTIFICATE), String(CommonConstants.URL_IMPORT_X509_CERTIFICATE)], + [String(CommonConstants.OIDC_VERIFIER_CREATE), String(CommonConstants.URL_OIDC_VERIFIER_CREATE)], + [String(CommonConstants.OIDC_VERIFIER_UPDATE), String(CommonConstants.URL_OIDC_VERIFIER_UPDATE)], + [String(CommonConstants.OIDC_VERIFIER_DELETE), String(CommonConstants.URL_OIDC_VERIFIER_DELETE)], + [ + String(CommonConstants.OIDC_VERIFIER_SESSION_GET_BY_ID), + String(CommonConstants.URL_OIDC_VERIFIER_SESSION_GET_BY_ID) + ], + [ + String(CommonConstants.OIDC_VERIFIER_SESSION_GET_BY_QUERY), + String(CommonConstants.URL_OIDC_VERIFIER_SESSION_GET_BY_QUERY) + ], + [ + String(CommonConstants.OIDC_VERIFIER_SESSION_RESPONSE_GET_BY_ID), + String(CommonConstants.URL_OIDC_VERIFIER_SESSION_RESPONSE_GET_BY_ID) + ], + [String(CommonConstants.OID4VP_VERIFICATION_SESSION), String(CommonConstants.URL_OID4VP_VERIFICATION_SESSION)] ]); const urlSuffix = agentUrlMap.get(urlFlag); @@ -107,3 +138,9 @@ export const getAgentUrl = async (agentEndPoint: string, urlFlag: string, paramI const url = `${agentEndPoint}${resolvedUrlPath}`; return url; }; + +export function shouldLoadOidcModules(): boolean { + const raw = process.env.HIDE_EXPERIMENTAL_OIDC_CONTROLLERS ?? 'true'; + const hide = 'true' === raw.toLowerCase(); + return !hide; +} diff --git a/libs/common/src/date-only.ts b/libs/common/src/date-only.ts new file mode 100644 index 000000000..5cd199339 --- /dev/null +++ b/libs/common/src/date-only.ts @@ -0,0 +1,58 @@ +const customInspectSymbol = Symbol.for('nodejs.util.inspect.custom'); +export class DateOnly { + private date: Date; + + public constructor(date?: string | Date) { + if (date instanceof Date) { + if (isNaN(date.getTime())) { + throw new TypeError('Invalid Date'); + } + this.date = date; + return; + } + if (!date) { + this.date = new Date(); + return; + } + // Accept only YYYY-MM-DD or full ISO strings + const iso = /^\d{4}-\d{2}-\d{2}(T.*Z)?$/.test(date) ? date : ''; + const d = new Date(iso || date); + if (isNaN(d.getTime())) { + throw new TypeError('Invalid date string'); + } + this.date = d; + } + + get [Symbol.toStringTag](): string { + return DateOnly.name; + } + + toString(): string { + return this.toISOString(); + } + + toJSON(): string { + return this.toISOString(); + } + + toISOString(): string { + return this.date.toISOString().split('T')[0]; + } + + [customInspectSymbol](): string { + return this.toISOString(); + } +} + +export const oneDayInMilliseconds = 24 * 60 * 60 * 1000; +export const tenDaysInMilliseconds = 10 * oneDayInMilliseconds; +export const oneYearInMilliseconds = 365 * oneDayInMilliseconds; +export const serverStartupTimeInMilliseconds = Date.now(); + +export function dateToSeconds(date: Date | DateOnly): number { + const realDate = date instanceof DateOnly ? new Date(date.toISOString()) : date; + if (isNaN(realDate.getTime())) { + throw new TypeError('dateToSeconds: invalid date'); + } + return Math.floor(realDate.getTime() / 1000); +} diff --git a/libs/common/src/interfaces/oid4vp-verification.ts b/libs/common/src/interfaces/oid4vp-verification.ts new file mode 100644 index 000000000..8c0e8b447 --- /dev/null +++ b/libs/common/src/interfaces/oid4vp-verification.ts @@ -0,0 +1,33 @@ +export interface ClientMetadata { + client_name: string; + logo_uri: string; +} + +export interface CreateVerifier { + verifierId: string; + clientMetadata?: ClientMetadata; +} + +export interface UpdateVerifier extends Omit { + publicVerifierId?: string; +} + +export interface VerifierRecord { + _tags: Record; + metadata: Record; + id: string; + createdAt: string; // ISO timestamp + verifierId: string; + clientMetadata: { + client_name: string; + logo_uri: string; + }; + updatedAt: string; // ISO timestamp +} + +export enum OpenId4VcVerificationPresentationState { + RequestCreated = 'RequestCreated', + RequestUriRetrieved = 'RequestUriRetrieved', + ResponseVerified = 'ResponseVerified', + Error = 'Error' +} diff --git a/libs/common/src/interfaces/x509.interface.ts b/libs/common/src/interfaces/x509.interface.ts new file mode 100644 index 000000000..05ee04f88 --- /dev/null +++ b/libs/common/src/interfaces/x509.interface.ts @@ -0,0 +1,246 @@ +import { X509ExtendedKeyUsage, X509KeyUsage, x5cKeyType, KeyType } from '@credebl/enum/enum'; + +// Enum remains the same +export enum GeneralNameType { + DNS = 'dns', + DN = 'dn', + EMAIL = 'email', + GUID = 'guid', + IP = 'ip', + URL = 'url', + UPN = 'upn', + REGISTERED_ID = 'id' +} + +export interface AuthorityAndSubjectKey { + // /** + // * @example "my-seed-12345" + // * @description Seed to deterministically derive the key (optional) + // */ + // seed?: string; + + // /** + // * @example "3yPQbnk6WwLgX8K3JZ4t7vBnJ8XqY2mMpRcD9fNvGtHw" + // * @description publicKeyBase58 for using existing key in wallet (optional) + // */ + // publicKeyBase58?: string; + + /** + * @example "p256" + * @description Type of the key used for signing the X.509 Certificate (default is p256) + */ + keyType?: x5cKeyType; +} + +export interface Name { + /** + * @example "dns" + */ + type: GeneralNameType; + + /** + * @example "example.com" + */ + value: string; +} + +export interface X509CertificateIssuerAndSubjectOptions { + /** + * @example "US" + */ + countryName?: string; + + /** + * @example "California" + */ + stateOrProvinceName?: string; + + /** + * @example "IT Department" + */ + organizationalUnit?: string; + + /** + * @example "Example Corporation" + */ + commonName?: string; +} + +export interface Validity { + /** + * @example "2024-01-01T00:00:00.000Z" + */ + notBefore?: Date; + + /** + * @example "2025-01-01T00:00:00.000Z" + */ + notAfter?: Date; +} + +export interface KeyUsage { + /** + * @example ["digitalSignature", "keyEncipherment", "crlSign"] + */ + usages: X509KeyUsage[]; + + /** + * @example true + */ + markAsCritical?: boolean; +} + +export interface ExtendedKeyUsage { + /** + * @example ["MdlDs", "ServerAuth", "ClientAuth"] + */ + usages: X509ExtendedKeyUsage[]; + + /** + * @example true + */ + markAsCritical?: boolean; +} + +export interface NameList { + /** + * @example [{ "type": "dns", "value": "example.com" }, { "type": "email", "value": "admin@example.com" }] + */ + name: Name[]; + + /** + * @example true + */ + markAsCritical?: boolean; +} + +export interface AuthorityAndSubjectKeyIdentifier { + /** + * @example true + */ + include: boolean; + + /** + * @example true + */ + markAsCritical?: boolean; +} + +export interface BasicConstraints { + /** + * @example false + */ + ca: boolean; + + /** + * @example 0 + */ + pathLenConstraint?: number; + + /** + * @example true + */ + markAsCritical?: boolean; +} + +export interface CrlDistributionPoints { + /** + * @example ["http://crl.example.com/ca.crl"] + */ + urls: string[]; + + /** + * @example true + */ + markAsCritical?: boolean; +} + +export interface X509CertificateExtensionsOptions { + keyUsage?: KeyUsage; + extendedKeyUsage?: ExtendedKeyUsage; + authorityKeyIdentifier?: AuthorityAndSubjectKeyIdentifier; + subjectKeyIdentifier?: AuthorityAndSubjectKeyIdentifier; + issuerAlternativeName?: NameList; + subjectAlternativeName?: NameList; + basicConstraints?: BasicConstraints; + crlDistributionPoints?: CrlDistributionPoints; +} + +export interface X509CreateCertificateOptions { + authorityKey?: AuthorityAndSubjectKey; + subjectPublicKey?: AuthorityAndSubjectKey; + + /** + * @example "1234567890" + */ + serialNumber?: string; + + /** + * @example { + * "countryName": "US", + * "stateOrProvinceName": "California", + * "commonName": "Example CA" + * } + * OR + * @example "/C=US/ST=California/O=Example Corporation/CN=Example CA" + */ + issuer: X509CertificateIssuerAndSubjectOptions | string; + + /** + * @example { + * "countryName": "US", + * "commonName": "www.example.com" + * } + * OR + * @example "/C=US/CN=www.example.com" + */ + subject?: X509CertificateIssuerAndSubjectOptions | string; + + validity?: Validity; + extensions?: X509CertificateExtensionsOptions; +} + +export interface X509CertificateRecord { + id: string; + orgAgentId: string; + keyType: string; + status: string; + validFrom: Date; + expiry: Date; + certificateBase64: string; + createdBy: string; + lastChangedBy: string; + createdAt: Date; + lastChangedDateTime: Date; +} + +export interface IX509SearchCriteria extends IPaginationSortingdto { + keyType: string; + status: string; +} + +export interface IPaginationSortingdto { + pageNumber: number; + pageSize: number; + sortField?: string; + sortBy?: string; + searchByText?: string; +} + +export interface IX509ImportCertificateOptionsDto { + /* + X.509 certificate in base64 string format + */ + certificate: string; + + /* + Private key in base64 string format + */ + privateKey?: string; + + keyType: KeyType; +} + +export interface x509CertificateDecodeDto { + certificate: string; +} diff --git a/libs/common/src/nats.config.ts b/libs/common/src/nats.config.ts index 7fd1ff011..984459979 100644 --- a/libs/common/src/nats.config.ts +++ b/libs/common/src/nats.config.ts @@ -14,8 +14,8 @@ export const getNatsOptions = ( const baseOptions = { servers: `${process.env.NATS_URL}`.split(','), maxReconnectAttempts: NATSReconnects.maxReconnectAttempts, - reconnectTimeWait: NATSReconnects.reconnectTimeWait, - queue: serviceName + reconnectTimeWait: NATSReconnects.reconnectTimeWait + // queue: serviceName }; if (nkeySeed) { diff --git a/libs/common/src/response-messages/index.ts b/libs/common/src/response-messages/index.ts index bd8f5f6fd..83e72d546 100644 --- a/libs/common/src/response-messages/index.ts +++ b/libs/common/src/response-messages/index.ts @@ -503,6 +503,119 @@ export const ResponseMessages = { walletRecordNotFound: 'Wallet record not found.' } }, + oidcIssuer: { + success: { + create: 'OID4VC issuer created successfully.', + update: 'OID4VC issuer updated successfully.', + delete: 'OID4VC issuer deleted successfully.', + fetch: 'OID4VC issuer(s) fetched successfully.', + issuerConfig: 'Issuer config details created successfully', + issuerConfigUpdate: 'Issuer config details updated successfully' + }, + error: { + notFound: 'OID4VC issuer not found.', + invalidId: 'Invalid OID4VC issuer ID.', + createFailed: 'Failed to create OID4VC issuer.', + updateFailed: 'Failed to update OID4VC issuer.', + deleteFailed: 'Failed to delete OID4VC issuer.' + } + }, + oidcTemplate: { + success: { + create: 'OID4VC template created successfully.', + update: 'OID4VC template updated successfully.', + delete: 'OID4VC template deleted successfully.', + fetch: 'OID4VC template(s) fetched successfully.', + getById: 'OID4VC template details fetched successfully.', + deleteTemplate: '[createTemplate] compensating delete succeeded for templateId=${templateId}' + }, + error: { + notFound: 'OID4VC template not found.', + invalidId: 'Invalid OID4VC template ID.', + createFailed: 'Failed to create OID4VC template.', + updateFailed: 'Failed to update OID4VC template.', + deleteFailed: 'Failed to delete OID4VC template.', + issuerDisplayNotFound: 'Issuer display not found.', + issuerDetailsNotFound: 'Issuer details not found.', + templateNameAlreadyExist: 'Template name already exists for this issuer.', + deleteTemplate: 'Error while deleting template.', + failedDeleteTemplate: '[createTemplate] compensating delete FAILED for templateId=' + } + }, + oidcIssuerSession: { + success: { + create: 'OID4VC Credential offer created successfully.', + getById: 'OID4VC Credential offer details fetched successfully.', + getAll: 'OID4VC Credential offers fetched successfully.', + update: 'OID4VC Credential offer updated successfully.', + delete: 'OID4VC Credential offer deleted successfully.' + }, + error: { + errorCreateOffer: 'Error while creating OID4VC credential offer on agent.', + errorUpdateOffer: 'Error while updating OID4VC credential offer on agent.', + deleteFailed: 'Failed to delete OID4VC credential offer.', + missingValidityInfo: 'Validity Info(validFrom, validTo) is required for validity of credential' + } + }, + oid4vp: { + success: { + create: 'OID4VP verifier created successfully.', + update: 'OID4VP verifier updated successfully.', + delete: 'OID4VP verifier deleted successfully.', + fetch: 'OID4VP verifier(s) fetched successfully.', + getById: 'OID4VP verifier details fetched successfully.' + }, + error: { + notFound: 'OID4VP verifier not found.', + invalidId: 'Invalid OID4VP verifier ID.', + createFailed: 'Failed to create OID4VP verifier.', + updateFailed: 'Failed to update OID4VP verifier.', + deleteFailed: 'Failed to delete OID4VP verifier.', + verifierIdAlreadyExists: 'Verifier ID already exists for this verifier.' + } + }, + oid4vpSession: { + success: { + create: 'OID4VP session verifier created successfully.', + update: 'OID4VP session verifier updated successfully.', + delete: 'OID4VP session verifier deleted successfully.', + fetch: 'OID4VP session verifier(s) fetched successfully.', + getById: 'OID4VP session verifier details fetched successfully.', + webhookReceived: 'OID4VP presentation webhook stored successfully.' + }, + error: { + notFound: 'OID4VP session verifier not found.', + invalidId: 'Invalid OID4VP session verifier ID.', + createFailed: 'Failed to create OID4VP session verifier.', + updateFailed: 'Failed to update OID4VP session verifier.', + deleteFailed: 'Failed to delete OID4VP session verifier.', + verifierIdAlreadyExists: 'Verifier ID already exists for this verifier.', + deleteTemplate: 'Error while deleting template.', + responseNotFound: 'Verification session response not found.' + } + }, + x509: { + success: { + create: 'x509 certificate created successfully', + activated: 'x509 certificate activated successfully', + deActivated: 'x509 certificate deactivated successfully', + fetch: 'x509 certificate fetched successfully', + fetchAll: 'x509 certificates fetched successfully', + import: 'x509 certificate imported successfully' + }, + error: { + errorCreate: 'Error while creating x509 certificate.', + errorUpdateStatus: 'Error while updating x509 certificate.', + errorActivation: 'Failed to activate x509 certificate.', + agentEndPointNotFound: 'Agent details not found', + collision: 'Certificate date range collides with existing certificates for this organization', + collisionForActivatingX5c: + 'Certificate date range collides with existing certificates for this organization, In order to active this you need to Inactivate the previous one.', + notFound: 'x509 certificate record not found.', + import: 'Failed to import x509 certificate', + errorDecode: 'Error while decoding x509 certificate.' + } + }, nats: { success: {}, error: { diff --git a/libs/context/src/contextInterceptorModule.ts b/libs/context/src/contextInterceptorModule.ts index a80a712f9..c978ac2de 100644 --- a/libs/context/src/contextInterceptorModule.ts +++ b/libs/context/src/contextInterceptorModule.ts @@ -1,14 +1,10 @@ -import { ExecutionContext, Global, Module} from '@nestjs/common'; -import { v4 } from 'uuid'; +import { ExecutionContext, Global, Logger, Module } from '@nestjs/common'; +import { v4 as uuid } from 'uuid'; import { ClsModule } from 'nestjs-cls'; import { ContextStorageServiceKey } from './contextStorageService.interface'; import NestjsClsContextStorageService from './nestjsClsContextStorageService'; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const isNullUndefinedOrEmpty = (obj: any): boolean => null === obj || obj === undefined || ('object' === typeof obj && 0 === Object.keys(obj).length); - @Global() @Module({ imports: [ @@ -17,17 +13,31 @@ const isNullUndefinedOrEmpty = (obj: any): boolean => null === obj || obj === un interceptor: { mount: true, - generateId: true, - idGenerator: (context: ExecutionContext) => { - const rpcContext = context.switchToRpc().getContext(); - const headers = rpcContext.getHeaders(); - if (!isNullUndefinedOrEmpty(headers)) { - return context.switchToRpc().getContext().getHeaders()['_description']; - } else { - return v4(); + generateId: true, + idGenerator: (context: ExecutionContext) => { + const logger = new Logger('ContextInterceptorModule'); + try { + const rpcContext = context.switchToRpc().getContext(); + const headers = rpcContext.getHeaders() ?? {}; + const contextId = headers.get?.('contextId'); + + if (contextId) { + logger.debug(`[idGenerator] Received contextId in headers: ${contextId}`); + return contextId; + } else { + const uuidGenerated = uuid(); + logger.debug( + '[idGenerator] Did not receive contextId in header, generated new contextId: ', + uuidGenerated + ); + return uuidGenerated; + } + } catch (error) { + // eslint-disable-next-line no-console + logger.error('[idGenerator] Error in idGenerator, generating fallback UUID', error); + return uuid(); } - } - + } } }) ], @@ -40,6 +50,4 @@ const isNullUndefinedOrEmpty = (obj: any): boolean => null === obj || obj === un ], exports: [ContextStorageServiceKey] }) - export class ContextInterceptorModule {} - diff --git a/libs/context/src/contextModule.ts b/libs/context/src/contextModule.ts index 4bfb9e2c3..a56df5510 100644 --- a/libs/context/src/contextModule.ts +++ b/libs/context/src/contextModule.ts @@ -4,6 +4,7 @@ import { ClsModule } from 'nestjs-cls'; import { ContextStorageServiceKey } from './contextStorageService.interface'; import NestjsClsContextStorageService from './nestjsClsContextStorageService'; +import { Request } from 'express'; @Global() @Module({ @@ -13,7 +14,24 @@ import NestjsClsContextStorageService from './nestjsClsContextStorageService'; middleware: { mount: true, generateId: true, - idGenerator: (req: Request) => req.headers['x-correlation-id'] ?? v4() + idGenerator: (req: Request) => { + // TODO: Check if we want the x-correlation-id or the correlationId + const contextIdHeader = req.headers['contextid'] ?? req.headers['context-id'] ?? req.headers['contextId']; + const correlationIdHeader = req.headers['x-correlation-id']; + let resolvedContextId = + (Array.isArray(contextIdHeader) ? contextIdHeader[0] : contextIdHeader) ?? + (Array.isArray(correlationIdHeader) ? correlationIdHeader[0] : correlationIdHeader); + + if (resolvedContextId) { + // eslint-disable-next-line no-console + console.log('ContextId received in request headers::::', resolvedContextId); + } else { + resolvedContextId = v4(); + // eslint-disable-next-line no-console + console.log('ContextId not received in request headers, generated a new one::::', resolvedContextId); + } + return resolvedContextId; + } } }) ], diff --git a/libs/context/src/nestjsClsContextStorageService.ts b/libs/context/src/nestjsClsContextStorageService.ts index 00107f21e..7c5593057 100644 --- a/libs/context/src/nestjsClsContextStorageService.ts +++ b/libs/context/src/nestjsClsContextStorageService.ts @@ -3,17 +3,14 @@ import { CLS_ID, ClsService } from 'nestjs-cls'; import { Injectable } from '@nestjs/common'; @Injectable() -export default class NestjsClsContextStorageService - implements ContextStorageService -{ - constructor(private readonly cls: ClsService) { - } +export default class NestjsClsContextStorageService implements ContextStorageService { + constructor(private readonly cls: ClsService) {} public get(key: string): T | undefined { return this.cls.get(key); } - public setContextId(id: string) : void { + public setContextId(id: string): void { this.cls.set(CLS_ID, id); } @@ -24,4 +21,4 @@ export default class NestjsClsContextStorageService public set(key: string, value: T): void { this.cls.set(key, value); } -} \ No newline at end of file +} diff --git a/libs/enum/src/enum.ts b/libs/enum/src/enum.ts index 9dc8b31ca..200f37674 100644 --- a/libs/enum/src/enum.ts +++ b/libs/enum/src/enum.ts @@ -271,3 +271,81 @@ export enum ProviderType { KEYCLOAK = 'keycloak', SUPABASE = 'supabase' } + +export enum OpenId4VcIssuanceSessionState { + OfferCreated = 'OfferCreated', + OfferUriRetrieved = 'OfferUriRetrieved', + AuthorizationInitiated = 'AuthorizationInitiated', + AuthorizationGranted = 'AuthorizationGranted', + AccessTokenRequested = 'AccessTokenRequested', + AccessTokenCreated = 'AccessTokenCreated', + CredentialRequestReceived = 'CredentialRequestReceived', + CredentialsPartiallyIssued = 'CredentialsPartiallyIssued', + Completed = 'Completed', + Error = 'Error' +} + +export enum x5cKeyType { + Ed25519 = 'ed25519', + P256 = 'p256' +} + +export enum x5cRecordStatus { + Active = 'Active', + PendingActivation = 'Pending activation', + InActive = 'In Active' +} + +export enum X509KeyUsage { + DigitalSignature = 1, + NonRepudiation = 2, + KeyEncipherment = 4, + DataEncipherment = 8, + KeyAgreement = 16, + KeyCertSign = 32, + CrlSign = 64, + EncipherOnly = 128, + DecipherOnly = 256 +} + +export enum X509ExtendedKeyUsage { + ServerAuth = '1.3.6.1.5.5.7.3.1', + ClientAuth = '1.3.6.1.5.5.7.3.2', + CodeSigning = '1.3.6.1.5.5.7.3.3', + EmailProtection = '1.3.6.1.5.5.7.3.4', + TimeStamping = '1.3.6.1.5.5.7.3.8', + OcspSigning = '1.3.6.1.5.5.7.3.9', + MdlDs = '1.0.18013.5.1.2' +} + +export enum CredentialFormat { + SdJwtVc = 'vc+sd-jwt', + Mdoc = 'mso_mdoc' +} + +export enum AttributeType { + STRING = 'string', + NUMBER = 'number', + BOOLEAN = 'boolean', + DATE = 'date', + OBJECT = 'object', + ARRAY = 'array', + IMAGE = 'image' +} + +export enum SignerMethodOption { + DID = 'did', + X5C = 'x5c' +} + +export declare enum HandshakeProtocol { + Connections = 'https://didcomm.org/connections/1.0', + DidExchange = 'https://didcomm.org/didexchange/1.0' +} + +export enum ResponseMode { + DIRECT_POST = 'direct_post', + DIRECT_POST_JWT = 'direct_post.jwt', + DC_API = 'dc_api', + DC_API_JWT = 'dc_api.jwt' +} diff --git a/libs/logger/src/logging.interceptor.ts b/libs/logger/src/logging.interceptor.ts index 6ac3a0611..301cca080 100644 --- a/libs/logger/src/logging.interceptor.ts +++ b/libs/logger/src/logging.interceptor.ts @@ -7,39 +7,43 @@ import Logger, { LoggerKey } from './logger.interface'; import { ClsService } from 'nestjs-cls'; import { v4 } from 'uuid'; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const isNullUndefinedOrEmpty = (obj: any): boolean => - null === obj || obj === undefined || ('object' === typeof obj && 0 === Object.keys(obj).length); - @Injectable() export class LoggingInterceptor implements NestInterceptor { constructor( private readonly clsService: ClsService, @Inject(ContextStorageServiceKey) private readonly contextStorageService: ContextStorageService, - @Inject(LoggerKey) private readonly _logger: Logger + @Inject(LoggerKey) private readonly logger: Logger ) {} // eslint-disable-next-line @typescript-eslint/no-explicit-any intercept(context: ExecutionContext, next: CallHandler): Observable { return this.clsService.run(() => { - this._logger.info('In LoggingInterceptor configuration'); + this.logger.info('In LoggingInterceptor configuration'); const rpcContext = context.switchToRpc().getContext(); - const headers = rpcContext.getHeaders(); + const headers = rpcContext.getHeaders?.() ?? rpcContext.getHeaders; + const contextIdFromHeader = headers && 'function' === typeof headers.get ? headers.get('contextId') : undefined; - if (!isNullUndefinedOrEmpty(headers) && headers._description) { - this.contextStorageService.set('x-correlation-id', headers._description); - this.contextStorageService.setContextId(headers._description); + if (contextIdFromHeader) { + this.logger.debug('We found context Id in header of logger Interceptor', contextIdFromHeader); + this.setupContextId(contextIdFromHeader); } else { const newContextId = v4(); - this.contextStorageService.set('x-correlation-id', newContextId); - this.contextStorageService.setContextId(newContextId); + this.logger.debug('Not found context Id in header of logger Interceptor, generating a new one:', newContextId); + this.setupContextId(newContextId); } + return next.handle().pipe( catchError((err) => { - this._logger.error(err); + this.logger.error('[intercept] Error in LoggingInterceptor', err); return throwError(() => err); }) ); }); } + + private setupContextId(contextIdFromHeader: string): void { + this.contextStorageService.set('x-correlation-id', contextIdFromHeader); + this.contextStorageService.set('contextId', contextIdFromHeader); + this.contextStorageService.setContextId(contextIdFromHeader); + } } diff --git a/libs/logger/src/winstonLogger.ts b/libs/logger/src/winstonLogger.ts index d2a8b0ef8..b9bbb165d 100644 --- a/libs/logger/src/winstonLogger.ts +++ b/libs/logger/src/winstonLogger.ts @@ -8,43 +8,46 @@ import * as ecsFormat from '@elastic/ecs-winston-format'; export const WinstonLoggerTransportsKey = Symbol(); let esTransport; if ('true' === process.env.ELK_LOG?.toLowerCase()) { - const esTransportOpts = { - level: `${process.env.LOG_LEVEL}`, - clientOpts: { node: `${process.env.ELK_LOG_PATH}`, - auth: { - username: `${process.env.ELK_USERNAME}`, - password: `${process.env.ELK_PASSWORD}` + const requiredVars = ['LOG_LEVEL', 'ELK_LOG_PATH', 'ELK_USERNAME', 'ELK_PASSWORD']; + const missingVars = requiredVars.filter((v) => !process.env[v]); + if (0 < missingVars.length) { + // eslint-disable-next-line no-console + console.warn(`Elasticsearch logging disabled: missing env vars [${missingVars.join(', ')}]`); + } else { + const esTransportOpts = { + level: `${process.env.LOG_LEVEL}`, + clientOpts: { + node: `${process.env.ELK_LOG_PATH}`, + auth: { + username: `${process.env.ELK_USERNAME}`, + password: `${process.env.ELK_PASSWORD}` + } + } + }; + esTransport = new Elasticsearch.ElasticsearchTransport(esTransportOpts); + esTransport.on('error', (error) => { + // eslint-disable-next-line no-console + console.error('Elasticsearch transport error:', error); + }); } - } -}; -esTransport = new Elasticsearch.ElasticsearchTransport(esTransportOpts); - -esTransport.on('error', (error) => { - console.error('Error caught in logger', error); -}); - } - @Injectable() export default class WinstonLogger implements Logger { private readonly logger: winston.Logger; - public constructor( - @Inject(WinstonLoggerTransportsKey) transports: winston.transport[] - ) { - if(esTransport){ + public constructor(@Inject(WinstonLoggerTransportsKey) transports: winston.transport[]) { + if (esTransport) { transports.push(esTransport); } - - + // Create winston logger this.logger = winston.createLogger(this.getLoggerFormatOptions(transports)); - } - private getLoggerFormatOptions(transports: winston.transport[]) : winston.LoggerOptions { + private getLoggerFormatOptions(transports: winston.transport[]): winston.LoggerOptions { // Setting log levels for winston + // eslint-disable-next-line @typescript-eslint/no-explicit-any const levels: any = {}; let cont = 0; Object.values(LogLevel).forEach((level) => { @@ -55,8 +58,8 @@ export default class WinstonLogger implements Logger { return { level: LogLevel.Debug, levels, - // format: ecsFormat.ecsFormat({ convertReqRes: true }), - format: winston.format.combine( + // format: ecsFormat.ecsFormat({ convertReqRes: true }), + format: winston.format.combine( ecsFormat.ecsFormat({ convertReqRes: true }), // Add timestamp and format the date // winston.format.timestamp({ @@ -65,7 +68,8 @@ export default class WinstonLogger implements Logger { // Errors will be logged with stack trace winston.format.errors({ stack: true }), // Add custom Log fields to the log - winston.format((info, opts) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + winston.format((info, _opts) => { // Info contains an Error property if (info.error && info.error instanceof Error) { info.stack = info.error.stack; @@ -82,21 +86,16 @@ export default class WinstonLogger implements Logger { fillExcept: ['timestamp', 'level', 'message'] }), // Format the log as JSON - winston.format.json(), + winston.format.json() ), transports, exceptionHandlers: transports }; } - public log( - level: LogLevel, - message: string | Error, - data?: LogData, - profile?: string - ) : void { + public log(level: LogLevel, message: string | Error, data?: LogData, profile?: string): void { const logData = { - level: level, + level, message: message instanceof Error ? message.message : message, error: message instanceof Error ? message : undefined, ...data @@ -109,34 +108,31 @@ export default class WinstonLogger implements Logger { } } - - public debug(message: string, data?: LogData, profile?: string) : void { + public debug(message: string, data?: LogData, profile?: string): void { this.log(LogLevel.Debug, message, data, profile); } - public info(message: string, data?: LogData, profile?: string) : void { + public info(message: string, data?: LogData, profile?: string): void { this.log(LogLevel.Info, message, data, profile); } - public warn(message: string | Error, data?: LogData, profile?: string) : void { + public warn(message: string | Error, data?: LogData, profile?: string): void { this.log(LogLevel.Warn, message, data, profile); } - public error(message: string | Error, data?: LogData, profile?: string) : void { + public error(message: string | Error, data?: LogData, profile?: string): void { this.log(LogLevel.Error, message, data, profile); } - public fatal(message: string | Error, data?: LogData, profile?: string) : void { + public fatal(message: string | Error, data?: LogData, profile?: string): void { this.log(LogLevel.Fatal, message, data, profile); } - public emergency(message: string | Error, data?: LogData, profile?: string) : void { + public emergency(message: string | Error, data?: LogData, profile?: string): void { this.log(LogLevel.Emergency, message, data, profile); } - public startProfile(id: string) : void { + public startProfile(id: string): void { this.logger.profile(id); } } - - diff --git a/libs/prisma-service/prisma/data/credebl-master-table.json b/libs/prisma-service/prisma/data/credebl-master-table.json new file mode 100644 index 000000000..c4167aa18 --- /dev/null +++ b/libs/prisma-service/prisma/data/credebl-master-table.json @@ -0,0 +1,178 @@ +{ + "platformConfigData": { + "externalIp": "##Machine Ip Address/Domain for agent setup##", + "inboundEndpoint": "##Machine Ip Address/Domain for agent setup##", + "username": "credebl", + "sgApiKey": "###Sendgrid Key###", + "emailFrom": "##Senders Mail ID##", + "apiEndpoint": "## Platform API Ip Address##", + "tailsFileServer": "##Machine Ip Address for agent setup##" + }, + "platformAdminData": { + "firstName": "CREDEBL", + "lastName": "CREDEBL", + "email": "", + "username": "", + "password": "####Please provide encrypted password using crypto-js###", + "verificationCode": "", + "isEmailVerified": true, + "supabaseUserId": "96cef763-e106-46c1-ac78-fadf2803b11f" + }, + "platformAdminOrganizationData": { + "name": "Platform-admin", + "description": "Platform-admin", + "logoUrl": "", + "website": "", + "createdBy": "", + "lastChangedBy": "" + }, + "orgRoleData": [ + { + "name": "owner", + "description": "Organization Owner" + }, + { + "name": "admin", + "description": "Organization Admin" + }, + { + "name": "issuer", + "description": "Organization Credential Issuer" + }, + { + "name": "verifier", + "description": "Organization Credential Verifier" + }, + { + "name": "holder", + "description": "Receives credentials issued by organization" + }, + { + "name": "member", + "description": "Joins the organization as member" + }, + { + "name": "platform_admin", + "description": "To setup all the platform of the user" + } + ], + "agentTypeData": [ + { + "agent": "AFJ" + }, + { + "agent": "ACAPY" + } + ], + "orgAgentTypeData": [ + { + "agent": "DEDICATED" + }, + { + "agent": "SHARED" + } + ], + "ledgerData": [ + { + "name": "Bcovrin Testnet", + "networkType": "testnet", + "poolConfig": "https://raw.githubusercontent.com/bcgov/von-network/main/BCovrin/genesis_test", + "isActive": true, + "networkString": "testnet", + "nymTxnEndpoint": "http://test.bcovrin.vonx.io/register", + "indyNamespace": "bcovrin:testnet" + }, + { + "name": "Indicio Testnet", + "networkType": "testnet", + "poolConfig": "https://raw.githubusercontent.com/Indicio-tech/indicio-network/main/genesis_files/pool_transactions_testnet_genesis", + "isActive": true, + "networkString": "testnet", + "nymTxnEndpoint": "https://selfserve.indiciotech.io/nym", + "indyNamespace": "indicio:testnet" + }, + { + "name": "Indicio Demonet", + "networkType": "demonet", + "poolConfig": "https://raw.githubusercontent.com/Indicio-tech/indicio-network/main/genesis_files/pool_transactions_demonet_genesis", + "isActive": true, + "networkString": "demonet", + "nymTxnEndpoint": "https://selfserve.indiciotech.io/nym", + "indyNamespace": "indicio:demonet" + }, + { + "name": "Indicio Mainnet", + "networkType": "mainnet", + "poolConfig": "https://raw.githubusercontent.com/Indicio-tech/indicio-network/main/genesis_files/pool_transactions_mainnet_genesis", + "isActive": true, + "networkString": "mainnet", + "nymTxnEndpoint": "https://selfserve.indiciotech.io/nym", + "indyNamespace": "indicio:mainnet" + }, + { + "name": "Polygon Testnet", + "networkType": "testnet", + "poolConfig": "", + "isActive": true, + "networkString": "testnet", + "nymTxnEndpoint": "", + "indyNamespace": "polygon:testnet" + }, + { + "name": "Polygon Mainnet", + "networkType": "mainnet", + "poolConfig": "", + "isActive": true, + "networkString": "mainnet", + "nymTxnEndpoint": "", + "indyNamespace": "polygon:mainnet" + }, + { + "name": "NA", + "networkType": "NA", + "poolConfig": "NA", + "isActive": true, + "networkString": "NA", + "nymTxnEndpoint": "NA", + "indyNamespace": "no_ledger" + } + ], + "ledgerConfig": [ + { + "name": "indy", + "details": { + "did:indy": { + "bcovrin:testnet":"did:indy:bcovrin:testnet", + "indicio:demonet":"did:indy:indicio:demonet", + "indicio:mainnet":"did:indy:indicio:mainnet", + "indicio:testnet":"did:indy:indicio:testnet" + } + } + }, + { + "name": "polygon", + "details": { + "did:polygon": { + "mainnet":"did:polygon:mainnet", + "testnet":"did:polygon:testnet" + } + } + }, + { + "name": "noLedger", + "details": { + "did:key": "did:key", + "did:web": "did:web" + } + } + ], + + "userRoleData": [ + { + "role": "HOLDER" + }, + { + "role": "DEFAULT_USER" + } + ] +} \ No newline at end of file diff --git a/libs/prisma-service/prisma/migrations/20250822104325_oidc_issuer_templates/migration.sql b/libs/prisma-service/prisma/migrations/20250822104325_oidc_issuer_templates/migration.sql new file mode 100644 index 000000000..4118c6b20 --- /dev/null +++ b/libs/prisma-service/prisma/migrations/20250822104325_oidc_issuer_templates/migration.sql @@ -0,0 +1,17 @@ +-- Drop the table if it exists (safe for dev) +DROP TABLE IF EXISTS "public"."credential_templates"; + +-- Create table +CREATE TABLE "public"."credential_templates" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT, + "format" TEXT NOT NULL, + "issuer" TEXT NOT NULL, + "canBeRevoked" BOOLEAN NOT NULL DEFAULT false, + "attributes" JSON NOT NULL, + "appearance" JSON NOT NULL, + "createdAt" TIMESTAMPTZ(6) NOT NULL DEFAULT now(), + "updatedAt" TIMESTAMPTZ(6) NOT NULL DEFAULT now(), + CONSTRAINT "credential_templates_pkey" PRIMARY KEY ("id") +); diff --git a/libs/prisma-service/prisma/migrations/20250822145801_fix_issuer_relation/migration.sql b/libs/prisma-service/prisma/migrations/20250822145801_fix_issuer_relation/migration.sql new file mode 100644 index 000000000..643f2dbf2 --- /dev/null +++ b/libs/prisma-service/prisma/migrations/20250822145801_fix_issuer_relation/migration.sql @@ -0,0 +1,32 @@ +/* + Warnings: + + - You are about to drop the column `issuer` on the `credential_templates` table. All the data in the column will be lost. + - Added the required column `issuerId` to the `credential_templates` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "credential_templates" DROP COLUMN "issuer", +ADD COLUMN "issuerId" UUID NOT NULL, +ALTER COLUMN "attributes" SET DATA TYPE JSONB, +ALTER COLUMN "appearance" SET DATA TYPE JSONB, +ALTER COLUMN "createdAt" SET DATA TYPE TIMESTAMP(3), +ALTER COLUMN "updatedAt" DROP DEFAULT, +ALTER COLUMN "updatedAt" SET DATA TYPE TIMESTAMP(3); + +-- CreateTable +CREATE TABLE "oidc_issuer" ( + "id" UUID NOT NULL, + "createDateTime" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "createdBy" UUID NOT NULL, + "agentIssuerId" TEXT NOT NULL, + "metadata" JSONB NOT NULL, + + CONSTRAINT "oidc_issuer_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "oidc_issuer" ADD CONSTRAINT "oidc_issuer_createdBy_fkey" FOREIGN KEY ("createdBy") REFERENCES "org_agents"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "credential_templates" ADD CONSTRAINT "credential_templates_issuerId_fkey" FOREIGN KEY ("issuerId") REFERENCES "oidc_issuer"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/libs/prisma-service/prisma/migrations/20250902082321_add_org_agent_id_to_oidc_issuer/migration.sql b/libs/prisma-service/prisma/migrations/20250902082321_add_org_agent_id_to_oidc_issuer/migration.sql new file mode 100644 index 000000000..b39a42c37 --- /dev/null +++ b/libs/prisma-service/prisma/migrations/20250902082321_add_org_agent_id_to_oidc_issuer/migration.sql @@ -0,0 +1,21 @@ +/* + Warnings: + + - You are about to drop the column `agentIssuerId` on the `oidc_issuer` table. All the data in the column will be lost. + - Added the required column `orgAgentId` to the `oidc_issuer` table without a default value. This is not possible if the table is not empty. + - Added the required column `publicIssuerId` to the `oidc_issuer` table without a default value. This is not possible if the table is not empty. + +*/ +-- DropForeignKey +ALTER TABLE "oidc_issuer" DROP CONSTRAINT "oidc_issuer_createdBy_fkey"; + +-- AlterTable +ALTER TABLE "oidc_issuer" DROP COLUMN "agentIssuerId", +ADD COLUMN "orgAgentId" UUID NOT NULL, +ADD COLUMN "publicIssuerId" TEXT NOT NULL; + +-- CreateIndex +CREATE INDEX "oidc_issuer_orgAgentId_idx" ON "oidc_issuer"("orgAgentId"); + +-- AddForeignKey +ALTER TABLE "oidc_issuer" ADD CONSTRAINT "oidc_issuer_orgAgentId_fkey" FOREIGN KEY ("orgAgentId") REFERENCES "org_agents"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/libs/prisma-service/prisma/migrations/20250904181430_added_fields_in_oidc_issuer/migration.sql b/libs/prisma-service/prisma/migrations/20250904181430_added_fields_in_oidc_issuer/migration.sql new file mode 100644 index 000000000..f8517c3c7 --- /dev/null +++ b/libs/prisma-service/prisma/migrations/20250904181430_added_fields_in_oidc_issuer/migration.sql @@ -0,0 +1,6 @@ +-- CreateEnum +CREATE TYPE "AccessTokenSignerKeyType" AS ENUM ('ed25519'); + +-- AlterTable +ALTER TABLE "oidc_issuer" ADD COLUMN "accessTokenSignerKeyType" "AccessTokenSignerKeyType"[] DEFAULT ARRAY['ed25519']::"AccessTokenSignerKeyType"[], +ADD COLUMN "batchCredentialIssuanceSize" INTEGER NOT NULL DEFAULT 0; diff --git a/libs/prisma-service/prisma/migrations/20250911102632_added_field_signer_option_in_credential_templates/migration.sql b/libs/prisma-service/prisma/migrations/20250911102632_added_field_signer_option_in_credential_templates/migration.sql new file mode 100644 index 000000000..58d8d3ebf --- /dev/null +++ b/libs/prisma-service/prisma/migrations/20250911102632_added_field_signer_option_in_credential_templates/migration.sql @@ -0,0 +1,18 @@ +/* + Warnings: + + - You are about to drop the column `accessTokenSignerKeyType` on the `oidc_issuer` table. All the data in the column will be lost. + - Added the required column `signerOption` to the `credential_templates` table without a default value. This is not possible if the table is not empty. + +*/ +-- CreateEnum +CREATE TYPE "SignerOption" AS ENUM ('did', 'x509'); + +-- AlterTable +ALTER TABLE "credential_templates" ADD COLUMN "signerOption" "SignerOption" NOT NULL; + +-- AlterTable +ALTER TABLE "oidc_issuer" DROP COLUMN "accessTokenSignerKeyType"; + +-- DropEnum +DROP TYPE "AccessTokenSignerKeyType"; diff --git a/libs/prisma-service/prisma/migrations/20251013125236_added_x509_certificate_table/migration.sql b/libs/prisma-service/prisma/migrations/20251013125236_added_x509_certificate_table/migration.sql new file mode 100644 index 000000000..74cd47e8e --- /dev/null +++ b/libs/prisma-service/prisma/migrations/20251013125236_added_x509_certificate_table/migration.sql @@ -0,0 +1,43 @@ + +-- CreateTable +CREATE TABLE "oid4vc_credentials" ( + "id" UUID NOT NULL, + "orgId" UUID NOT NULL, + "offerId" TEXT NOT NULL, + "credentialOfferId" TEXT NOT NULL, + "state" TEXT NOT NULL, + "contextCorrelationId" TEXT NOT NULL, + "createdBy" UUID NOT NULL, + "createDateTime" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "lastChangedDateTime" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "lastChangedBy" UUID NOT NULL, + + CONSTRAINT "oid4vc_credentials_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "x509_certificates" ( + "id" TEXT NOT NULL, + "orgAgentId" UUID NOT NULL, + "keyType" TEXT NOT NULL, + "status" TEXT NOT NULL, + "validFrom" TIMESTAMP(3) NOT NULL, + "expiry" TIMESTAMP(3) NOT NULL, + "certificateBase64" TEXT NOT NULL, + "isImported" BOOLEAN NOT NULL DEFAULT false, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "createdBy" UUID NOT NULL, + "lastChangedDateTime" TIMESTAMP(3) NOT NULL, + "lastChangedBy" UUID NOT NULL, + + CONSTRAINT "x509_certificates_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "oid4vc_credentials_offerId_key" ON "oid4vc_credentials"("offerId"); + +-- AddForeignKey +ALTER TABLE "oid4vc_credentials" ADD CONSTRAINT "oid4vc_credentials_orgId_fkey" FOREIGN KEY ("orgId") REFERENCES "organisation"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "x509_certificates" ADD CONSTRAINT "x509_certificates_orgAgentId_fkey" FOREIGN KEY ("orgAgentId") REFERENCES "org_agents"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/libs/prisma-service/prisma/migrations/20251016105943_added_new_oid4vp_verifier_table/migration.sql b/libs/prisma-service/prisma/migrations/20251016105943_added_new_oid4vp_verifier_table/migration.sql new file mode 100644 index 000000000..4a31a1b31 --- /dev/null +++ b/libs/prisma-service/prisma/migrations/20251016105943_added_new_oid4vp_verifier_table/migration.sql @@ -0,0 +1,24 @@ +-- CreateTable +CREATE TABLE "oid4vp_verifier" ( + "id" UUID NOT NULL, + "publicVerifierId" TEXT NOT NULL, + "metadata" JSONB NOT NULL, + "verifierId" TEXT NOT NULL, + "createDateTime" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "createdBy" TEXT NOT NULL DEFAULT '1', + "lastChangedDateTime" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "lastChangedBy" TEXT NOT NULL DEFAULT '1', + "deletedAt" TIMESTAMP(6), + "orgAgentId" UUID NOT NULL, + + CONSTRAINT "oid4vp_verifier_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "oid4vp_verifier_verifierId_key" ON "oid4vp_verifier"("verifierId"); + +-- CreateIndex +CREATE INDEX "oid4vp_verifier_orgAgentId_idx" ON "oid4vp_verifier"("orgAgentId"); + +-- AddForeignKey +ALTER TABLE "oid4vp_verifier" ADD CONSTRAINT "oid4vp_verifier_orgAgentId_fkey" FOREIGN KEY ("orgAgentId") REFERENCES "org_agents"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/libs/prisma-service/prisma/migrations/20251017081348_changed_signer_options_values/migration.sql b/libs/prisma-service/prisma/migrations/20251017081348_changed_signer_options_values/migration.sql new file mode 100644 index 000000000..3026fd028 --- /dev/null +++ b/libs/prisma-service/prisma/migrations/20251017081348_changed_signer_options_values/migration.sql @@ -0,0 +1,14 @@ +/* + Warnings: + + - The values [did,x509] on the enum `SignerOption` will be removed. If these variants are still used in the database, this will fail. + +*/ +-- AlterEnum +BEGIN; +CREATE TYPE "SignerOption_new" AS ENUM ('DID', 'X509_P256', 'X509_ED25519'); +ALTER TABLE "credential_templates" ALTER COLUMN "signerOption" TYPE "SignerOption_new" USING ("signerOption"::text::"SignerOption_new"); +ALTER TYPE "SignerOption" RENAME TO "SignerOption_old"; +ALTER TYPE "SignerOption_new" RENAME TO "SignerOption"; +DROP TYPE "SignerOption_old"; +COMMIT; diff --git a/libs/prisma-service/prisma/migrations/20251023120134_added_authorization_server_url/migration.sql b/libs/prisma-service/prisma/migrations/20251023120134_added_authorization_server_url/migration.sql new file mode 100644 index 000000000..8e6325394 --- /dev/null +++ b/libs/prisma-service/prisma/migrations/20251023120134_added_authorization_server_url/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - Added the required column `authorizationServerUrl` to the `oidc_issuer` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "oidc_issuer" ADD COLUMN "authorizationServerUrl" TEXT NOT NULL; diff --git a/libs/prisma-service/prisma/migrations/20251029164125_updated_table_oid4vc_credentials/migration.sql b/libs/prisma-service/prisma/migrations/20251029164125_updated_table_oid4vc_credentials/migration.sql new file mode 100644 index 000000000..c87bd9258 --- /dev/null +++ b/libs/prisma-service/prisma/migrations/20251029164125_updated_table_oid4vc_credentials/migration.sql @@ -0,0 +1,22 @@ +/* + Warnings: + + - You are about to drop the column `offerId` on the `oid4vc_credentials` table. All the data in the column will be lost. + - A unique constraint covering the columns `[issuanceSessionId]` on the table `oid4vc_credentials` will be added. If there are existing duplicate values, this will fail. + - Added the required column `issuanceSessionId` to the `oid4vc_credentials` table without a default value. This is not possible if the table is not empty. + +*/ +-- DropIndex +DROP INDEX "oid4vc_credentials_offerId_key"; + +-- AlterTable +ALTER TABLE "oid4vc_credentials" DROP COLUMN "offerId", +ADD COLUMN "credentialConfigurationIds" TEXT[], +ADD COLUMN "issuanceSessionId" TEXT NOT NULL, +ADD COLUMN "issuedCredentials" TEXT[]; + +-- CreateIndex +CREATE UNIQUE INDEX "oid4vc_credentials_issuanceSessionId_key" ON "oid4vc_credentials"("issuanceSessionId"); + +-- CreateIndex +CREATE INDEX "oid4vc_credentials_credentialConfigurationIds_idx" ON "oid4vc_credentials" USING GIN ("credentialConfigurationIds"); diff --git a/libs/prisma-service/prisma/migrations/20251103071640_updated_table_oid4vp_verifier_to_remove_unnecessary_column_deleted_at/migration.sql b/libs/prisma-service/prisma/migrations/20251103071640_updated_table_oid4vp_verifier_to_remove_unnecessary_column_deleted_at/migration.sql new file mode 100644 index 000000000..3008e2110 --- /dev/null +++ b/libs/prisma-service/prisma/migrations/20251103071640_updated_table_oid4vp_verifier_to_remove_unnecessary_column_deleted_at/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - You are about to drop the column `deletedAt` on the `oid4vp_verifier` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "oid4vp_verifier" DROP COLUMN "deletedAt"; diff --git a/libs/prisma-service/prisma/migrations/20251105180717_created_table_oid4vp_presentation/migration.sql b/libs/prisma-service/prisma/migrations/20251105180717_created_table_oid4vp_presentation/migration.sql new file mode 100644 index 000000000..1ba8f23c5 --- /dev/null +++ b/libs/prisma-service/prisma/migrations/20251105180717_created_table_oid4vp_presentation/migration.sql @@ -0,0 +1,21 @@ +-- CreateTable +CREATE TABLE "oid4vp_presentations" ( + "id" UUID NOT NULL, + "orgId" UUID NOT NULL, + "verificationSessionId" TEXT NOT NULL, + "presentationId" TEXT NOT NULL, + "state" TEXT NOT NULL, + "contextCorrelationId" TEXT NOT NULL, + "createdBy" UUID NOT NULL, + "createDateTime" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "lastChangedDateTime" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "lastChangedBy" UUID NOT NULL, + + CONSTRAINT "oid4vp_presentations_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "oid4vp_presentations_verificationSessionId_key" ON "oid4vp_presentations"("verificationSessionId"); + +-- AddForeignKey +ALTER TABLE "oid4vp_presentations" ADD CONSTRAINT "oid4vp_presentations_orgId_fkey" FOREIGN KEY ("orgId") REFERENCES "organisation"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/libs/prisma-service/prisma/migrations/20251108070604_added_public_issuer_id_and_public_verifier_id_in_wehook_data_tables/migration.sql b/libs/prisma-service/prisma/migrations/20251108070604_added_public_issuer_id_and_public_verifier_id_in_wehook_data_tables/migration.sql new file mode 100644 index 000000000..684e2a590 --- /dev/null +++ b/libs/prisma-service/prisma/migrations/20251108070604_added_public_issuer_id_and_public_verifier_id_in_wehook_data_tables/migration.sql @@ -0,0 +1,25 @@ +/* + Warnings: + + - Added the required column `publicIssuerId` to the `oid4vc_credentials` table without a default value. This is not possible if the table is not empty. + - Added the required column `publicVerifierId` to the `oid4vp_presentations` table without a default value. This is not possible if the table is not empty. + +*/ + +-- AlterTable +-- Add the column first +ALTER TABLE "oid4vc_credentials" +ADD COLUMN "publicIssuerId" TEXT; + +-- Optional: add a temporary default or backfill +UPDATE "oid4vc_credentials" SET "publicIssuerId" = 'default-issuer'; + +-- Make it required if needed +ALTER TABLE "oid4vc_credentials" +ALTER COLUMN "publicIssuerId" SET NOT NULL; + + +-- AlterTable +ALTER TABLE "oid4vp_presentations" ADD COLUMN "publicVerifierId" TEXT; +UPDATE "oid4vp_presentations" SET "publicVerifierId" = 'default-verifier-id'; +ALTER TABLE "oid4vp_presentations" ALTER COLUMN "publicVerifierId" SET NOT NULL; diff --git a/libs/prisma-service/prisma/schema.prisma b/libs/prisma-service/prisma/schema.prisma index dfba4e1f4..ac80fd785 100644 --- a/libs/prisma-service/prisma/schema.prisma +++ b/libs/prisma-service/prisma/schema.prisma @@ -39,35 +39,35 @@ model user { } model account { - id String @id @default(uuid()) @db.Uuid - userId String @unique @db.Uuid + id String @id @default(uuid()) @db.Uuid + userId String @unique @db.Uuid type String? provider String - providerAccountId String + providerAccountId String tokenType String? scope String? idToken String? sessionState String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - user user @relation(fields: [userId], references: [id]) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + user user @relation(fields: [userId], references: [id]) sessions session[] - } +} model session { - id String @id @default(uuid()) @db.Uuid - sessionToken String - userId String @db.Uuid - expires Int - refreshToken String? - user user @relation(fields: [userId], references: [id]) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - accountId String? @db.Uuid - sessionType String? - account account? @relation(fields: [accountId], references:[id]) - expiresAt DateTime? @db.Timestamp(6) - clientInfo Json? + id String @id @default(uuid()) @db.Uuid + sessionToken String + userId String @db.Uuid + expires Int + refreshToken String? + user user @relation(fields: [userId], references: [id]) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + accountId String? @db.Uuid + sessionType String? + account account? @relation(fields: [accountId], references: [id]) + expiresAt DateTime? @db.Timestamp(6) + clientInfo Json? } model token { @@ -151,6 +151,8 @@ model organisation { agent_invitations agent_invitations[] credential_definition credential_definition[] file_upload file_upload[] + oid4vc_credentials oid4vc_credentials[] + oid4vp_presentations oid4vp_presentations[] } model org_invitations { @@ -229,6 +231,9 @@ model org_agents { organisation organisation? @relation(fields: [orgId], references: [id]) webhookUrl String? @db.VarChar org_dids org_dids[] + oidc_issuer oidc_issuer[] + x509_certificates x509_certificates[] + oid4vp_verifiers oid4vp_verifier[] } model org_dids { @@ -304,7 +309,7 @@ model schema { type String? @db.VarChar isSchemaArchived Boolean @default(false) credential_definition credential_definition[] - alias String? + alias String? } model credential_definition { @@ -350,16 +355,16 @@ model agent_invitations { } model connections { - id String @id @default(uuid()) @db.Uuid - createDateTime DateTime @default(now()) @db.Timestamptz(6) - createdBy String @db.Uuid - lastChangedDateTime DateTime @default(now()) @db.Timestamptz(6) - lastChangedBy String @db.Uuid - connectionId String @unique - theirLabel String @default("") + id String @id @default(uuid()) @db.Uuid + createDateTime DateTime @default(now()) @db.Timestamptz(6) + createdBy String @db.Uuid + lastChangedDateTime DateTime @default(now()) @db.Timestamptz(6) + lastChangedBy String @db.Uuid + connectionId String @unique + theirLabel String @default("") state String - orgId String? @db.Uuid - organisation organisation? @relation(fields: [orgId], references: [id]) + orgId String? @db.Uuid + organisation organisation? @relation(fields: [orgId], references: [id]) presentations presentations[] credentials credentials[] } @@ -387,7 +392,7 @@ model presentations { createdBy String @db.Uuid lastChangedDateTime DateTime @default(now()) @db.Timestamptz(6) lastChangedBy String @db.Uuid - connectionId String? + connectionId String? state String? threadId String @unique isVerified Boolean? @@ -398,7 +403,6 @@ model presentations { orgId String? @db.Uuid organisation organisation? @relation(fields: [orgId], references: [id]) connections connections? @relation(fields: [connectionId], references: [connectionId]) - } model file_upload { @@ -415,7 +419,7 @@ model file_upload { organisation organisation? @relation(fields: [orgId], references: [id]) orgId String? @db.Uuid credential_type String? - templateId String? @db.VarChar + templateId String? @db.VarChar } model file_data { @@ -548,12 +552,12 @@ model cloud_wallet_user_info { createdBy String @db.Uuid lastChangedDateTime DateTime @default(now()) @db.Timestamptz(6) lastChangedBy String @db.Uuid - userId String? @db.Uuid + userId String? @db.Uuid agentEndpoint String? agentApiKey String? key String? connectionImageUrl String? - user user? @relation(fields: [userId], references: [id]) + user user? @relation(fields: [userId], references: [id]) } enum CloudWalletType { @@ -562,9 +566,118 @@ enum CloudWalletType { } model client_aliases { - id String @id @default(uuid()) @db.Uuid - createDateTime DateTime @default(now()) @db.Timestamptz(6) - lastChangedDateTime DateTime @default(now()) @db.Timestamptz(6) - clientAlias String? - clientUrl String + id String @id @default(uuid()) @db.Uuid + createDateTime DateTime @default(now()) @db.Timestamptz(6) + lastChangedDateTime DateTime @default(now()) @db.Timestamptz(6) + clientAlias String? + clientUrl String +} + +model oidc_issuer { + id String @id @default(uuid()) @db.Uuid + createDateTime DateTime @default(now()) @db.Timestamptz(6) + createdBy String @db.Uuid + publicIssuerId String + metadata Json + authorizationServerUrl String + orgAgentId String @db.Uuid + orgAgent org_agents @relation(fields: [orgAgentId], references: [id]) + templates credential_templates[] + batchCredentialIssuanceSize Int @default(0) + + @@index([orgAgentId]) +} + +model oid4vc_credentials { + id String @id @default(uuid()) @db.Uuid + orgId String @db.Uuid + issuanceSessionId String @unique + credentialOfferId String + state String + contextCorrelationId String + createdBy String @db.Uuid + createDateTime DateTime @default(now()) @db.Timestamptz(6) + lastChangedDateTime DateTime @default(now()) @db.Timestamptz(6) + lastChangedBy String @db.Uuid + credentialConfigurationIds String[] + issuedCredentials String[] + publicIssuerId String + + organisation organisation @relation(fields: [orgId], references: [id]) + + @@index([credentialConfigurationIds], type: Gin) +} + +model oid4vp_presentations { + id String @id @default(uuid()) @db.Uuid + orgId String @db.Uuid + verificationSessionId String @unique + presentationId String + state String + contextCorrelationId String + createdBy String @db.Uuid + createDateTime DateTime @default(now()) @db.Timestamptz(6) + lastChangedDateTime DateTime @default(now()) @db.Timestamptz(6) + lastChangedBy String @db.Uuid + publicVerifierId String + organisation organisation @relation(fields: [orgId], references: [id]) +} + +enum SignerOption { + DID + X509_P256 + X509_ED25519 +} + +model credential_templates { + id String @id @default(uuid()) + name String + description String? + format String // e.g. "sd_jwt", "mso_mdoc" + canBeRevoked Boolean @default(false) + + attributes Json + appearance Json + + issuerId String @db.Uuid + issuer oidc_issuer @relation(fields: [issuerId], references: [id]) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + signerOption SignerOption +} + +model x509_certificates { + id String @id @default(uuid()) + + orgAgentId String @db.Uuid + org_agents org_agents @relation(fields: [orgAgentId], references: [id]) + + keyType String // "p256", "ed25519" + status String //e.g "Active", "Pending activation", "InActive" + validFrom DateTime + + expiry DateTime + certificateBase64 String + isImported Boolean @default(false) + + createdAt DateTime @default(now()) + createdBy String @db.Uuid + lastChangedDateTime DateTime @updatedAt + lastChangedBy String @db.Uuid +} + +model oid4vp_verifier { + id String @id @default(uuid()) @db.Uuid + publicVerifierId String + metadata Json + verifierId String @unique + createDateTime DateTime @default(now()) @db.Timestamptz(6) + createdBy String @default("1") + lastChangedDateTime DateTime @default(now()) @db.Timestamptz(6) + lastChangedBy String @default("1") + orgAgentId String @db.Uuid + orgAgent org_agents @relation(fields: [orgAgentId], references: [id]) + + @@index([orgAgentId]) } diff --git a/nest-cli.json b/nest-cli.json index ee0cc7122..663799cc6 100644 --- a/nest-cli.json +++ b/nest-cli.json @@ -8,40 +8,40 @@ "tsConfigPath": "apps/api-gateway/tsconfig.app.json" }, "projects": { - "api-gateway": { + "agent-provisioning": { "type": "application", - "root": "apps/api-gateway", + "root": "apps/agent-provisioning", "entryFile": "main", - "sourceRoot": "apps/api-gateway/src", + "sourceRoot": "apps/agent-provisioning/src", "compilerOptions": { - "tsConfigPath": "apps/api-gateway/tsconfig.app.json" + "tsConfigPath": "apps/agent-provisioning/tsconfig.app.json" } }, - "platform-service": { + "agent-service": { "type": "application", - "root": "apps/platform-service", + "root": "apps/agent-service", "entryFile": "main", - "sourceRoot": "apps/platform-service/src", + "sourceRoot": "apps/agent-service/src", "compilerOptions": { - "tsConfigPath": "apps/platform-service/tsconfig.app.json" + "tsConfigPath": "apps/agent-service/tsconfig.app.json" } }, - "common": { - "type": "library", - "root": "libs/common", - "entryFile": "index", - "sourceRoot": "libs/common/src", + "api-gateway": { + "type": "application", + "root": "apps/api-gateway", + "entryFile": "main", + "sourceRoot": "apps/api-gateway/src", "compilerOptions": { - "tsConfigPath": "libs/common/tsconfig.lib.json" + "tsConfigPath": "apps/api-gateway/tsconfig.app.json" } }, - "keycloak-url": { + "aws": { "type": "library", - "root": "libs/keycloak-url", + "root": "libs/aws", "entryFile": "index", - "sourceRoot": "libs/keycloak-url/src", + "sourceRoot": "libs/aws/src", "compilerOptions": { - "tsConfigPath": "libs/keycloak-url/tsconfig.lib.json" + "tsConfigPath": "libs/aws/tsconfig.lib.json" } }, "client-registration": { @@ -53,40 +53,49 @@ "tsConfigPath": "libs/client-registration/tsconfig.lib.json" } }, - "connection": { + "cloud-wallet": { "type": "application", - "root": "apps/connection", + "root": "apps/cloud-wallet", "entryFile": "main", - "sourceRoot": "apps/connection/src", + "sourceRoot": "apps/cloud-wallet/src", "compilerOptions": { - "tsConfigPath": "apps/connection/tsconfig.app.json" + "tsConfigPath": "apps/cloud-wallet/tsconfig.app.json" } }, - "prisma": { + "common": { "type": "library", - "root": "libs/prisma", + "root": "libs/common", "entryFile": "index", - "sourceRoot": "libs/prisma/src", + "sourceRoot": "libs/common/src", "compilerOptions": { - "tsConfigPath": "libs/prisma/tsconfig.lib.json" + "tsConfigPath": "libs/common/tsconfig.lib.json" } }, - "repositories": { + "config": { "type": "library", - "root": "libs/repositories", + "root": "libs/config", "entryFile": "index", - "sourceRoot": "libs/repositories/src", + "sourceRoot": "libs/config/src", "compilerOptions": { - "tsConfigPath": "libs/repositories/tsconfig.lib.json" + "tsConfigPath": "libs/config/tsconfig.lib.json" } }, - "user-request": { + "connection": { + "type": "application", + "root": "apps/connection", + "entryFile": "main", + "sourceRoot": "apps/connection/src", + "compilerOptions": { + "tsConfigPath": "apps/connection/tsconfig.app.json" + } + }, + "context": { "type": "library", - "root": "libs/user-request", + "root": "libs/context", "entryFile": "index", - "sourceRoot": "libs/user-request/src", + "sourceRoot": "libs/context/src", "compilerOptions": { - "tsConfigPath": "libs/user-request/tsconfig.lib.json" + "tsConfigPath": "libs/context/tsconfig.lib.json" } }, "enum": { @@ -98,103 +107,130 @@ "tsConfigPath": "libs/enum/tsconfig.lib.json" } }, - "prisma-service": { - "type": "library", - "root": "libs/prisma-service", - "entryFile": "index", - "sourceRoot": "libs/prisma-service/src", - "compilerOptions": { - "tsConfigPath": "libs/prisma-service/tsconfig.lib.json" - } - }, - "organization": { + "geo-location": { "type": "application", - "root": "apps/organization", + "root": "apps/geo-location", "entryFile": "main", - "sourceRoot": "apps/organization/src", + "sourceRoot": "apps/geo-location/src", "compilerOptions": { - "tsConfigPath": "apps/organization/tsconfig.app.json" + "tsConfigPath": "apps/geo-location/tsconfig.app.json" } }, - "user": { + "issuance": { "type": "application", - "root": "apps/user", + "root": "apps/issuance", "entryFile": "main", - "sourceRoot": "apps/user/src", + "sourceRoot": "apps/issuance/src", "compilerOptions": { - "tsConfigPath": "apps/user/tsconfig.app.json" + "tsConfigPath": "apps/issuance/tsconfig.app.json" } }, - "org-roles": { + "keycloak-url": { "type": "library", - "root": "libs/org-roles", + "root": "libs/keycloak-url", "entryFile": "index", - "sourceRoot": "libs/org-roles/src", + "sourceRoot": "libs/keycloak-url/src", "compilerOptions": { - "tsConfigPath": "libs/org-roles/tsconfig.lib.json" + "tsConfigPath": "libs/keycloak-url/tsconfig.lib.json" } }, - "user-org-roles": { + "ledger": { + "type": "application", + "root": "apps/ledger", + "entryFile": "main", + "sourceRoot": "apps/ledger/src", + "compilerOptions": { + "tsConfigPath": "apps/ledger/tsconfig.app.json" + } + }, + "logger": { "type": "library", - "root": "libs/user-org-roles", + "root": "libs/logger", "entryFile": "index", - "sourceRoot": "libs/user-org-roles/src", + "sourceRoot": "libs/logger/src", "compilerOptions": { - "tsConfigPath": "libs/user-org-roles/tsconfig.lib.json" + "tsConfigPath": "libs/logger/tsconfig.lib.json" } }, - "ledger": { + "notification": { "type": "application", - "root": "apps/ledger", + "root": "apps/notification", "entryFile": "main", - "sourceRoot": "apps/ledger/src", + "sourceRoot": "apps/notification/src", "compilerOptions": { - "tsConfigPath": "apps/ledger/tsconfig.app.json" + "tsConfigPath": "apps/notification/tsconfig.app.json" } }, - "agent-service": { + "oid4vc-issuance": { "type": "application", - "root": "apps/agent-service", + "root": "apps/oid4vc-issuance", "entryFile": "main", - "sourceRoot": "apps/agent-service/src", + "sourceRoot": "apps/oid4vc-issuance/src", "compilerOptions": { - "tsConfigPath": "apps/agent-service/tsconfig.app.json" + "tsConfigPath": "apps/oid4vc-issuance/tsconfig.app.json" } }, - "agent-provisioning": { + "oid4vc-verification": { "type": "application", - "root": "apps/agent-provisioning", + "root": "apps/oid4vc-verification", "entryFile": "main", - "sourceRoot": "apps/agent-provisioning/src", + "sourceRoot": "apps/oid4vc-verification/src", "compilerOptions": { - "tsConfigPath": "apps/agent-provisioning/tsconfig.app.json" + "tsConfigPath": "apps/oid4vc-verification/tsconfig.app.json" } }, - "issuance": { + "org-roles": { + "type": "library", + "root": "libs/org-roles", + "entryFile": "index", + "sourceRoot": "libs/org-roles/src", + "compilerOptions": { + "tsConfigPath": "libs/org-roles/tsconfig.lib.json" + } + }, + "organization": { "type": "application", - "root": "apps/issuance", + "root": "apps/organization", "entryFile": "main", - "sourceRoot": "apps/issuance/src", + "sourceRoot": "apps/organization/src", "compilerOptions": { - "tsConfigPath": "apps/issuance/tsconfig.app.json" + "tsConfigPath": "apps/organization/tsconfig.app.json" } }, - "verification": { + "platform-service": { "type": "application", - "root": "apps/verification", + "root": "apps/platform-service", "entryFile": "main", - "sourceRoot": "apps/verification/src", + "sourceRoot": "apps/platform-service/src", "compilerOptions": { - "tsConfigPath": "apps/verification/tsconfig.app.json" + "tsConfigPath": "apps/platform-service/tsconfig.app.json" } }, - "user-activity": { + "prisma": { "type": "library", - "root": "libs/user-activity", + "root": "libs/prisma", "entryFile": "index", - "sourceRoot": "libs/user-activity/src", + "sourceRoot": "libs/prisma/src", "compilerOptions": { - "tsConfigPath": "libs/user-activity/tsconfig.lib.json" + "tsConfigPath": "libs/prisma/tsconfig.lib.json" + } + }, + "prisma-service": { + "type": "library", + "root": "libs/prisma-service", + "entryFile": "index", + "sourceRoot": "libs/prisma-service/src", + "compilerOptions": { + "tsConfigPath": "libs/prisma-service/tsconfig.lib.json" + } + }, + "repositories": { + "type": "library", + "root": "libs/repositories", + "entryFile": "index", + "sourceRoot": "libs/repositories/src", + "compilerOptions": { + "tsConfigPath": "libs/repositories/tsconfig.lib.json" } }, "supabase": { @@ -206,22 +242,40 @@ "tsConfigPath": "libs/supabase/tsconfig.lib.json" } }, - "webhook": { + "user": { "type": "application", - "root": "apps/webhook", + "root": "apps/user", "entryFile": "main", - "sourceRoot": "apps/webhook/src", + "sourceRoot": "apps/user/src", "compilerOptions": { - "tsConfigPath": "apps/webhook/tsconfig.app.json" + "tsConfigPath": "apps/user/tsconfig.app.json" } }, - "aws": { + "user-activity": { "type": "library", - "root": "libs/aws", + "root": "libs/user-activity", "entryFile": "index", - "sourceRoot": "libs/aws/src", + "sourceRoot": "libs/user-activity/src", "compilerOptions": { - "tsConfigPath": "libs/aws/tsconfig.lib.json" + "tsConfigPath": "libs/user-activity/tsconfig.lib.json" + } + }, + "user-org-roles": { + "type": "library", + "root": "libs/user-org-roles", + "entryFile": "index", + "sourceRoot": "libs/user-org-roles/src", + "compilerOptions": { + "tsConfigPath": "libs/user-org-roles/tsconfig.lib.json" + } + }, + "user-request": { + "type": "library", + "root": "libs/user-request", + "entryFile": "index", + "sourceRoot": "libs/user-request/src", + "compilerOptions": { + "tsConfigPath": "libs/user-request/tsconfig.lib.json" } }, "utility": { @@ -233,59 +287,32 @@ "tsConfigPath": "apps/utility/tsconfig.app.json" } }, - "notification": { + "verification": { "type": "application", - "root": "apps/notification", + "root": "apps/verification", "entryFile": "main", - "sourceRoot": "apps/notification/src", + "sourceRoot": "apps/verification/src", "compilerOptions": { - "tsConfigPath": "apps/notification/tsconfig.app.json" + "tsConfigPath": "apps/verification/tsconfig.app.json" } }, - "geo-location": { + "webhook": { "type": "application", - "root": "apps/geo-location", + "root": "apps/webhook", "entryFile": "main", - "sourceRoot": "apps/geo-location/src", + "sourceRoot": "apps/webhook/src", "compilerOptions": { - "tsConfigPath": "apps/geo-location/tsconfig.app.json" + "tsConfigPath": "apps/webhook/tsconfig.app.json" } }, - "cloud-wallet": { + "x509": { "type": "application", - "root": "apps/cloud-wallet", + "root": "apps/x509", "entryFile": "main", - "sourceRoot": "apps/cloud-wallet/src", - "compilerOptions": { - "tsConfigPath": "apps/cloud-wallet/tsconfig.app.json" - } - }, - "config": { - "type": "library", - "root": "libs/config", - "entryFile": "index", - "sourceRoot": "libs/config/src", - "compilerOptions": { - "tsConfigPath": "libs/config/tsconfig.lib.json" - } - }, - "context": { - "type": "library", - "root": "libs/context", - "entryFile": "index", - "sourceRoot": "libs/context/src", + "sourceRoot": "apps/x509/src", "compilerOptions": { - "tsConfigPath": "libs/context/tsconfig.lib.json" - } - }, - "logger": { - "type": "library", - "root": "libs/logger", - "entryFile": "index", - "sourceRoot": "libs/logger/src", - "compilerOptions": { - "tsConfigPath": "libs/logger/tsconfig.lib.json" + "tsConfigPath": "apps/x509/tsconfig.app.json" } } } -} +} \ No newline at end of file diff --git a/package.json b/package.json index 418a81be2..6a1e02570 100644 --- a/package.json +++ b/package.json @@ -209,4 +209,4 @@ "engines": { "node": ">=18" } -} +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8be044baf..2b3b5ecd2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -91,7 +91,7 @@ importers: version: 11.0.3(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6)(bull@4.16.5) '@nestjs/cache-manager': specifier: 'catalog:' - version: 3.0.1(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6)(cache-manager@5.7.6)(keyv@5.5.3)(rxjs@7.8.2) + version: 3.0.1(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6)(cache-manager@5.7.6)(keyv@4.5.4)(rxjs@7.8.2) '@nestjs/common': specifier: 'catalog:' version: 11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2) @@ -106,7 +106,7 @@ importers: version: 11.0.0(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2)) '@nestjs/microservices': specifier: 'catalog:' - version: 11.1.6(@grpc/grpc-js@1.14.0)(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6)(@nestjs/websockets@11.1.6)(cache-manager@5.7.6)(ioredis@5.8.1)(nats@2.29.3)(reflect-metadata@0.1.14)(rxjs@7.8.2) + version: 11.1.6(@grpc/grpc-js@1.14.0)(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6)(@nestjs/websockets@11.1.6)(cache-manager@5.7.6)(ioredis@5.8.0)(nats@2.29.3)(reflect-metadata@0.1.14)(rxjs@7.8.2) '@nestjs/passport': specifier: 'catalog:' version: 11.0.5(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(passport@0.6.0) @@ -124,7 +124,7 @@ importers: version: 11.2.0(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6)(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14) '@nestjs/typeorm': specifier: 'catalog:' - version: 11.0.0(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6)(reflect-metadata@0.1.14)(rxjs@7.8.2)(typeorm@0.3.27(ioredis@5.8.1)(pg@8.16.3)(redis@3.1.2)(reflect-metadata@0.1.14)(ts-node@10.9.2(@types/node@20.19.17)(typescript@5.9.2))) + version: 11.0.0(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6)(reflect-metadata@0.1.14)(rxjs@7.8.2)(typeorm@0.3.27(ioredis@5.8.0)(pg@8.16.3)(redis@3.1.2)(reflect-metadata@0.1.14)(ts-node@10.9.2(@types/node@20.19.17)(typescript@5.9.2))) '@nestjs/websockets': specifier: 'catalog:' version: 11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6)(@nestjs/platform-socket.io@11.1.6)(reflect-metadata@0.1.14)(rxjs@7.8.2) @@ -286,7 +286,7 @@ importers: version: 1.0.9 nestjs-typeorm-paginate: specifier: 'catalog:' - version: 4.1.0(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(typeorm@0.3.27(ioredis@5.8.1)(pg@8.16.3)(redis@3.1.2)(reflect-metadata@0.1.14)(ts-node@10.9.2(@types/node@20.19.17)(typescript@5.9.2))) + version: 4.1.0(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(typeorm@0.3.27(ioredis@5.8.0)(pg@8.16.3)(redis@3.1.2)(reflect-metadata@0.1.14)(ts-node@10.9.2(@types/node@20.19.17)(typescript@5.9.2))) node-html-to-image: specifier: ^4.0.0 version: 4.0.0 @@ -352,7 +352,7 @@ importers: version: 5.0.1(express@4.21.2) typeorm: specifier: ^0.3.10 - version: 0.3.27(ioredis@5.8.1)(pg@8.16.3)(redis@3.1.2)(reflect-metadata@0.1.14)(ts-node@10.9.2(@types/node@20.19.17)(typescript@5.9.2)) + version: 0.3.27(ioredis@5.8.0)(pg@8.16.3)(redis@3.1.2)(reflect-metadata@0.1.14)(ts-node@10.9.2(@types/node@20.19.17)(typescript@5.9.2)) unzipper: specifier: ^0.10.14 version: 0.10.14 @@ -476,7 +476,7 @@ importers: version: 11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2) '@nestjs/microservices': specifier: 'catalog:' - version: 11.1.6(@grpc/grpc-js@1.14.0)(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6)(@nestjs/websockets@11.1.6)(cache-manager@5.7.6)(ioredis@5.8.1)(nats@2.29.3)(reflect-metadata@0.1.14)(rxjs@7.8.2) + version: 11.1.6(@grpc/grpc-js@1.14.0)(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6)(@nestjs/websockets@11.1.6)(cache-manager@5.7.6)(ioredis@5.8.0)(nats@2.29.3)(reflect-metadata@0.1.14)(rxjs@7.8.2) '@nestjs/testing': specifier: 'catalog:' version: 11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6)(@nestjs/microservices@11.1.6)(@nestjs/platform-express@11.1.6) @@ -501,7 +501,7 @@ importers: version: 4.0.1(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(axios@0.26.1)(rxjs@7.8.2) '@nestjs/cache-manager': specifier: 'catalog:' - version: 3.0.1(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6)(cache-manager@5.7.6)(keyv@5.5.3)(rxjs@7.8.2) + version: 3.0.1(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6)(cache-manager@5.7.6)(keyv@4.5.4)(rxjs@7.8.2) '@nestjs/common': specifier: 'catalog:' version: 11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2) @@ -510,7 +510,7 @@ importers: version: 11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/microservices@11.1.6)(@nestjs/platform-express@11.1.6)(@nestjs/websockets@11.1.6)(reflect-metadata@0.1.14)(rxjs@7.8.2) '@nestjs/microservices': specifier: 'catalog:' - version: 11.1.6(@grpc/grpc-js@1.14.0)(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6)(@nestjs/websockets@11.1.6)(cache-manager@5.7.6)(ioredis@5.8.1)(nats@2.29.3)(reflect-metadata@0.1.14)(rxjs@7.8.2) + version: 11.1.6(@grpc/grpc-js@1.14.0)(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6)(@nestjs/websockets@11.1.6)(cache-manager@5.7.6)(ioredis@5.8.0)(nats@2.29.3)(reflect-metadata@0.1.14)(rxjs@7.8.2) '@nestjs/swagger': specifier: 'catalog:' version: 11.2.0(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6)(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14) @@ -1187,9 +1187,6 @@ packages: '@js-sdsl/ordered-map@4.4.2': resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} - '@keyv/serialize@1.1.1': - resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==} - '@lukeed/csprng@1.1.0': resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} engines: {node: '>=8'} @@ -4066,8 +4063,8 @@ packages: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} - ioredis@5.8.1: - resolution: {integrity: sha512-Qho8TgIamqEPdgiMadJwzRMW3TudIg6vpg4YONokGDudy4eqRIJtDbVX72pfLBcWxvbn3qm/40TyGUObdW4tLQ==} + ioredis@5.8.0: + resolution: {integrity: sha512-AUXbKn9gvo9hHKvk6LbZJQSKn/qIfkWXrnsyL9Yrf+oeXmla9Nmf6XEumOddyhM8neynpK5oAV6r9r99KBuwzA==} engines: {node: '>=12.22.0'} ip-address@10.0.1: @@ -4534,9 +4531,6 @@ packages: keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} - keyv@5.5.3: - resolution: {integrity: sha512-h0Un1ieD+HUrzBH6dJXhod3ifSghk5Hw/2Y4/KHBziPlZecrFyE9YOTPU6eOs0V9pYl8gOs86fkr/KN8lUX39A==} - klaw@1.3.1: resolution: {integrity: sha512-TED5xi9gGQjGpNnvRWknrwAB1eL5GciPfVFOt3Vk1OJCVDQbzuSfrF3hkUQKlsgKrG1F+0t5W0m+Fje1jIt8rw==} @@ -7744,8 +7738,6 @@ snapshots: '@js-sdsl/ordered-map@4.4.2': {} - '@keyv/serialize@1.1.1': {} - '@lukeed/csprng@1.1.0': {} '@mapbox/node-pre-gyp@1.0.11': @@ -7803,12 +7795,12 @@ snapshots: bull: 4.16.5 tslib: 2.8.1 - '@nestjs/cache-manager@3.0.1(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6)(cache-manager@5.7.6)(keyv@5.5.3)(rxjs@7.8.2)': + '@nestjs/cache-manager@3.0.1(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6)(cache-manager@5.7.6)(keyv@4.5.4)(rxjs@7.8.2)': dependencies: '@nestjs/common': 11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2) '@nestjs/core': 11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/microservices@11.1.6)(@nestjs/platform-express@11.1.6)(@nestjs/websockets@11.1.6)(reflect-metadata@0.1.14)(rxjs@7.8.2) cache-manager: 5.7.6 - keyv: 5.5.3 + keyv: 4.5.4 rxjs: 7.8.2 '@nestjs/cli@11.0.10(@types/node@20.19.17)': @@ -7873,7 +7865,7 @@ snapshots: tslib: 2.8.1 uid: 2.0.2 optionalDependencies: - '@nestjs/microservices': 11.1.6(@grpc/grpc-js@1.14.0)(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6)(@nestjs/websockets@11.1.6)(cache-manager@5.7.6)(ioredis@5.8.1)(nats@2.29.3)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/microservices': 11.1.6(@grpc/grpc-js@1.14.0)(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6)(@nestjs/websockets@11.1.6)(cache-manager@5.7.6)(ioredis@5.8.0)(nats@2.29.3)(reflect-metadata@0.1.14)(rxjs@7.8.2) '@nestjs/platform-express': 11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6) '@nestjs/websockets': 11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6)(@nestjs/platform-socket.io@11.1.6)(reflect-metadata@0.1.14)(rxjs@7.8.2) @@ -7891,7 +7883,7 @@ snapshots: class-transformer: 0.5.1 class-validator: 0.14.2 - '@nestjs/microservices@11.1.6(@grpc/grpc-js@1.14.0)(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6)(@nestjs/websockets@11.1.6)(cache-manager@5.7.6)(ioredis@5.8.1)(nats@2.29.3)(reflect-metadata@0.1.14)(rxjs@7.8.2)': + '@nestjs/microservices@11.1.6(@grpc/grpc-js@1.14.0)(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6)(@nestjs/websockets@11.1.6)(cache-manager@5.7.6)(ioredis@5.8.0)(nats@2.29.3)(reflect-metadata@0.1.14)(rxjs@7.8.2)': dependencies: '@nestjs/common': 11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2) '@nestjs/core': 11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/microservices@11.1.6)(@nestjs/platform-express@11.1.6)(@nestjs/websockets@11.1.6)(reflect-metadata@0.1.14)(rxjs@7.8.2) @@ -7903,7 +7895,7 @@ snapshots: '@grpc/grpc-js': 1.14.0 '@nestjs/websockets': 11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6)(@nestjs/platform-socket.io@11.1.6)(reflect-metadata@0.1.14)(rxjs@7.8.2) cache-manager: 5.7.6 - ioredis: 5.8.1 + ioredis: 5.8.0 nats: 2.29.3 '@nestjs/passport@11.0.5(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(passport@0.6.0)': @@ -7984,16 +7976,16 @@ snapshots: '@nestjs/core': 11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/microservices@11.1.6)(@nestjs/platform-express@11.1.6)(@nestjs/websockets@11.1.6)(reflect-metadata@0.1.14)(rxjs@7.8.2) tslib: 2.8.1 optionalDependencies: - '@nestjs/microservices': 11.1.6(@grpc/grpc-js@1.14.0)(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6)(@nestjs/websockets@11.1.6)(cache-manager@5.7.6)(ioredis@5.8.1)(nats@2.29.3)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/microservices': 11.1.6(@grpc/grpc-js@1.14.0)(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6)(@nestjs/websockets@11.1.6)(cache-manager@5.7.6)(ioredis@5.8.0)(nats@2.29.3)(reflect-metadata@0.1.14)(rxjs@7.8.2) '@nestjs/platform-express': 11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6) - '@nestjs/typeorm@11.0.0(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6)(reflect-metadata@0.1.14)(rxjs@7.8.2)(typeorm@0.3.27(ioredis@5.8.1)(pg@8.16.3)(redis@3.1.2)(reflect-metadata@0.1.14)(ts-node@10.9.2(@types/node@20.19.17)(typescript@5.9.2)))': + '@nestjs/typeorm@11.0.0(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6)(reflect-metadata@0.1.14)(rxjs@7.8.2)(typeorm@0.3.27(ioredis@5.8.0)(pg@8.16.3)(redis@3.1.2)(reflect-metadata@0.1.14)(ts-node@10.9.2(@types/node@20.19.17)(typescript@5.9.2)))': dependencies: '@nestjs/common': 11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2) '@nestjs/core': 11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/microservices@11.1.6)(@nestjs/platform-express@11.1.6)(@nestjs/websockets@11.1.6)(reflect-metadata@0.1.14)(rxjs@7.8.2) reflect-metadata: 0.1.14 rxjs: 7.8.2 - typeorm: 0.3.27(ioredis@5.8.1)(pg@8.16.3)(redis@3.1.2)(reflect-metadata@0.1.14)(ts-node@10.9.2(@types/node@20.19.17)(typescript@5.9.2)) + typeorm: 0.3.27(ioredis@5.8.0)(pg@8.16.3)(redis@3.1.2)(reflect-metadata@0.1.14)(ts-node@10.9.2(@types/node@20.19.17)(typescript@5.9.2)) '@nestjs/websockets@11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6)(@nestjs/platform-socket.io@11.1.6)(reflect-metadata@0.1.14)(rxjs@7.8.2)': dependencies: @@ -9684,7 +9676,7 @@ snapshots: dependencies: cron-parser: 4.9.0 get-port: 5.1.1 - ioredis: 5.8.1 + ioredis: 5.8.0 lodash: 4.17.21 msgpackr: 1.11.5 semver: 7.7.2 @@ -9701,7 +9693,7 @@ snapshots: cache-manager-ioredis-yet@2.1.2: dependencies: cache-manager: 5.7.6 - ioredis: 5.8.1 + ioredis: 5.8.0 telejson: 7.2.0 transitivePeerDependencies: - supports-color @@ -11309,7 +11301,7 @@ snapshots: hasown: 2.0.2 side-channel: 1.1.0 - ioredis@5.8.1: + ioredis@5.8.0: dependencies: '@ioredis/commands': 1.4.0 cluster-key-slot: 1.1.2 @@ -11989,10 +11981,6 @@ snapshots: dependencies: json-buffer: 3.0.1 - keyv@5.5.3: - dependencies: - '@keyv/serialize': 1.1.1 - klaw@1.3.1: optionalDependencies: graceful-fs: 4.2.11 @@ -12377,10 +12365,10 @@ snapshots: nestjs-supabase-auth@1.0.9: {} - nestjs-typeorm-paginate@4.1.0(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(typeorm@0.3.27(ioredis@5.8.1)(pg@8.16.3)(redis@3.1.2)(reflect-metadata@0.1.14)(ts-node@10.9.2(@types/node@20.19.17)(typescript@5.9.2))): + nestjs-typeorm-paginate@4.1.0(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(typeorm@0.3.27(ioredis@5.8.0)(pg@8.16.3)(redis@3.1.2)(reflect-metadata@0.1.14)(ts-node@10.9.2(@types/node@20.19.17)(typescript@5.9.2))): dependencies: '@nestjs/common': 11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2) - typeorm: 0.3.27(ioredis@5.8.1)(pg@8.16.3)(redis@3.1.2)(reflect-metadata@0.1.14)(ts-node@10.9.2(@types/node@20.19.17)(typescript@5.9.2)) + typeorm: 0.3.27(ioredis@5.8.0)(pg@8.16.3)(redis@3.1.2)(reflect-metadata@0.1.14)(ts-node@10.9.2(@types/node@20.19.17)(typescript@5.9.2)) netmask@2.0.2: {} @@ -13944,7 +13932,7 @@ snapshots: typedarray@0.0.6: {} - typeorm@0.3.27(ioredis@5.8.1)(pg@8.16.3)(redis@3.1.2)(reflect-metadata@0.1.14)(ts-node@10.9.2(@types/node@20.19.17)(typescript@5.9.2)): + typeorm@0.3.27(ioredis@5.8.0)(pg@8.16.3)(redis@3.1.2)(reflect-metadata@0.1.14)(ts-node@10.9.2(@types/node@20.19.17)(typescript@5.9.2)): dependencies: '@sqltools/formatter': 1.2.5 ansis: 3.17.0 @@ -13962,7 +13950,7 @@ snapshots: uuid: 11.1.0 yargs: 17.7.2 optionalDependencies: - ioredis: 5.8.1 + ioredis: 5.8.0 pg: 8.16.3 redis: 3.1.2 ts-node: 10.9.2(@types/node@20.19.17)(typescript@5.9.2) diff --git a/tsconfig.json b/tsconfig.json index 74a5fa904..48e1b3c98 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,17 +2,11 @@ "extends": "./tsconfig.build.json", "compilerOptions": { "paths": { - "@credebl/common": [ - "libs/common/src" - ], - "@credebl/common/*": [ - "libs/common/src/*" - ], - "@credebl/keycloak-url": [ - "libs/keycloak-url/src" + "@credebl/aws": [ + "libs/aws/src" ], - "@credebl/keycloak-url/*": [ - "libs/keycloak-url/src/*" + "@credebl/aws/*": [ + "libs/aws/src/*" ], "@credebl/client-registration": [ "libs/client-registration/src" @@ -20,23 +14,35 @@ "@credebl/client-registration/*": [ "libs/client-registration/src/*" ], - "@credebl/prisma": [ - "libs/prisma/src" + "@credebl/common": [ + "libs/common/src" ], - "@credebl/prisma/*": [ - "libs/prisma/src/*" + "@credebl/common/*": [ + "libs/common/src/*" ], - "@credebl/repositories": [ - "libs/repositories/src" + "@credebl/config": [ + "libs/config/src" ], - "@credebl/repositories/*": [ - "libs/repositories/src/*" + "@credebl/config/*": [ + "libs/config/src/*" ], - "@credebl/user-request": [ - "libs/user-request/src" + "@credebl/context": [ + "libs/context/src" ], - "@credebl/user-request/*": [ - "libs/user-request/src/*" + "@credebl/context/*": [ + "libs/context/src/*" + ], + "@credebl/enum": [ + "libs/enum/src" + ], + "@credebl/enum/*": [ + "libs/enum/src/*" + ], + "@credebl/keycloak-url": [ + "libs/keycloak-url/src" + ], + "@credebl/keycloak-url/*": [ + "libs/keycloak-url/src/*" ], "@credebl/logger": [ "libs/logger/src" @@ -44,11 +50,14 @@ "@credebl/logger/*": [ "libs/logger/src/*" ], - "@credebl/enum": [ - "libs/enum/src" + "@credebl/org-roles": [ + "libs/org-roles/src" ], - "@credebl/enum/*": [ - "libs/enum/src/*" + "@credebl/org-roles/*": [ + "libs/org-roles/src/*" + ], + "@credebl/prisma": [ + "libs/prisma/src" ], "@credebl/prisma-service": [ "libs/prisma-service/src" @@ -56,17 +65,20 @@ "@credebl/prisma-service/*": [ "libs/prisma-service/src/*" ], - "@credebl/org-roles": [ - "libs/org-roles/src" + "@credebl/prisma/*": [ + "libs/prisma/src/*" ], - "@credebl/org-roles/*": [ - "libs/org-roles/src/*" + "@credebl/repositories": [ + "libs/repositories/src" ], - "@credebl/user-org-roles": [ - "libs/user-org-roles/src" + "@credebl/repositories/*": [ + "libs/repositories/src/*" ], - "@credebl/user-org-roles/*": [ - "libs/user-org-roles/src/*" + "@credebl/supabase": [ + "libs/supabase/src" + ], + "@credebl/supabase/*": [ + "libs/supabase/src/*" ], "@credebl/user-activity": [ "libs/user-activity/src" @@ -74,37 +86,26 @@ "@credebl/user-activity/*": [ "libs/user-activity/src/*" ], - "@credebl/supabase": [ - "libs/supabase/src" + "@credebl/user-org-roles": [ + "libs/user-org-roles/src" ], - "@credebl/supabase/*": [ - "libs/supabase/src/*" + "@credebl/user-org-roles/*": [ + "libs/user-org-roles/src/*" ], - "@credebl/aws": [ - "libs/aws/src" + "@credebl/user-request": [ + "libs/user-request/src" ], - "@credebl/aws/*": [ - "libs/aws/src/*" + "@credebl/user-request/*": [ + "libs/user-request/src/*" ], "credebl/utility": [ "libs/utility/src" ], "credebl/utility/*": [ "libs/utility/src/*" - ], - "@credebl/config": [ - "libs/config/src" - ], - "@credebl/config/*": [ - "libs/config/src/*" - ], - "@credebl/context": [ - "libs/context/src" - ], - "@credebl/context/*": [ - "libs/context/src/*" ] - } + }, + "baseUrl": "./" }, "exclude": [ "node_modules",