diff --git a/Dockerfiles/Dockerfile.oid4vc-issuance b/Dockerfiles/Dockerfile.oid4vc-issuance new file mode 100644 index 000000000..d127d2e9a --- /dev/null +++ b/Dockerfiles/Dockerfile.oid4vc-issuance @@ -0,0 +1,45 @@ +# 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 package-lock.json ./ + +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 migrate deploy && npx prisma generate +RUN cd libs/prisma-service && npx prisma generate + +# Build the issuance service +RUN pnpm run build issuance + +# Stage 2: Create the final image +FROM node:18-alpine +# Install OpenSSL +RUN apk add --no-cache openssl +# RUN npm install -g pnpm +# 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/package.json ./ +COPY --from=build /app/node_modules ./node_modules +# COPY --from=build /app/uploadedFiles ./uploadedFiles + + +# 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"] diff --git a/apps/agent-service/src/agent-service.controller.ts b/apps/agent-service/src/agent-service.controller.ts index dcd3cf149..0c4d7c81b 100644 --- a/apps/agent-service/src/agent-service.controller.ts +++ b/apps/agent-service/src/agent-service.controller.ts @@ -324,37 +324,60 @@ export class AgentServiceController { return this.agentServiceService.getAgentDetails(payload.orgId); } - @MessagePattern({ cmd: 'agent-create-oidc-issuer' }) + @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-oidc-issuer' }) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - async oidcDeleteIssuer(payload: { url: string; orgId: string }): Promise { + @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-oidc-template' }) + @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 oidc - @MessagePattern({ cmd: 'oidc-get-issuer-by-id' }) + //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: 'oidc-get-issuers' }) + @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-oidc-create-credential-offer' }) + @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; offerId: 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); + } } diff --git a/apps/agent-service/src/agent-service.service.ts b/apps/agent-service/src/agent-service.service.ts index 7a4709b2b..4f4e877cb 100644 --- a/apps/agent-service/src/agent-service.service.ts +++ b/apps/agent-service/src/agent-service.service.ts @@ -1423,13 +1423,15 @@ export class AgentServiceService { } } - async deleteOidcIssuer(url: string, orgId: string): Promise { + async deleteOidcIssuer(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; + 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; @@ -1475,6 +1477,58 @@ export class AgentServiceService { } } + 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); diff --git a/apps/api-gateway/src/app.module.ts b/apps/api-gateway/src/app.module.ts index 9994df54d..fe70c182f 100644 --- a/apps/api-gateway/src/app.module.ts +++ b/apps/api-gateway/src/app.module.ts @@ -32,6 +32,7 @@ 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'; @Module({ imports: [ @@ -64,7 +65,8 @@ import { ConfigModule as PlatformConfig } from '@credebl/config/config.module'; GlobalConfigModule, CacheModule.register({ store: redisStore, host: process.env.REDIS_HOST, port: process.env.REDIS_PORT }), GeoLocationModule, - CloudWalletModule + CloudWalletModule, + Oid4vcIssuanceModule ], controllers: [AppController], providers: [ diff --git a/apps/api-gateway/src/issuance/dtos/issuer-sessions.dto.ts b/apps/api-gateway/src/issuance/dtos/issuer-sessions.dto.ts deleted file mode 100644 index a31c65799..000000000 --- a/apps/api-gateway/src/issuance/dtos/issuer-sessions.dto.ts +++ /dev/null @@ -1,224 +0,0 @@ -/* 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, - IsEnum, - IsNotEmpty, - IsObject, - IsOptional, - IsString, - Matches, - ValidateNested, - registerDecorator, - ValidationOptions, - IsInt, - Min, - IsIn, - ArrayMinSize, - IsUrl, - ValidatorConstraint, - ValidatorConstraintInterface, - ValidationArguments, - Validate -} from 'class-validator'; -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; - -export enum CredentialFormat { - SdJwtVc = 'vc+sd-jwt', - Mdoc = 'mdoc' -} - -export enum SignerMethodOption { - DID = 'did', - X5C = 'x5c' -} - -/** ---------- custom validator: disclosureFrame ---------- */ -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; - } - 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'; - } - } - }); - }; -} - -/** ---------- payload DTOs ---------- */ -export class CredentialPayloadDto { - @ApiPropertyOptional() - @IsOptional() - @IsString() - vct?: string; - - @ApiPropertyOptional({ example: 'Garry' }) - @IsOptional() - @IsString() - full_name?: string; - - @ApiPropertyOptional({ example: '2000-01-01', description: 'YYYY-MM-DD' }) - @IsOptional() - @Matches(/^\d{4}-\d{2}-\d{2}$/, { message: 'birth_date must be YYYY-MM-DD' }) - birth_date?: string; - - @ApiPropertyOptional({ example: 'Africa' }) - @IsOptional() - @IsString() - birth_place?: string; - - @ApiPropertyOptional({ example: 'James Bear' }) - @IsOptional() - @IsString() - parent_names?: string; - - [key: string]: unknown; -} - -export class CredentialRequestDto { - @ApiProperty({ example: '1b2d3c4e-...' }) - @IsString() - @IsNotEmpty() - templateId!: string; - - @ApiProperty({ enum: CredentialFormat, example: CredentialFormat.SdJwtVc }) - @IsEnum(CredentialFormat) - format!: CredentialFormat; - - @ApiProperty({ - type: CredentialPayloadDto, - description: 'Credential payload (structure depends on the format)' - }) - @ValidateNested() - @Type(() => CredentialPayloadDto) - payload!: CredentialPayloadDto; - - @ApiPropertyOptional({ - description: 'Selective disclosure frame (claim -> boolean or nested map).', - example: { full_name: true, birth_date: true, birth_place: false, parent_names: false }, - required: false - }) - @IsOptional() - @IsObject() - @IsDisclosureFrame() - disclosureFrame?: Record>; -} - -/** ---------- auth-config 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.credebl.ae:8443/realms/credebl', - description: 'AS (Authorization Server) base URL' - }) - @IsUrl({ require_tld: false }) - authorizationServerUrl!: string; -} - -/** ---------- class-level constraint: EXACTLY ONE of the two configs ---------- */ -@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); -} - -/** ---------- root DTO (no authenticationType) ---------- */ -export class CreateOidcCredentialOfferDto { - @ApiProperty({ - type: [CredentialRequestDto], - description: 'At least one credential to be issued.' - }) - @IsArray() - @ArrayMinSize(1) - @ValidateNested({ each: true }) - @Type(() => CredentialRequestDto) - credentials!: CredentialRequestDto[]; - - // Each is optional individually; XOR rule below enforces exactly one present. - @ApiPropertyOptional({ type: PreAuthorizedCodeFlowConfigDto }) - @IsOptional() - @ValidateNested() - @Type(() => PreAuthorizedCodeFlowConfigDto) - preAuthorizedCodeFlowConfig?: PreAuthorizedCodeFlowConfigDto; - - @ApiPropertyOptional({ type: AuthorizationCodeFlowConfigDto }) - @IsOptional() - @ValidateNested() - @Type(() => AuthorizationCodeFlowConfigDto) - authorizationCodeFlowConfig?: AuthorizationCodeFlowConfigDto; - - issuerId?: string; - - // Host the class-level XOR validator on a dummy property - @ExactlyOneOf(['preAuthorizedCodeFlowConfig', 'authorizationCodeFlowConfig'], { - message: 'Provide exactly one of preAuthorizedCodeFlowConfig or authorizationCodeFlowConfig.' - }) - private readonly _exactlyOne?: unknown; -} diff --git a/apps/api-gateway/src/issuance/issuance.controller.ts b/apps/api-gateway/src/issuance/issuance.controller.ts index b94ff1777..65014d79d 100644 --- a/apps/api-gateway/src/issuance/issuance.controller.ts +++ b/apps/api-gateway/src/issuance/issuance.controller.ts @@ -960,7 +960,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.module.ts b/apps/api-gateway/src/issuance/issuance.module.ts index 5a7df978c..d5f184d16 100644 --- a/apps/api-gateway/src/issuance/issuance.module.ts +++ b/apps/api-gateway/src/issuance/issuance.module.ts @@ -8,7 +8,6 @@ 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'; -import { OidcController } from './oidc.controller'; @Module({ imports: [ @@ -21,7 +20,7 @@ import { OidcController } from './oidc.controller'; } ]) ], - controllers: [IssuanceController, OidcController], + controllers: [IssuanceController], providers: [IssuanceService, CommonService, AwsService, NATSClient] }) export class IssuanceModule {} diff --git a/apps/api-gateway/src/issuance/issuance.service.ts b/apps/api-gateway/src/issuance/issuance.service.ts index 018285844..6012e20e2 100644 --- a/apps/api-gateway/src/issuance/issuance.service.ts +++ b/apps/api-gateway/src/issuance/issuance.service.ts @@ -28,11 +28,8 @@ import { IIssuedCredential } from '@credebl/common/interfaces/issuance.interface'; import { IssueCredentialDto } from './dtos/multi-connection.dto'; -import { oidc_issuer, user } from '@prisma/client'; import { NATSClient } from '@credebl/common/NATSClient'; -import { CreateCredentialTemplateDto, UpdateCredentialTemplateDto } from './dtos/oidc-issuer-template.dto'; -import { CreateOidcCredentialOfferDto } from './dtos/issuer-sessions.dto'; -import { IssuerCreationDto, IssuerUpdationDto } from './dtos/oidc-issuer.dto'; +import { user } from '@prisma/client'; @Injectable() export class IssuanceService extends BaseService { constructor( @@ -275,91 +272,4 @@ export class IssuanceService extends BaseService { }; return this.natsClient.sendNatsMessage(this.issuanceProxy, 'issued-file-data-and-file-details', payload); } - - // OIDC methods - - async oidcIssuerCreate( - issueCredentialDto: IssuerCreationDto, - orgId: string, - userDetails: user - ): Promise { - const payload = { issueCredentialDto, orgId, userDetails }; - return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oidc-issuer-create', payload); - } - - async oidcIssuerUpdate(issueUpdationDto: IssuerUpdationDto, orgId: string, userDetails: user) { - const payload = { issueUpdationDto, orgId, userDetails }; - return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oidc-issuer-update', payload); - } - - async oidcGetIssuerById(issuerId: string, orgId: string) { - const payload = { issuerId, orgId }; - return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oidc-issuer-get-by-id', payload); - } - - async oidcGetIssuers(orgId: string) { - const payload = { orgId }; - return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oidc-get-issuers', payload); - } - - async oidcDeleteIssuer(userDetails: user, orgId: string, issuerId: string) { - const payload = { issuerId, orgId, userDetails }; - return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oidc-delete-issuer', payload); - } - - async deleteTemplate(userDetails: user, orgId: string, templateId: string) { - const payload = { templateId, orgId, userDetails }; - return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oidc-template-delete', payload); - } - - async updateTemplate( - userDetails: user, - orgId: string, - templateId: string, - dto: UpdateCredentialTemplateDto, - issuerId: string - ) { - const payload = { templateId, orgId, userDetails, dto, issuerId }; - return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oidc-template-update', payload); - } - - async findByIdTemplate(userDetails: user, orgId: string, templateId: string) { - const payload = { templateId, orgId, userDetails }; - return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oidc-template-find-id', payload); - } - - async findAllTemplate(userDetails: user, orgId: string) { - const payload = { orgId, userDetails }; - return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oidc-template-find-all', payload); - } - - async createTemplate( - CredentialTemplate: CreateCredentialTemplateDto, - userDetails: user, - orgId: string, - issuerId: string - ) { - const payload = { CredentialTemplate, orgId, userDetails, issuerId }; - return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oidc-template-create', payload); - } - - async createOidcCredentialOffer( - oidcCredentialPayload: CreateOidcCredentialOfferDto, - userDetails: user, - orgId: string, - issuerId: string - ) { - const payload = { oidcCredentialPayload, orgId, userDetails, issuerId }; - return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oidc-create-credential-offer', payload); - } - - oidcIssueCredentialWebhook( - oidcIssueCredentialDto, - id: string - ): Promise<{ - response: object; - }> { - const payload = { oidcIssueCredentialDto, id }; - return this.natsClient.sendNats(this.issuanceProxy, 'webhook-oidc-issue-credential', payload); - } } 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..4d056d2fc --- /dev/null +++ b/apps/api-gateway/src/oid4vc-issuance/dtos/issuer-sessions.dto.ts @@ -0,0 +1,380 @@ +/* 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 +} from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; + +/* ========= Enums ========= */ +export enum CredentialFormat { + SdJwtVc = 'vc+sd-jwt', + Mdoc = 'mdoc' +} + +/* ========= 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); +} + +/* ========= 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({ + description: 'Selective disclosure: claim -> boolean (or nested map)', + example: { name: true, DOB: true, additionalProp3: false }, + required: false + }) + @IsOptional() + @IsDisclosureFrame() + disclosureFrame?: Record>; +} + +export class CreateOidcCredentialOfferDto { + @ApiProperty({ + type: [CredentialRequestDto], + description: 'At least one credential to be issued.' + }) + @IsArray() + @ArrayMinSize(1) + @ValidateNested({ each: true }) + @Type(() => CredentialRequestDto) + credentials!: CredentialRequestDto[]; + + // XOR: exactly one present + @ApiPropertyOptional({ type: PreAuthorizedCodeFlowConfigDto }) + @IsOptional() + @ValidateNested() + @Type(() => PreAuthorizedCodeFlowConfigDto) + preAuthorizedCodeFlowConfig?: PreAuthorizedCodeFlowConfigDto; + + @IsOptional() + @ValidateNested() + @Type(() => AuthorizationCodeFlowConfigDto) + authorizationCodeFlowConfig?: AuthorizationCodeFlowConfigDto; + + issuerId?: string; + + // host XOR rule + @ExactlyOneOf(['preAuthorizedCodeFlowConfig', 'authorizationCodeFlowConfig'], { + message: 'Provide exactly one of preAuthorizedCodeFlowConfig or authorizationCodeFlowConfig.' + }) + private readonly _exactlyOne?: unknown; +} + +export class GetAllCredentialOfferDto { + @ApiProperty({ required: false, example: 'credebl university' }) + @IsOptional() + publicIssuerId: string = ''; + + @ApiProperty({ required: false, example: '568345' }) + @IsOptional() + preAuthorizedCode: string = ''; + + // @ApiPropertyOptional({ + // example: OpenId4VcIssuanceSessionState.OfferCreated, + // enum: OpenId4VcIssuanceSessionState, + // required: false, + // }) + // @IsOptional() + // @IsEnum(OpenId4VcIssuanceSessionState) + // state?: OpenId4VcIssuanceSessionState; + + @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' + } + } + }) + @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..d4a02f669 --- /dev/null +++ b/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-credential-wh.dto.ts @@ -0,0 +1,50 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsArray, IsObject, IsString } from 'class-validator'; + +export class CredentialOfferPayloadDto { + @ApiProperty() + @IsString() + // eslint-disable-next-line camelcase + credential_issuer!: string; + + @ApiProperty({ type: [String] }) + @IsArray() + // eslint-disable-next-line camelcase + credential_configuration_ids!: string[]; + + @ApiProperty({ type: 'object', additionalProperties: true }) + @IsObject() + grants!: Record; + + @ApiProperty({ type: [Object] }) + @IsArray() + credentials!: Record[]; +} + +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() + @IsString() + state!: string; + + @ApiProperty() + @IsString() + contextCorrelationId!: string; +} diff --git a/apps/api-gateway/src/issuance/dtos/oidc-issuer-template.dto.ts b/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer-template.dto.ts similarity index 77% rename from apps/api-gateway/src/issuance/dtos/oidc-issuer-template.dto.ts rename to apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer-template.dto.ts index efd901a89..1035166d7 100644 --- a/apps/api-gateway/src/issuance/dtos/oidc-issuer-template.dto.ts +++ b/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer-template.dto.ts @@ -7,11 +7,13 @@ import { ValidateNested, IsObject, IsNotEmpty, - IsArray + IsArray, + ValidateIf, + IsEmpty } from 'class-validator'; import { ApiExtraModels, ApiProperty, ApiPropertyOptional, getSchemaPath, PartialType } from '@nestjs/swagger'; import { Type } from 'class-transformer'; -import { DisplayDto } from './oidc-issuer.dto'; +import { DisplayDto } from './oid4vc-issuer.dto'; export class CredentialAttributeDto { @ApiProperty({ required: false, description: 'Whether the attribute is mandatory' }) @@ -125,9 +127,34 @@ export class CreateCredentialTemplateDto { @IsEnum(SignerOption) signerOption!: SignerOption; - @ApiProperty({ enum: ['sd-jwt-vc', 'mdoc'], description: 'Credential format type' }) - @IsEnum(['sd-jwt-vc', 'mso_mdoc', 'vc+sd-jwt']) - format: 'sd-jwt-vc' | 'mso_mdoc ' | 'vc+sd-jwt'; + @ApiProperty({ enum: ['mso_mdoc', 'vc+sd-jwt'], description: 'Credential format type' }) + @IsEnum(['mso_mdoc', 'vc+sd-jwt']) + format: 'mso_mdoc' | 'vc+sd-jwt'; + + @ApiPropertyOptional({ + 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; + + @ValidateIf((o: CreateCredentialTemplateDto) => 'vc+sd-jwt' === o.format) + @IsEmpty({ message: 'doctype must not be provided when format is "vc+sd-jwt"' }) + readonly _doctypeAbsentGuard?: unknown; + + @ApiPropertyOptional({ + description: + 'Verifiable Credential Type (required when format is "vc+sd-jwt"; must NOT be provided when format is "mso_mdoc")', + example: 'org.iso.18013.5.1.mDL' + }) + @ValidateIf((o: CreateCredentialTemplateDto) => 'vc+sd-jwt' === o.format) + @IsString() + vct?: string; + + @ValidateIf((o: CreateCredentialTemplateDto) => 'mso_mdoc' === o.format) + @IsEmpty({ message: 'vct must not be provided when format is "mso_mdoc"' }) + readonly _vctAbsentGuard?: unknown; @ApiProperty({ default: false, description: 'Indicates whether credentials can be revoked' }) @IsBoolean() diff --git a/apps/api-gateway/src/issuance/dtos/oidc-issuer.dto.ts b/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer.dto.ts similarity index 94% rename from apps/api-gateway/src/issuance/dtos/oidc-issuer.dto.ts rename to apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer.dto.ts index 89216fcb7..b9e67f423 100644 --- a/apps/api-gateway/src/issuance/dtos/oidc-issuer.dto.ts +++ b/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer.dto.ts @@ -199,7 +199,7 @@ export class ClientAuthenticationDto { export class AuthorizationServerConfigDto { @ApiProperty({ description: 'Authorization server issuer URL', - example: 'https://example-oidc-provider.com' + example: 'https://example-oid4vc-provider.com' }) @IsString() issuer: string; @@ -248,23 +248,15 @@ export class IssuerCreationDto { description: 'Configuration of the authorization server', type: AuthorizationServerConfigDto }) + @IsOptional() @ValidateNested() @Type(() => AuthorizationServerConfigDto) - authorizationServerConfigs: AuthorizationServerConfigDto; + authorizationServerConfigs?: AuthorizationServerConfigDto; } export class IssuerUpdationDto { issuerId?: string; - @ApiProperty({ - description: 'accessTokenSignerKeyType', - example: 'ed25519' - }) - @IsString({ - message: 'accessTokenSignerKeyType from IssuerCreationDto -> accessTokenSignerKeyType, must be a string' - }) - accessTokenSignerKeyType: string; - @ApiProperty({ description: 'Localized display information for the credential', type: [DisplayDto] diff --git a/apps/api-gateway/src/issuance/oidc.controller.ts b/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.controller.ts similarity index 53% rename from apps/api-gateway/src/issuance/oidc.controller.ts rename to apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.controller.ts index b37cc4cb4..f88b8718a 100644 --- a/apps/api-gateway/src/issuance/oidc.controller.ts +++ b/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.controller.ts @@ -16,7 +16,9 @@ import { BadRequestException, ParseUUIDPipe, Delete, - Patch + Patch, + Query, + Put } from '@nestjs/common'; import { ApiTags, @@ -33,7 +35,6 @@ 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 { IssuanceService } from './issuance.service'; import { User } from '../authz/decorators/user.decorator'; import { ResponseMessages } from '@credebl/common/response-messages'; import { Roles } from '../authz/decorators/roles.decorator'; @@ -42,17 +43,23 @@ 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/oidc-issuer.dto'; -import { CreateCredentialTemplateDto, UpdateCredentialTemplateDto } from './dtos/oidc-issuer-template.dto'; -import { CreateOidcCredentialOfferDto } from './dtos/issuer-sessions.dto'; -import { IssuanceDto } from './dtos/issuance.dto'; +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('OIDC') +@ApiTags('OID4VC') @ApiUnauthorizedResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Unauthorized', type: UnauthorizedErrorDto }) @ApiForbiddenResponse({ status: HttpStatus.FORBIDDEN, description: 'Forbidden', type: ForbiddenErrorDto }) -export class OidcController { - constructor(private readonly issueCredentialService: IssuanceService) {} +export class Oid4vcIssuanceController { + constructor(private readonly oid4vcIssuanceService: Oid4vcIssuanceService) {} /** * Create issuer against a org(tenant) * @param orgId The ID of the organization @@ -60,9 +67,13 @@ export class OidcController { * @param res The response object * @returns The status of the deletion operation */ - @Post('/orgs/:orgId/oidc/issuers') - @ApiOperation({ summary: 'Create OIDC issuer', description: 'Create OIDC issuer by orgId' }) - @ApiResponse({ status: HttpStatus.OK, description: 'Success', type: ApiResponseDto }) + + @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) @@ -80,7 +91,7 @@ export class OidcController { @Body() issueCredentialDto: IssuerCreationDto, @Res() res: Response ): Promise { - const createIssuer = await this.issueCredentialService.oidcIssuerCreate(issueCredentialDto, orgId, user); + const createIssuer = await this.oid4vcIssuanceService.oidcIssuerCreate(issueCredentialDto, orgId, user); const finalResponse: IResponse = { statusCode: HttpStatus.CREATED, message: ResponseMessages.oidcIssuer.success.issuerConfig, @@ -89,9 +100,12 @@ export class OidcController { return res.status(HttpStatus.CREATED).json(finalResponse); } - @Post('/orgs/:orgId/oidc/issuers/:issuerId') - @ApiOperation({ summary: 'Update OIDC issuer', description: 'Update OIDC issuer by orgId' }) - @ApiResponse({ status: HttpStatus.OK, description: 'Success', type: ApiResponseDto }) + @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) @@ -112,7 +126,7 @@ export class OidcController { @Res() res: Response ): Promise { issueCredentialDto.issuerId = issuerId; - const createIssuer = await this.issueCredentialService.oidcIssuerUpdate(issueCredentialDto, orgId, user); + const createIssuer = await this.oid4vcIssuanceService.oidcIssuerUpdate(issueCredentialDto, orgId, user); const finalResponse: IResponse = { statusCode: HttpStatus.CREATED, message: ResponseMessages.oidcIssuer.success.issuerConfigUpdate, @@ -121,9 +135,9 @@ export class OidcController { return res.status(HttpStatus.CREATED).json(finalResponse); } - @Get('/orgs/:orgId/oidc/issuers/:issuerId') - @ApiOperation({ summary: 'Get OIDC issuer', description: 'Get OIDC issuer by orgId' }) - @ApiResponse({ status: HttpStatus.OK, description: 'Success', type: ApiResponseDto }) + @Get('/orgs/:orgId/oid4vc/issuers/:issuerId') + @ApiOperation({ summary: 'Get OID4VC issuer', description: 'Retrieves an OID4VC issuer by issuerId.' }) + @ApiResponse({ status: HttpStatus.OK, description: 'Issuer fetched successfully.', type: ApiResponseDto }) @ApiBearerAuth() @Roles(OrgRoles.OWNER) @UseGuards(AuthGuard('jwt'), OrgRolesGuard) @@ -141,7 +155,7 @@ export class OidcController { issuerId: string, @Res() res: Response ): Promise { - const oidcIssuer = await this.issueCredentialService.oidcGetIssuerById(issuerId, orgId); + const oidcIssuer = await this.oid4vcIssuanceService.oidcGetIssuerById(issuerId, orgId); const finalResponse: IResponse = { statusCode: HttpStatus.CREATED, message: ResponseMessages.oidcIssuer.success.fetch, @@ -150,9 +164,12 @@ export class OidcController { return res.status(HttpStatus.CREATED).json(finalResponse); } - @Get('/orgs/:orgId/oidc/issuers') - @ApiOperation({ summary: 'Get OIDC issuer', description: 'Get OIDC issuer by orgId' }) - @ApiResponse({ status: HttpStatus.OK, description: 'Success', type: ApiResponseDto }) + @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) @@ -168,20 +185,23 @@ export class OidcController { orgId: string, @Res() res: Response ): Promise { - const oidcIssuer = await this.issueCredentialService.oidcGetIssuers(orgId); + const oidcIssuer = await this.oid4vcIssuanceService.oidcGetIssuers(orgId); const finalResponse: IResponse = { - statusCode: HttpStatus.CREATED, + statusCode: HttpStatus.OK, message: ResponseMessages.oidcIssuer.success.fetch, data: oidcIssuer }; - return res.status(HttpStatus.CREATED).json(finalResponse); + return res.status(HttpStatus.OK).json(finalResponse); } - @Delete('/orgs/:orgId/oidc/:issuerId') + @Delete('/orgs/:orgId/oid4vc/:issuerId') + @ApiOperation({ + summary: 'Delete OID4VC issuer', + description: 'Deletes an OID4VC issuer for the specified organization.' + }) @ApiBearerAuth() @Roles(OrgRoles.OWNER) @UseGuards(AuthGuard('jwt'), OrgRolesGuard) - @ApiOperation({ summary: 'Delete oidc issuer' }) async deleteOidcIssuer( @Param( 'orgId', @@ -204,7 +224,7 @@ export class OidcController { @User() user: user, @Res() res: Response ): Promise { - await this.issueCredentialService.oidcDeleteIssuer(user, orgId, issuerId); + await this.oid4vcIssuanceService.oidcDeleteIssuer(user, orgId, issuerId); const finalResponse: IResponse = { statusCode: HttpStatus.OK, @@ -214,9 +234,12 @@ export class OidcController { return res.status(HttpStatus.OK).json(finalResponse); } - @Post('/orgs/:orgId/oidc/:issuerId/template') - @ApiOperation({ summary: 'Create credential template' }) - @ApiResponse({ status: HttpStatus.CREATED, description: 'Template created successfully' }) + @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) @@ -231,7 +254,7 @@ export class OidcController { ): Promise { CredentialTemplate.issuerId = issuerId; console.log('THis is dto', JSON.stringify(CredentialTemplate, null, 2)); - const template = await this.issueCredentialService.createTemplate(CredentialTemplate, user, orgId, issuerId); + const template = await this.oid4vcIssuanceService.createTemplate(CredentialTemplate, user, orgId, issuerId); const finalResponse: IResponse = { statusCode: HttpStatus.CREATED, @@ -242,9 +265,12 @@ export class OidcController { return res.status(HttpStatus.CREATED).json(finalResponse); } - @Get('/orgs/:orgId/oidc/:issuerId/template') - @ApiOperation({ summary: 'List credential templates' }) - @ApiResponse({ status: HttpStatus.OK, description: 'List of templates' }) + @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) @@ -262,15 +288,14 @@ export class OidcController { 'issuerId', new ParseUUIDPipe({ exceptionFactory: (): Error => { - throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + throw new BadRequestException(ResponseMessages.oidcTemplate.error.invalidId); } }) ) issuerId: string, - @User() user: user, @Res() res: Response ): Promise { - const templates = await this.issueCredentialService.findAllTemplate(user, orgId, issuerId); + const templates = await this.oid4vcIssuanceService.findAllTemplate(orgId, issuerId); const finalResponse: IResponse = { statusCode: HttpStatus.OK, @@ -281,8 +306,8 @@ export class OidcController { return res.status(HttpStatus.OK).json(finalResponse); } - @Get('/orgs/:orgId/oidc/:issuerId/template/:templateId') - @ApiOperation({ summary: 'Get credential template by ID' }) + @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) @@ -296,43 +321,29 @@ export class OidcController { }) ) 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); - } - }) - ) + @Param('templateId') templateId: string, - @User() user: user, @Res() res: Response ): Promise { - const template = await this.issueCredentialService.findByIdTemplate(user, orgId, templateId, issuerId); + const template = await this.oid4vcIssuanceService.findByIdTemplate(orgId, templateId); const finalResponse: IResponse = { statusCode: HttpStatus.OK, - message: ResponseMessages.oidcTemplate.success.fetch, + message: ResponseMessages.oidcTemplate.success.getById, data: template }; return res.status(HttpStatus.OK).json(finalResponse); } - @Patch('/orgs/:orgId/oidc/:issuerId/template/:templateId') + @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) - @ApiOperation({ summary: 'Update credential template' }) async updateTemplate( @Param( 'orgId', @@ -365,7 +376,7 @@ export class OidcController { @Body() dto: UpdateCredentialTemplateDto, @Res() res: Response ): Promise { - const updated = await this.issueCredentialService.updateTemplate(user, orgId, templateId, dto, issuerId); + const updated = await this.oid4vcIssuanceService.updateTemplate(user, orgId, templateId, dto, issuerId); const finalResponse: IResponse = { statusCode: HttpStatus.OK, @@ -376,11 +387,14 @@ export class OidcController { return res.status(HttpStatus.OK).json(finalResponse); } - @Delete('/orgs/:orgId/oidc/:issuerId/template/:templateId') + @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) - @ApiOperation({ summary: 'Delete credential template' }) async deleteTemplate( @Param( 'orgId', @@ -412,8 +426,7 @@ export class OidcController { @User() user: user, @Res() res: Response ): Promise { - await this.issueCredentialService.deleteTemplate(user, orgId, templateId, issuerId); - + await this.oid4vcIssuanceService.deleteTemplate(user, orgId, templateId); const finalResponse: IResponse = { statusCode: HttpStatus.OK, message: ResponseMessages.oidcTemplate.success.delete @@ -422,15 +435,15 @@ export class OidcController { return res.status(HttpStatus.OK).json(finalResponse); } - @Post('/orgs/:orgId/oidc/:issuerId/create-offer') - @ApiOperation({ summary: 'Create OIDC Credential Offer' }) - @ApiResponse({ - status: HttpStatus.CREATED, - description: `This endpoint creates a new OIDC4VCI credential-offer for a given issuer. It allows clients to request issuance of credentials (e.g., Birth Certificate, Driving License, Student ID) from a registered OIDC issuer using the issuer's ID.` + @Post('/orgs/:orgId/oid4vc/:issuerId/create-offer') + @ApiOperation({ + summary: 'Create OID4VC Credential Offer', + description: 'Creates a new OIDC4VCI credential-offer for a given issuer.' }) - // @ApiBearerAuth() - // @Roles(OrgRoles.OWNER) - // @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @ApiResponse({ status: HttpStatus.CREATED, description: 'Credential offer created successfully.' }) + @ApiBearerAuth() + @Roles(OrgRoles.OWNER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) async createOidcCredentialOffer( @Param('orgId') orgId: string, @@ -441,7 +454,7 @@ export class OidcController { @Res() res: Response ): Promise { oidcCredentialPayload.issuerId = issuerId; - const template = await this.issueCredentialService.createOidcCredentialOffer( + const template = await this.oid4vcIssuanceService.createOidcCredentialOffer( oidcCredentialPayload, user, orgId, @@ -449,32 +462,177 @@ export class OidcController { ); const finalResponse: IResponse = { statusCode: HttpStatus.CREATED, - message: ResponseMessages.oidcTemplate.success.create, + 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 oidc issued credential + * @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 oidc issued credential + * @returns The details of the oid4vc issued credential */ - @Post('wh/:id/credentials') + @Post('wh/:id/openid4vc-issuance') @ApiExcludeEndpoint() @ApiOperation({ - summary: 'Catch OIDC credential states', - description: 'Catch OIDC credential states' + summary: 'Catch OID4VC credential states', + description: 'Handles webhook responses for OID4VC credential issuance.' }) async getIssueCredentialWebhook( - @Body() oidcIssueCredentialDto, + @Body() oidcIssueCredentialDto: OidcIssueCredentialDto, @Param('id') id: string, @Res() res: Response ): Promise { - const getCredentialDetails = await this.issueCredentialService.oidcIssueCredentialWebhook( + console.log('Webhook received:', JSON.stringify(oidcIssueCredentialDto, null, 2)); + const getCredentialDetails = await this.oid4vcIssuanceService.oidcIssueCredentialWebhook( oidcIssueCredentialDto, id ); 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..b5282d9dc --- /dev/null +++ b/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.service.ts @@ -0,0 +1,138 @@ +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 { + 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(issuerId: string, orgId: string): Promise { + const payload = { issuerId, 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, issuerId: string): Promise { + const payload = { issuerId, orgId, userDetails }; + return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oid4vc-delete-issuer', payload); + } + + async deleteTemplate(userDetails: user, orgId: string, templateId: string): Promise { + const payload = { templateId, orgId, userDetails }; + 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, 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); + } +} diff --git a/apps/issuance/constant/issuance.ts b/apps/issuance/constant/issuance.ts index a39a160ca..d32498997 100644 --- a/apps/issuance/constant/issuance.ts +++ b/apps/issuance/constant/issuance.ts @@ -1,4 +1,4 @@ -import { AccessTokenSignerKeyType } from '../interfaces/oidc-issuance.interfaces'; +import { AccessTokenSignerKeyType } from '../interfaces/oid4vc-issuance.interfaces'; export const dpopSigningAlgValuesSupported = ['RS256', 'ES256', 'EdDSA']; export const credentialConfigurationsSupported = {}; diff --git a/apps/issuance/src/issuance.controller.ts b/apps/issuance/src/issuance.controller.ts index 6538c39fd..0f1339cd6 100644 --- a/apps/issuance/src/issuance.controller.ts +++ b/apps/issuance/src/issuance.controller.ts @@ -21,18 +21,11 @@ import { Controller } from '@nestjs/common'; import { IssuanceService } from './issuance.service'; import { MessagePattern } from '@nestjs/microservices'; import { OOBIssueCredentialDto } from 'apps/api-gateway/src/issuance/dtos/issuance.dto'; -import { credential_templates, oidc_issuer, user } from '@prisma/client'; -import { CreateCredentialTemplate, UpdateCredentialTemplate } from '../interfaces/oidc-template.interface'; -import { IssuerCreation, IssuerUpdation } from '../interfaces/oidc-issuance.interfaces'; -import { CreateOidcCredentialOffer } from '../interfaces/oidc-issuer-sessions.interfaces'; -import { OIDCIssuanceService } from './oidc-issuance.service'; +import { user } from '@prisma/client'; @Controller() export class IssuanceController { - constructor( - private readonly issuanceService: IssuanceService, - private readonly oidcIssuanceService: OIDCIssuanceService - ) {} + constructor(private readonly issuanceService: IssuanceService) {} @MessagePattern({ cmd: 'get-issuance-records' }) async getIssuanceRecordsByOrgId(payload: { orgId: string; userId: string }): Promise { @@ -142,121 +135,4 @@ export class IssuanceController { async getFileDetailsAndFileDataByFileId(payload: { fileId: string; orgId: string }): Promise { return this.issuanceService.getFileDetailsAndFileDataByFileId(payload.fileId, payload.orgId); } - - @MessagePattern({ cmd: 'oidc-issuer-create' }) - async oidcIssuerCreate(payload: { - issueCredentialDto: IssuerCreation; - orgId: string; - userDetails: user; - }): Promise { - const { issueCredentialDto, orgId, userDetails } = payload; - return this.oidcIssuanceService.oidcIssuerCreate(issueCredentialDto, orgId, userDetails); - } - - @MessagePattern({ cmd: 'oidc-issuer-update' }) - async oidcIssuerUpdate(payload: { - issueUpdationDto: IssuerUpdation; - orgId: string; - userDetails: user; - }): Promise { - const { issueUpdationDto, orgId, userDetails } = payload; - return this.oidcIssuanceService.oidcIssuerUpdate(issueUpdationDto, orgId, userDetails); - } - - @MessagePattern({ cmd: 'oidc-issuer-get-by-id' }) - async oidcGetIssuerById(payload: { - issuerId: string; - orgId: string; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - }): Promise { - const { issuerId, orgId } = payload; - return this.oidcIssuanceService.oidcIssuerGetById(issuerId, orgId); - } - - @MessagePattern({ cmd: 'oidc-get-issuers' }) - async oidcGetIssuers(payload: { orgId: string }): Promise { - const { orgId } = payload; - return this.oidcIssuanceService.oidcIssuers(orgId); - } - - @MessagePattern({ cmd: 'oidc-delete-issuer' }) - async deleteOidcIssuer(payload: { - orgId: string; - userDetails: user; - issuerId: string; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - }): Promise { - const { orgId, userDetails, issuerId } = payload; - return this.oidcIssuanceService.deleteOidcIssuer(orgId, userDetails, issuerId); - } - - @MessagePattern({ cmd: 'oidc-template-create' }) - async oidcTemplateCreate(payload: { - CredentialTemplate: CreateCredentialTemplate; - orgId: string; - issuerId: string; - }): Promise { - const { CredentialTemplate, orgId, issuerId } = payload; - return this.oidcIssuanceService.createTemplate(CredentialTemplate, orgId, issuerId); - } - - @MessagePattern({ cmd: 'oidc-template-update' }) - async oidcTemplateUpdate(payload: { - templateId: string; - dto: UpdateCredentialTemplate; - orgId: string; - issuerId: string; - }): Promise { - const { templateId, dto, orgId, issuerId } = payload; - return this.oidcIssuanceService.updateTemplate(templateId, dto, orgId, issuerId); - } - - @MessagePattern({ cmd: 'oidc-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.oidcIssuanceService.deleteTemplate(templateId, orgId, userDetails, issuerId); - } - - @MessagePattern({ cmd: 'oidc-template-find-id' }) - async oidcTemplateFindById(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.oidcIssuanceService.findByIdTemplate(templateId, orgId, userDetails, issuerId); - } - - @MessagePattern({ cmd: 'oidc-template-find-all' }) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - async oidcTemplateFindAll(payload: { orgId: string; userDetails: user; issuerId: string }): Promise { - const { orgId, userDetails, issuerId } = payload; - return this.oidcIssuanceService.findAllTemplate(orgId, userDetails, issuerId); - } - - @MessagePattern({ cmd: 'oidc-create-credential-offer' }) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - async createOidcCredentialOffer(payload: { - oidcCredentialPayload: CreateOidcCredentialOffer; - orgId: string; - userDetails: user; - issuerId: string; - }): Promise { - const { oidcCredentialPayload, orgId, userDetails, issuerId } = payload; - return this.oidcIssuanceService.createOidcCredentialOffer(oidcCredentialPayload, orgId, userDetails, issuerId); - } - - //TODO: complete the logic - @MessagePattern({ cmd: 'webhook-oidc-issue-credential' }) - async oidcIssueCredentialWebhook(payload: IssueCredentialWebhookPayload): Promise { - return this.issuanceService.getIssueCredentialWebhook(payload); - } } diff --git a/apps/issuance/src/issuance.module.ts b/apps/issuance/src/issuance.module.ts index 080907d74..84886a913 100644 --- a/apps/issuance/src/issuance.module.ts +++ b/apps/issuance/src/issuance.module.ts @@ -21,7 +21,6 @@ import { ConfigModule as PlatformConfig } from '@credebl/config/config.module'; import { ContextInterceptorModule } from '@credebl/context/contextInterceptorModule'; import { GlobalConfigModule } from '@credebl/config/global-config.module'; import { NATSClient } from '@credebl/common/NATSClient'; -import { OIDCIssuanceService } from './oidc-issuance.service'; @Module({ imports: [ @@ -52,7 +51,6 @@ import { OIDCIssuanceService } from './oidc-issuance.service'; controllers: [IssuanceController], providers: [ IssuanceService, - OIDCIssuanceService, IssuanceRepository, UserActivityRepository, PrismaService, diff --git a/apps/issuance/src/issuance.repository.ts b/apps/issuance/src/issuance.repository.ts index 6258a47af..8fa95635a 100644 --- a/apps/issuance/src/issuance.repository.ts +++ b/apps/issuance/src/issuance.repository.ts @@ -14,11 +14,9 @@ import { IssueCredentials, IssuedCredentialStatus } from '../enum/issuance.enum' import { Prisma, agent_invitations, - credential_templates, credentials, file_data, file_upload, - oidc_issuer, org_agents, organisation, platform_config, @@ -32,7 +30,6 @@ import { IIssuedCredentialSearchParams } from 'apps/api-gateway/src/issuance/int import { IUserRequest } from '@credebl/user-request/user-request.interface'; import { PrismaService } from '@credebl/prisma-service'; import { ResponseMessages } from '@credebl/common/response-messages'; -import { IssuerMetadata, IssuerUpdation } from '../interfaces/oidc-issuance.interfaces'; @Injectable() export class IssuanceRepository { @@ -796,186 +793,4 @@ export class IssuanceRepository { 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 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; - } - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - async addOidcIssuerDetails(issuerMetadata: IssuerMetadata, issuerProfileJson): Promise { - try { - const { publicIssuerId, createdById, orgAgentId, batchCredentialIssuanceSize } = issuerMetadata; - const oidcIssuerDetails = await this.prisma.oidc_issuer.create({ - data: { - metadata: issuerProfileJson, - publicIssuerId, - createdBy: createdById, - orgAgentId, - batchCredentialIssuanceSize - } - }); - - 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 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/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/issuance/interfaces/oidc-issuance.interfaces.ts b/apps/oid4vc-issuance/interfaces/oid4vc-issuance.interfaces.ts similarity index 89% rename from apps/issuance/interfaces/oidc-issuance.interfaces.ts rename to apps/oid4vc-issuance/interfaces/oid4vc-issuance.interfaces.ts index 854480f70..1c55048af 100644 --- a/apps/issuance/interfaces/oidc-issuance.interfaces.ts +++ b/apps/oid4vc-issuance/interfaces/oid4vc-issuance.interfaces.ts @@ -1,3 +1,22 @@ +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 Claim { key: string; label: string; diff --git a/apps/issuance/interfaces/oidc-issuer-sessions.interfaces.ts b/apps/oid4vc-issuance/interfaces/oid4vc-issuer-sessions.interfaces.ts similarity index 77% rename from apps/issuance/interfaces/oidc-issuer-sessions.interfaces.ts rename to apps/oid4vc-issuance/interfaces/oid4vc-issuer-sessions.interfaces.ts index e6db80dc0..6fd33ead5 100644 --- a/apps/issuance/interfaces/oidc-issuer-sessions.interfaces.ts +++ b/apps/oid4vc-issuance/interfaces/oid4vc-issuer-sessions.interfaces.ts @@ -1,3 +1,5 @@ +import { OpenId4VcIssuanceSessionState } from '@credebl/enum/enum'; + /* --------------------------------------------------------- * Enums * --------------------------------------------------------- */ @@ -36,7 +38,7 @@ export interface CredentialPayload { } export interface CredentialRequest { - credentialSupportedId: string; + credentialSupportedId?: string; templateId: string; format: CredentialFormat; // "vc+sd-jwt" | "mso_mdoc" payload: CredentialPayload; // user-supplied payload (without vct) @@ -49,3 +51,17 @@ export interface CreateOidcCredentialOffer { 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/issuance/interfaces/oidc-template.interface.ts b/apps/oid4vc-issuance/interfaces/oid4vc-template.interfaces.ts similarity index 86% rename from apps/issuance/interfaces/oidc-template.interface.ts rename to apps/oid4vc-issuance/interfaces/oid4vc-template.interfaces.ts index 2025cfecd..5d2984414 100644 --- a/apps/issuance/interfaces/oidc-template.interface.ts +++ b/apps/oid4vc-issuance/interfaces/oid4vc-template.interfaces.ts @@ -1,5 +1,5 @@ import { Prisma } from '@prisma/client'; -import { Display } from './oidc-issuance.interfaces'; +import { Display } from './oid4vc-issuance.interfaces'; export interface CredentialAttribute { mandatory?: boolean; @@ -21,6 +21,8 @@ export interface CreateCredentialTemplate { attributes: Prisma.JsonValue; appearance?: Prisma.JsonValue; issuerId: string; + vct?: string; + doctype?: string; } export interface UpdateCredentialTemplate extends Partial {} 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..3f878528e --- /dev/null +++ b/apps/oid4vc-issuance/interfaces/oid4vc-wh-interfaces.ts @@ -0,0 +1,48 @@ +export interface CredentialOfferPayload { + credential_issuer: string; + credential_configuration_ids: string[]; + grants: Record; + credentials: Record[]; +} + +export interface IssuanceMetadata { + issuerDid: string; + credentials: Record[]; +} + +export interface OidcIssueCredential { + _tags: Record; + metadata: Record; + issuedCredentials: Record[]; + id: string; + createdAt: string; // ISO date string + issuerId: string; + userPin: string; + preAuthorizedCode: string; + credentialOfferUri: string; + credentialOfferId: string; + credentialOfferPayload: CredentialOfferPayload; + issuanceMetadata: IssuanceMetadata; + state: string; + updatedAt: string; // ISO date string + contextCorrelationId: string; +} + +export interface CredentialOfferWebhookPayload { + credentialOfferId: string; + id: string; + State: string; + contextCorrelationId: string; +} + +export interface CredentialPayload { + orgId: string; + schemaId?: string; + connectionId?: string; + credDefid?: string; + threadId: string; + createdBy: string; + lastChangedBy: string; + state: string; + credentialExchangeId: string; +} diff --git a/apps/issuance/libs/helpers/credential-sessions.builder.ts b/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts similarity index 72% rename from apps/issuance/libs/helpers/credential-sessions.builder.ts rename to apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts index 6764e0634..85cf77e23 100644 --- a/apps/issuance/libs/helpers/credential-sessions.builder.ts +++ b/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts @@ -1,7 +1,7 @@ // builder/credential-offer.builder.ts /* eslint-disable @typescript-eslint/naming-convention, @typescript-eslint/explicit-function-return-type, @typescript-eslint/explicit-module-boundary-types, camelcase */ import { Prisma, credential_templates } from '@prisma/client'; - +import { GetAllCredentialOffer, SignerOption } from '../../interfaces/oid4vc-issuer-sessions.interfaces'; /* ============================================================================ Domain Types ============================================================================ */ @@ -65,6 +65,7 @@ export interface ResolvedSignerOption { export interface BuiltCredential { /** e.g., "BirthCertificateCredential-sdjwt" or "DrivingLicenseCredential-mdoc" */ credentialSupportedId: string; + signerOptions?: ResolvedSignerOption; /** Derived from template.format ("vc+sd-jwt" | "mdoc") */ format: CredentialFormat; /** User-provided payload (validated, with vct removed) */ @@ -75,7 +76,7 @@ export interface BuiltCredential { export interface BuiltCredentialOfferBase { /** Resolved signer option (DID or x5c) */ - signerOption: ResolvedSignerOption; + signerOption?: ResolvedSignerOption; /** Normalized credential entries */ credentials: BuiltCredential[]; /** Optional public issuer id to include */ @@ -111,7 +112,7 @@ const isRecord = (v: unknown): v is Record => Boolean(v) && 'ob /** Map DB format string -> API enum */ function mapDbFormatToApiFormat(db: string): CredentialFormat { const norm = db.toLowerCase().replace(/_/g, '-'); - if ('sd-jwt' === norm || 'vc+sd-jwt' === norm || 'sdjwt' === norm) { + if ('sd-jwt' === norm || 'vc+sd-jwt' === norm || 'sdjwt' === norm || 'sd+jwt-vc' === norm) { return CredentialFormat.SdJwtVc; } if ('mdoc' === norm || 'mso-mdoc' === norm || 'mso-mdoc' === norm) { @@ -188,7 +189,8 @@ function ensureTemplateAttributes(v: Prisma.JsonValue): TemplateAttributes { function buildOneCredential( cred: CredentialRequestDtoLike, template: credential_templates, - attrs: TemplateAttributes + attrs: TemplateAttributes, + signerOptions?: SignerOption[] ): BuiltCredential { // 1) Validate payload against template attributes assertMandatoryClaims(cred.payload, attrs, { templateId: cred.templateId }); @@ -206,6 +208,7 @@ function buildOneCredential( return { credentialSupportedId, // e.g., "BirthCertificateCredential-sdjwt" + signerOptions: signerOptions[0], format: apiFormat, // 'vc+sd-jwt' | 'mdoc' payload, // without vct ...(cred.disclosureFrame ? { disclosureFrame: cred.disclosureFrame } : {}) @@ -213,7 +216,7 @@ function buildOneCredential( } /** - * Build the full OIDC credential offer payload. + * Build the full OID4VC credential offer payload. * - Verifies template IDs * - Validates mandatory claims per template * - Normalizes formats & IDs @@ -225,7 +228,7 @@ function buildOneCredential( export function buildCredentialOfferPayload( dto: CreateOidcCredentialOfferDtoLike, templates: credential_templates[], - signerOption: ResolvedSignerOption + signerOptions?: SignerOption[] ): CredentialOfferPayload { // Index templates const byId = new Map(templates.map((t) => [t.id, t])); @@ -240,12 +243,11 @@ export function buildCredentialOfferPayload( const credentials: BuiltCredential[] = dto.credentials.map((cred) => { const template = byId.get(cred.templateId)!; const attrs = ensureTemplateAttributes(template.attributes); // narrow JsonValue safely - return buildOneCredential(cred, template, attrs); + return buildOneCredential(cred, template, attrs, signerOptions); }); // --- Base envelope (issuerId deliberately NOT included) --- const base: BuiltCredentialOfferBase = { - signerOption, // resolved keys (did/x5c) from DB credentials, ...(dto.publicIssuerId ? { publicIssuerId: dto.publicIssuerId } : {}) }; @@ -269,3 +271,79 @@ export function buildCredentialOfferPayload( authorizationCodeFlowConfig: dto.authorizationCodeFlowConfig! // definite since !hasPre }; } + +// ----------------------------------------------------------------------------- +// Builder: Update Credential Offer +// ----------------------------------------------------------------------------- +export function buildUpdateCredentialOfferPayload( + dto: CreateOidcCredentialOfferDtoLike, + templates: credential_templates[] +): { credentials: BuiltCredential[] } { + // Index templates by id + const byId = new Map(templates.map((t) => [t.id, t])); + + // Validate all templateIds exist + const unknown = dto.credentials.map((c) => c.templateId).filter((id) => !byId.has(id)); + if (unknown.length) { + throw new Error(`Unknown template ids: ${unknown.join(', ')}`); + } + + // Validate each credential against its template + const credentials: BuiltCredential[] = dto.credentials.map((cred) => { + const template = byId.get(cred.templateId)!; + const attrs = ensureTemplateAttributes(template.attributes); // safely narrow JsonValue + + // check that all payload keys exist in template attributes + const payloadKeys = Object.keys(cred.payload); + const invalidKeys = payloadKeys.filter((k) => !attrs[k]); + if (invalidKeys.length) { + throw new Error(`Invalid attributes for template "${cred.templateId}": ${invalidKeys.join(', ')}`); + } + + // also validate mandatory fields are present + assertMandatoryClaims(cred.payload, attrs, { templateId: cred.templateId }); + + // build minimal normalized credential (no vct, issuerId, etc.) + const apiFormat = mapDbFormatToApiFormat(template.format); + const suffix = formatSuffix(apiFormat); + const credentialSupportedId = `${template.name}-${suffix}`; + return { + credentialSupportedId, + format: apiFormat, + payload: cred.payload, + ...(cred.disclosureFrame ? { disclosureFrame: cred.disclosureFrame } : {}) + }; + }); + + // Only return credentials array here (update flow doesn't need preAuth/auth configs) + return { + credentials + }; +} + +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; +} diff --git a/apps/issuance/libs/helpers/issuer.metadata.ts b/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts similarity index 84% rename from apps/issuance/libs/helpers/issuer.metadata.ts rename to apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts index f9950f807..22735c10a 100644 --- a/apps/issuance/libs/helpers/issuer.metadata.ts +++ b/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts @@ -1,7 +1,8 @@ /* eslint-disable camelcase */ import { oidc_issuer, Prisma } from '@prisma/client'; import { batchCredentialIssuanceDefault } from '../../constant/issuance'; -import { CreateOidcCredentialOffer } from 'apps/issuance/interfaces/oidc-issuer-sessions.interfaces'; +import { CreateOidcCredentialOffer } from '../../interfaces/oid4vc-issuer-sessions.interfaces'; +import { IssuerResponse } from 'apps/oid4vc-issuance/interfaces/oid4vc-issuance.interfaces'; type AttributeDisplay = { name: string; locale: string }; type AttributeDef = { @@ -46,7 +47,6 @@ type CredentialConfigurationsSupported = { // ---- Static Lists (as requested) ---- const STATIC_CREDENTIAL_ALGS = ['ES256', 'EdDSA'] as const; const STATIC_BINDING_METHODS = ['did:key'] as const; -const DOCTYPE = 'org.iso.18013.5.1'; // for mso_mdoc format const MSO_MDOC = 'mso_mdoc'; // alternative format value // Safe coercion helpers @@ -95,6 +95,7 @@ export function buildCredentialConfigurationsSupported( templates: TemplateRowPrisma[], opts?: { vct?: string; + doctype?: string; scopeVct?: string; keyResolver?: (t: TemplateRowPrisma) => string; format?: string; @@ -117,14 +118,23 @@ export function buildCredentialConfigurationsSupported( // ---- dynamic format per row ---- // eslint-disable-next-line @typescript-eslint/no-explicit-any const rowFormat = (t as any).format ?? format; + const isMdoc = rowFormat === `${MSO_MDOC}`; const suffix = rowFormat === `${MSO_MDOC}` ? 'mdoc' : 'sdjwt'; // key: keep your keyResolver override; otherwise include suffix const key = 'function' === typeof opts?.keyResolver ? opts.keyResolver(t) : `${t.name}-${suffix}`; // vct/scope: vct only for non-mdoc; scope always uses suffix - const vct = opts?.vct ?? t.name; - const scopeBase = opts?.scopeVct ?? vct; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const rowDoctype: string | undefined = opts?.doctype ?? (t as any).doctype; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const rowVct: string = opts?.vct ?? (t as any).vct ?? t.name; + + if (isMdoc && !rowDoctype) { + throw new Error(`Template ${t.id}: doctype is required for mdoc format`); + } + + const scopeBase = opts?.scopeVct ?? rowVct; const scope = `openid4vc:credential:${scopeBase}-${suffix}`; const claims = Object.fromEntries( Object.entries(attrs).map(([claimName, def]) => { @@ -155,9 +165,7 @@ export function buildCredentialConfigurationsSupported( credential_signing_alg_values_supported: [...STATIC_CREDENTIAL_ALGS], cryptographic_binding_methods_supported: [...STATIC_BINDING_METHODS], display, - ...(rowFormat === `${MSO_MDOC}` - ? { doctype: `${DOCTYPE}` } // static for mdoc - : { vct }) // keep vct only for non-mdoc + ...(isMdoc ? { doctype: rowDoctype as string } : { vct: rowVct }) }; } @@ -206,7 +214,7 @@ function isDisplayArray(x: unknown): x is DisplayItem[] { * Build issuer metadata payload from issuer row + credential configurations. * * @param credentialConfigurations Object with credentialConfigurationsSupported (from your template builder) - * @param oidcIssuer OIDC issuer row (uses publicIssuerId and metadata -> display) + * @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 @@ -242,3 +250,20 @@ export function extractTemplateIds(offer: CreateOidcCredentialOffer): string[] { 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()); +} diff --git a/apps/oid4vc-issuance/src/main.ts b/apps/oid4vc-issuance/src/main.ts new file mode 100644 index 000000000..3c6933fc9 --- /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.ISSUANCE_SERVICE, process.env.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..7d9f476f7 --- /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 { CredentialOfferWebhookPayload } 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: { + issuerId: string; + orgId: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + }): Promise { + const { issuerId, orgId } = payload; + return this.oid4vcIssuanceService.oidcIssuerGetById(issuerId, 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; + issuerId: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + }): Promise { + const { orgId, userDetails, issuerId } = payload; + return this.oid4vcIssuanceService.deleteOidcIssuer(orgId, userDetails, issuerId); + } + + @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: CredentialOfferWebhookPayload): 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..6c12475fb --- /dev/null +++ b/apps/oid4vc-issuance/src/oid4vc-issuance.repository.ts @@ -0,0 +1,278 @@ +/* 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'; + +@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): Promise { + try { + const { credentialOfferId, state, offerId, contextCorrelationId, orgId } = credentialPayload; + const credentialDetails = await this.prisma.oid4vc_credentials.upsert({ + where: { + offerId + }, + update: { + lastChangedBy: orgId, + credentialOfferId, + contextCorrelationId, + state + }, + create: { + lastChangedBy: orgId, + createdBy: orgId, + state, + orgId, + credentialOfferId, + offerId, + contextCorrelationId + } + }); + + return credentialDetails; + } catch (error) { + this.logger.error(`Error in get storeOidcCredentialDetails: ${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 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; + } + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async addOidcIssuerDetails(issuerMetadata: IssuerMetadata, issuerProfileJson): Promise { + try { + const { publicIssuerId, createdById, orgAgentId, batchCredentialIssuanceSize } = issuerMetadata; + const oidcIssuerDetails = await this.prisma.oidc_issuer.create({ + data: { + metadata: issuerProfileJson, + publicIssuerId, + createdBy: createdById, + orgAgentId, + batchCredentialIssuanceSize + } + }); + + 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 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/issuance/src/oidc-issuance.service.ts b/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts similarity index 51% rename from apps/issuance/src/oidc-issuance.service.ts rename to apps/oid4vc-issuance/src/oid4vc-issuance.service.ts index da743deb6..afdf46be6 100644 --- a/apps/issuance/src/oidc-issuance.service.ts +++ b/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts @@ -17,7 +17,7 @@ import { NotFoundException, Scope } from '@nestjs/common'; -import { IssuanceRepository } from './issuance.repository'; +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'; @@ -31,8 +31,8 @@ import { IssuerMetadata, IssuerResponse, IssuerUpdation -} from '../interfaces/oidc-issuance.interfaces'; -import { CreateCredentialTemplate, UpdateCredentialTemplate } from '../interfaces/oidc-template.interface'; +} from '../interfaces/oid4vc-issuance.interfaces'; +import { CreateCredentialTemplate, UpdateCredentialTemplate } from '../interfaces/oid4vc-template.interfaces'; import { accessTokenSignerKeyType, batchCredentialIssuanceDefault, @@ -42,15 +42,21 @@ import { import { buildCredentialConfigurationsSupported, buildIssuerPayload, - extractTemplateIds + encodeIssuerPublicId, + extractTemplateIds, + normalizeJson } from '../libs/helpers/issuer.metadata'; import { CreateOidcCredentialOffer, + GetAllCredentialOffer, SignerMethodOption, - SignerOption -} from '../interfaces/oidc-issuer-sessions.interfaces'; -import { BadRequestErrorDto } from 'apps/api-gateway/src/dtos/bad-request-error.dto'; -import { buildCredentialOfferPayload, CredentialOfferPayload } from '../libs/helpers/credential-sessions.builder'; + UpdateCredentialRequest +} from '../interfaces/oid4vc-issuer-sessions.interfaces'; +import { + buildCredentialOfferPayload, + buildCredentialOfferUrl, + CredentialOfferPayload +} from '../libs/helpers/credential-sessions.builder'; type CredentialDisplayItem = { logo?: { uri: string; alt_text?: string }; @@ -63,22 +69,22 @@ type Appearance = { display: CredentialDisplayItem[]; }; @Injectable() -export class OIDCIssuanceService { +export class Oid4vcIssuanceService { private readonly logger = new Logger('IssueCredentialService'); constructor( @Inject('NATS_CLIENT') private readonly issuanceServiceProxy: ClientProxy, - private readonly issuanceRepository: IssuanceRepository + private readonly oid4vcIssuanceRepository: Oid4vcIssuanceRepository ) {} async oidcIssuerCreate(issuerCreation: IssuerCreation, orgId: string, userDetails: user): Promise { try { const { issuerId, batchCredentialIssuanceSize } = issuerCreation; - const agentDetails = await this.issuanceRepository.getAgentEndPoint(orgId); + 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.issuanceRepository.getOrgAgentType(orgAgentTypeId); + const orgAgentType = await this.oid4vcIssuanceRepository.getOrgAgentType(orgAgentTypeId); if (!orgAgentType) { throw new NotFoundException(ResponseMessages.issuance.error.orgAgentTypeNotFound); } @@ -86,7 +92,7 @@ export class OIDCIssuanceService { const issuerInitialConfig: IssuerInitialConfig = { issuerId, display: issuerCreation?.display || {}, - authorizationServerConfigs: issuerCreation?.authorizationServerConfigs || {}, + authorizationServerConfigs: issuerCreation?.authorizationServerConfigs || undefined, accessTokenSignerKeyType, dpopSigningAlgValuesSupported, batchCredentialIssuance: { @@ -119,13 +125,13 @@ export class OIDCIssuanceService { orgAgentId, batchCredentialIssuanceSize: issuerCreation?.batchCredentialIssuanceSize }; - const addOidcIssuerDetails = await this.issuanceRepository.addOidcIssuerDetails( + const addOidcIssuerDetails = await this.oid4vcIssuanceRepository.addOidcIssuerDetails( issuerMetadata, issuerCreation?.display ); if (!addOidcIssuerDetails) { - throw new InternalServerErrorException('Error in adding OIDC Issuer details in DB'); + throw new InternalServerErrorException('Error in adding OID4VC Issuer details in DB'); } return addOidcIssuerDetails; } catch (error) { @@ -136,27 +142,29 @@ export class OIDCIssuanceService { async oidcIssuerUpdate(issuerUpdationConfig: IssuerUpdation, orgId: string, userDetails: user): Promise { try { - const getIssuerDetails = await this.issuanceRepository.getOidcIssuerDetailsById(issuerUpdationConfig.issuerId); + const getIssuerDetails = await this.oid4vcIssuanceRepository.getOidcIssuerDetailsById( + issuerUpdationConfig.issuerId + ); if (!getIssuerDetails) { throw new NotFoundException(ResponseMessages.oidcIssuer.error.notFound); } - const agentDetails = await this.issuanceRepository.getAgentEndPoint(orgId); + 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.issuanceRepository.getOrgAgentType(orgAgentTypeId); + const { agentEndPoint, orgAgentTypeId } = agentDetails; + const orgAgentType = await this.oid4vcIssuanceRepository.getOrgAgentType(orgAgentTypeId); if (!orgAgentType) { throw new NotFoundException(ResponseMessages.issuance.error.orgAgentTypeNotFound); } - const addOidcIssuerDetails = await this.issuanceRepository.updateOidcIssuerDetails( + const addOidcIssuerDetails = await this.oid4vcIssuanceRepository.updateOidcIssuerDetails( userDetails.id, issuerUpdationConfig ); if (!addOidcIssuerDetails) { - throw new InternalServerErrorException('Error in updating OIDC Issuer details in DB'); + throw new InternalServerErrorException('Error in updating OID4VC Issuer details in DB'); } const url = await getAgentUrl( @@ -165,6 +173,7 @@ export class OIDCIssuanceService { 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( @@ -181,86 +190,96 @@ export class OIDCIssuanceService { async oidcIssuerGetById(issuerId: string, orgId: string): Promise { try { - const getIssuerDetails = await this.issuanceRepository.getOidcIssuerDetailsById(issuerId); + const getIssuerDetails = await this.oid4vcIssuanceRepository.getOidcIssuerDetailsById(issuerId); + console.log('This is the getIssuerDetails:', JSON.stringify(getIssuerDetails, null, 2)); if (!getIssuerDetails && getIssuerDetails.publicIssuerId) { throw new NotFoundException(ResponseMessages.oidcIssuer.error.notFound); } - const agentDetails = await this.issuanceRepository.getAgentEndPoint(orgId); + const agentDetails = await this.oid4vcIssuanceRepository.getAgentEndPoint(orgId); if (!agentDetails) { throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); } - const url = await getAgentUrl( - agentDetails?.agentEndPoint, - CommonConstants.OIDC_ISSUER_BY_ID, - getIssuerDetails?.publicIssuerId - ); + console.log('This is the agentDetails:', getIssuerDetails?.publicIssuerId); + const encodedId = encodeIssuerPublicId(getIssuerDetails?.publicIssuerId); + const url = await getAgentUrl(agentDetails?.agentEndPoint, CommonConstants.OIDC_ISSUER_BY_ID, encodedId); + console.log('This is the oidcIssuerGetById url:', url); const issuerDetailsRaw = await this._oidcGetIssuerById(url, orgId); - + console.log('This is the issuerDetailsRaw:', JSON.stringify(issuerDetailsRaw, null, 2)); if (!issuerDetailsRaw) { throw new InternalServerErrorException(`Error from agent while getting issuer`); } - - // response is a string → parse it into IssuerResponse const issuerDetails = { - response: JSON.parse(issuerDetailsRaw.response) as IssuerResponse + response: normalizeJson(issuerDetailsRaw.response) }; return issuerDetails.response; } catch (error) { - this.logger.error(`[oidcIssuerGetById] - error in oidcIssuerGetById issuance records: ${JSON.stringify(error)}`); - throw new RpcException(error?.response ?? 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 { + async oidcIssuers(orgId: string): Promise { try { - const agentDetails = await this.issuanceRepository.getAgentEndPoint(orgId); - if (!agentDetails) { + const agentDetails = await this.oid4vcIssuanceRepository.getAgentEndPoint(orgId); + if (!agentDetails?.agentEndPoint) { throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); } - const url = await getAgentUrl(agentDetails?.agentEndPoint, CommonConstants.OIDC_GET_ALL_ISSUERS); + + const url = await getAgentUrl(agentDetails.agentEndPoint, CommonConstants.OIDC_GET_ALL_ISSUERS); const issuersDetails = await this._oidcGetIssuers(url, orgId); - if (!issuersDetails) { - throw new InternalServerErrorException(`Error from agent while oidcIssuers`); + 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[]); - return issuersDetails; - } catch (error) { - this.logger.error(`[oidcIssuers] - error in oidcIssuers issuance records: ${JSON.stringify(error)}`); + if (!Array.isArray(response)) { + throw new InternalServerErrorException('Invalid issuer payload from agent'); + } + return response; + } 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, issuerId: string) { try { - const deleteOidcIssuer = await this.issuanceRepository.deleteOidcIssuer(issuerId); + const deleteOidcIssuer = await this.oid4vcIssuanceRepository.deleteOidcIssuer(issuerId); if (!deleteOidcIssuer) { - throw new NotFoundException(ResponseMessages.oidcTemplate.error.deleteTemplate); + throw new NotFoundException(ResponseMessages.oidcIssuer.error.deleteFailed); } - const issuerRecordId = await this.oidcIssuerGetById(issuerId, orgId); if (!issuerRecordId.id) { throw new NotFoundException(ResponseMessages.oidcIssuer.error.notFound); } - const agentDetails = await this.issuanceRepository.getAgentEndPoint(orgId); + const agentDetails = await this.oid4vcIssuanceRepository.getAgentEndPoint(orgId); if (!agentDetails) { throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); } const { agentEndPoint } = agentDetails; - const issuerDetails = await this.issuanceRepository.getOidcIssuerDetailsById(issuerId); - if (!issuerDetails) { - throw new NotFoundException(ResponseMessages.oidcTemplate.error.issuerDetailsNotFound); - } 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); } return deleteOidcIssuer; } catch (error) { - this.logger.error(`[deleteTemplate] - error: ${JSON.stringify(error)}`); - throw new RpcException(error.response ?? error); + if ('PrismaClientKnownRequestError' === error.name) { + throw new BadRequestException(error.meta?.cause ?? ResponseMessages.oidcIssuer.error.deleteFailed); + } + throw new Error(error.response ? error.response : error); } } @@ -270,8 +289,9 @@ export class OIDCIssuanceService { issuerId: string ): Promise { try { - const { name, description, format, canBeRevoked, attributes, appearance, signerOption } = CredentialTemplate; - const checkNameExist = await this.issuanceRepository.getTemplateByNameForIssuer(name, issuerId); + const { name, description, format, canBeRevoked, attributes, appearance, signerOption, vct, doctype } = + CredentialTemplate; + const checkNameExist = await this.oid4vcIssuanceRepository.getTemplateByNameForIssuer(name, issuerId); if (0 < checkNameExist.length) { throw new ConflictException(ResponseMessages.oidcTemplate.error.templateNameAlreadyExist); } @@ -286,17 +306,24 @@ export class OIDCIssuanceService { signerOption }; // Persist in DB - const createdTemplate = await this.issuanceRepository.createTemplate(issuerId, metadata); + const createdTemplate = await this.oid4vcIssuanceRepository.createTemplate(issuerId, metadata); if (!createdTemplate) { throw new InternalServerErrorException(ResponseMessages.oidcTemplate.error.createFailed); } - const issuerTemplateConfig = await this.buildOidcIssuerConfig(issuerId); - const agentDetails = await this.issuanceRepository.getAgentEndPoint(orgId); + let opts = {}; + if (vct) { + opts = { ...opts, vct }; + } + if (doctype) { + opts = { ...opts, doctype }; + } + const issuerTemplateConfig = await this.buildOidcIssuerConfig(issuerId, opts); + const agentDetails = await this.oid4vcIssuanceRepository.getAgentEndPoint(orgId); if (!agentDetails) { throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); } const { agentEndPoint } = agentDetails; - const issuerDetails = await this.issuanceRepository.getOidcIssuerDetailsById(issuerId); + const issuerDetails = await this.oid4vcIssuanceRepository.getOidcIssuerDetailsById(issuerId); if (!issuerDetails) { throw new NotFoundException(ResponseMessages.oidcTemplate.error.issuerDetailsNotFound); } @@ -319,12 +346,12 @@ export class OIDCIssuanceService { issuerId: string ): Promise { try { - const template = await this.issuanceRepository.getTemplateById(templateId); + const template = await this.oid4vcIssuanceRepository.getTemplateById(templateId); if (!template) { throw new NotFoundException(ResponseMessages.oidcTemplate.error.notFound); } if (updateCredentialTemplate.name) { - const checkNameExist = await this.issuanceRepository.getTemplateByNameForIssuer( + const checkNameExist = await this.oid4vcIssuanceRepository.getTemplateByNameForIssuer( updateCredentialTemplate.name, issuerId ); @@ -348,19 +375,19 @@ export class OIDCIssuanceService { ...(issuerId ? { issuerId } : {}) }; - const updatedTemplate = await this.issuanceRepository.updateTemplate(templateId, payload); + const updatedTemplate = await this.oid4vcIssuanceRepository.updateTemplate(templateId, payload); - const templates = await this.issuanceRepository.getTemplatesByIssuerId(issuerId); + 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.issuanceRepository.getAgentEndPoint(orgId); + const agentDetails = await this.oid4vcIssuanceRepository.getAgentEndPoint(orgId); if (!agentDetails) { throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); } const { agentEndPoint } = agentDetails; - const issuerDetails = await this.issuanceRepository.getOidcIssuerDetailsById(issuerId); + const issuerDetails = await this.oid4vcIssuanceRepository.getOidcIssuerDetailsById(issuerId); if (!issuerDetails) { throw new NotFoundException(ResponseMessages.oidcTemplate.error.issuerDetailsNotFound); } @@ -381,22 +408,22 @@ export class OIDCIssuanceService { // 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.issuanceRepository.getTemplateById(templateId); + const template = await this.oid4vcIssuanceRepository.getTemplateById(templateId); if (!template) { throw new NotFoundException(ResponseMessages.oidcTemplate.error.notFound); } - const deleteTemplate = await this.issuanceRepository.deleteTemplate(templateId); + 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.issuanceRepository.getAgentEndPoint(orgId); + const agentDetails = await this.oid4vcIssuanceRepository.getAgentEndPoint(orgId); if (!agentDetails) { throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); } const { agentEndPoint } = agentDetails; - const issuerDetails = await this.issuanceRepository.getOidcIssuerDetailsById(issuerId); + const issuerDetails = await this.oid4vcIssuanceRepository.getOidcIssuerDetailsById(issuerId); if (!issuerDetails) { throw new NotFoundException(ResponseMessages.oidcTemplate.error.issuerDetailsNotFound); } @@ -413,15 +440,16 @@ export class OIDCIssuanceService { } } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - async findByIdTemplate(templateId: string, orgId: string, userDetails: user, issuerId: string): Promise { + async findByIdTemplate(templateId: string, orgId: string): Promise { try { - const template = await this.issuanceRepository.getTemplateById(templateId); + 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 { message: ResponseMessages.oidcTemplate.success.fetch, data: template }; + return template; } catch (error) { this.logger.error(`[findByIdTemplate] - error: ${JSON.stringify(error)}`); throw new RpcException(error.response ?? error); @@ -439,26 +467,36 @@ export class OIDCIssuanceService { if (!filterTemplateIds) { throw new BadRequestException('Please provide a valid id'); } - const getAllOfferTemplates = await this.issuanceRepository.getTemplateByIds(filterTemplateIds, issuerId); + const getAllOfferTemplates = await this.oid4vcIssuanceRepository.getTemplateByIds(filterTemplateIds, issuerId); if (!getAllOfferTemplates) { throw new NotFoundException('No templates found for the issuer'); } - const agentDetails = await this.issuanceRepository.getAgentEndPoint(orgId); + const agentDetails = await this.oid4vcIssuanceRepository.getAgentEndPoint(orgId); //TDOD: signerOption should be under credentials change this with x509 support - const signerOption: SignerOption = { - method: SignerMethodOption.DID, - did: agentDetails.orgDid - }; + + //TDOD: signerOption should be under credentials change this with x509 support + const signerOptions = []; + getAllOfferTemplates.forEach((template) => { + if (template.signerOption === SignerMethodOption.DID) { + signerOptions.push({ + method: SignerMethodOption.DID, + did: agentDetails.orgDid + }); + } + }); + //TODO: Implement x509 support and discuss with team const buildOidcCredentialOffer: CredentialOfferPayload = buildCredentialOfferPayload( createOidcCredentialOffer, getAllOfferTemplates, - signerOption + signerOptions ); + console.log('This is the buildOidcCredentialOffer:', JSON.stringify(buildOidcCredentialOffer, null, 2)); + if (!buildOidcCredentialOffer) { - throw new BadRequestException('Error while creating oidc credential offer'); + throw new BadRequestException('Error while creating oid4vc credential offer'); } - const issuerDetails = await this.issuanceRepository.getOidcIssuerDetailsById(issuerId); + const issuerDetails = await this.oid4vcIssuanceRepository.getOidcIssuerDetailsById(issuerId); if (!issuerDetails) { throw new NotFoundException(ResponseMessages.oidcTemplate.error.issuerDetailsNotFound); } @@ -471,7 +509,7 @@ export class OIDCIssuanceService { const createCredentialOfferOnAgent = await this._oidcCreateCredentialOffer(buildOidcCredentialOffer, url, orgId); if (!createCredentialOfferOnAgent) { - throw new NotFoundException(ResponseMessages.oidcIssuerSession.error.errroCreateOffer); + throw new NotFoundException(ResponseMessages.oidcIssuerSession.error.errorCreateOffer); } return createCredentialOfferOnAgent.response; @@ -480,13 +518,153 @@ export class OIDCIssuanceService { 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 + ); + console.log('This is the updateCredentialOfferOnAgent:', JSON.stringify(updateCredentialOfferOnAgent)); + if (!updateCredentialOfferOnAgent) { + throw new NotFoundException(ResponseMessages.oidcIssuerSession.error.errorUpdateOffer); + } + + return updateCredentialOfferOnAgent.response; + } catch (error) { + this.logger.error(`[createOidcCredentialOffer] - error: ${JSON.stringify(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._oidcGetCredentialOfferById(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) { + async buildOidcIssuerConfig(issuerId: string, configMetadata?) { try { - const issuerDetails = await this.issuanceRepository.getOidcIssuerDetailsById(issuerId); - const templates = await this.issuanceRepository.getTemplatesByIssuerId(issuerId); + const issuerDetails = await this.oid4vcIssuanceRepository.getOidcIssuerDetailsById(issuerId); + const templates = await this.oid4vcIssuanceRepository.getTemplatesByIssuerId(issuerId); - const credentialConfigurationsSupported = buildCredentialConfigurationsSupported(templates); + const credentialConfigurationsSupported = buildCredentialConfigurationsSupported(templates, configMetadata); return buildIssuerPayload(credentialConfigurationsSupported, issuerDetails); } catch (error) { @@ -495,11 +673,12 @@ export class OIDCIssuanceService { } } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - async findAllTemplate(orgId: string, userDetails: user, issuerId: string): Promise { + async findAllTemplate(orgId: string, issuerId: string): Promise { try { - const templates = await this.issuanceRepository.getTemplatesByIssuerId(issuerId); - return { message: ResponseMessages.oidcTemplate.success.fetch, data: templates }; + 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); @@ -509,67 +688,125 @@ export class OIDCIssuanceService { // eslint-disable-next-line @typescript-eslint/no-explicit-any async _createOIDCIssuer(issuerCreation, url: string, orgId: string): Promise { try { - const pattern = { cmd: 'agent-create-oidc-issuer' }; + 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 OIDC Issuer : ${JSON.stringify(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-oidc-template' }; + 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 OIDC Template : ${JSON.stringify(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-oidc-issuer' }; + 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 OIDC Template : ${JSON.stringify(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: 'oidc-get-issuer-by-id' }; + 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 oidc get issuer by id : ${JSON.stringify(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: 'oidc-get-issuers' }; + 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 oidc get issuers : ${JSON.stringify(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-oidc-create-credential-offer' }; + 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 oidc create credential offer : ${JSON.stringify(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; } @@ -607,7 +844,7 @@ export class OIDCIssuanceService { } async getAgentEndpoint(orgId: string): Promise { - const agentDetails = await this.issuanceRepository.getAgentEndPoint(orgId); + const agentDetails = await this.oid4vcIssuanceRepository.getAgentEndPoint(orgId); if (!agentDetails) { throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); @@ -619,4 +856,34 @@ export class OIDCIssuanceService { return agentDetails.agentEndPoint; } + + async storeOidcCredentialWebhook(CredentialOfferWebhookPayload): Promise { + try { + console.log('Storing OID4VC Credential Webhook:', CredentialOfferWebhookPayload); + const { credentialOfferId, state, id, contextCorrelationId } = CredentialOfferWebhookPayload; + let orgId: string; + if ('default' !== contextCorrelationId) { + const getOrganizationId = await this.oid4vcIssuanceRepository.getOrganizationByTenantId(contextCorrelationId); + orgId = getOrganizationId?.orgId; + } else { + orgId = id; + } + + const credentialPayload = { + orgId, + offerId: id, + credentialOfferId, + state, + contextCorrelationId + }; + + const agentDetails = await this.oid4vcIssuanceRepository.storeOidcCredentialDetails(credentialPayload); + return agentDetails; + } catch (error) { + this.logger.error( + `[getIssueCredentialsbyCredentialRecordId] - error in get credentials : ${JSON.stringify(error)}` + ); + throw new RpcException(error.response ? error.response : 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/libs/common/src/common.constant.ts b/libs/common/src/common.constant.ts index da939394d..cd28289d3 100644 --- a/libs/common/src/common.constant.ts +++ b/libs/common/src/common.constant.ts @@ -117,12 +117,14 @@ export enum CommonConstants { // CREATE KEYS CREATE_POLYGON_SECP256k1_KEY = '/polygon/create-keys', - // OIDC URLs + // 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_ISSUES = '/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', // Nested attribute separator NESTED_ATTRIBUTE_SEPARATOR = '~', @@ -371,6 +373,7 @@ export enum CommonConstants { NOTIFICATION_SERVICE = 'notification', GEO_LOCATION_SERVICE = 'geo-location', CLOUD_WALLET_SERVICE = 'cloud-wallet', + OIDC4VC_ISSUANCE_SERVICE = 'oid4vc-issuance', ACCEPT_OFFER = '/didcomm/credentials/accept-offer', SEED_LENGTH = 32, @@ -392,13 +395,17 @@ export enum CommonConstants { SEND_QUESTION = 'send-question', SEND_BASIC_MESSAGE = 'send-basic-message', - // OIDC - OIDC_ISSUER_CREATE = 'create-oidc-issuer', - OIDC_ISSUER_DELETE = 'delete-oidc-issuer', - OIDC_GET_ALL_ISSUERS = 'get-all-oidc-issuers', + // 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-oidc-template', - OIDC_ISSUER_SESSIONS_CREDENTIAL_OFFER = 'create-oidc-credential-offer' + 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' } 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 b1c70e012..5091239fc 100644 --- a/libs/common/src/common.utils.ts +++ b/libs/common/src/common.utils.ts @@ -102,7 +102,11 @@ export const getAgentUrl = async (agentEndPoint: string, urlFlag: string, paramI [ 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)] ]); const urlSuffix = agentUrlMap.get(urlFlag); 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 681751427..edae74634 100644 --- a/libs/common/src/response-messages/index.ts +++ b/libs/common/src/response-messages/index.ts @@ -499,34 +499,35 @@ export const ResponseMessages = { }, oidcIssuer: { success: { - create: 'OIDC issuer created successfully.', - update: 'OIDC issuer updated successfully.', - delete: 'OIDC issuer deleted successfully.', - fetch: 'OIDC issuer(s) fetched successfully.', + 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: 'OIDC issuer not found.', - invalidId: 'Invalid OIDC issuer ID.', - createFailed: 'Failed to create OIDC issuer.', - updateFailed: 'Failed to update OIDC issuer.', - deleteFailed: 'Failed to delete OIDC issuer.' + 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: 'OIDC template created successfully.', - update: 'OIDC template updated successfully.', - delete: 'OIDC template deleted successfully.', - fetch: 'OIDC template(s) fetched successfully.' + 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.' }, error: { - notFound: 'OIDC template not found.', - invalidId: 'Invalid OIDC template ID.', - createFailed: 'Failed to create OIDC template.', - updateFailed: 'Failed to update OIDC template.', - deleteFailed: 'Failed to delete OIDC template.', + 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.', @@ -535,14 +536,16 @@ export const ResponseMessages = { }, oidcIssuerSession: { success: { - create: 'OIDC Credential offer created successfully.' + 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: { - errroCreateOffer: 'Error while creating OIDC credential offer on agent.', - invalidId: 'Invalid OIDC issuer ID.', - createFailed: 'Failed to create OIDC issuer.', - updateFailed: 'Failed to update OIDC issuer.', - deleteFailed: 'Failed to delete OIDC issuer.' + 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.' } }, nats: { diff --git a/libs/enum/src/enum.ts b/libs/enum/src/enum.ts index 9dc8b31ca..787fc1f92 100644 --- a/libs/enum/src/enum.ts +++ b/libs/enum/src/enum.ts @@ -271,3 +271,16 @@ export enum ProviderType { KEYCLOAK = 'keycloak', SUPABASE = 'supabase' } + +export declare enum OpenId4VcIssuanceSessionState { + OfferCreated = 'OfferCreated', + OfferUriRetrieved = 'OfferUriRetrieved', + AuthorizationInitiated = 'AuthorizationInitiated', + AuthorizationGranted = 'AuthorizationGranted', + AccessTokenRequested = 'AccessTokenRequested', + AccessTokenCreated = 'AccessTokenCreated', + CredentialRequestReceived = 'CredentialRequestReceived', + CredentialsPartiallyIssued = 'CredentialsPartiallyIssued', + Completed = 'Completed', + Error = 'Error' +} diff --git a/libs/prisma-service/prisma/schema.prisma b/libs/prisma-service/prisma/schema.prisma index e544c8e94..74bf6189f 100644 --- a/libs/prisma-service/prisma/schema.prisma +++ b/libs/prisma-service/prisma/schema.prisma @@ -149,6 +149,7 @@ model organisation { agent_invitations agent_invitations[] credential_definition credential_definition[] file_upload file_upload[] + oid4vc_credentials oid4vc_credentials[] } model org_invitations { @@ -581,6 +582,21 @@ model oidc_issuer { @@index([orgAgentId]) } +model oid4vc_credentials { + id String @id @default(uuid()) @db.Uuid + orgId String @db.Uuid + offerId 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 + + organisation organisation @relation(fields: [orgId], references: [id]) +} + enum SignerOption { did x509 @@ -602,4 +618,5 @@ model credential_templates { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt signerOption SignerOption -} \ No newline at end of file +} + diff --git a/nest-cli.json b/nest-cli.json index ee0cc7122..819afa8b7 100644 --- a/nest-cli.json +++ b/nest-cli.json @@ -286,6 +286,15 @@ "compilerOptions": { "tsConfigPath": "libs/logger/tsconfig.lib.json" } + }, + "oid4vc-issuance": { + "type": "application", + "root": "apps/oid4vc-issuance", + "entryFile": "main", + "sourceRoot": "apps/oid4vc-issuance/src", + "compilerOptions": { + "tsConfigPath": "apps/oid4vc-issuance/tsconfig.app.json" + } } } -} +} \ No newline at end of file diff --git a/package.json b/package.json index 214141f89..8839ec129 100644 --- a/package.json +++ b/package.json @@ -205,4 +205,4 @@ "engines": { "node": ">=18" } -} +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 74a5fa904..d0da5851f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -104,7 +104,8 @@ "@credebl/context/*": [ "libs/context/src/*" ] - } + }, + "baseUrl": "./" }, "exclude": [ "node_modules",