From 1f64ba8c1a05c9e5425a61dbb98c330da5818f1d Mon Sep 17 00:00:00 2001 From: devwums Date: Tue, 23 Jun 2026 04:35:31 +0100 Subject: [PATCH] feat(did): complete DID document management and VC issuance endpoints - Add POST /api/did/register to anchor a DID with its public key and return a W3C DID document - Add GET /api/did/document/:did to resolve a W3C Core-compliant DID document - Add POST /api/did/credentials/issue to issue KYC verifiable credentials - Add PATCH /api/did/credentials/:id/revoke to revoke credentials - Expose buildW3cDocument helper in DidAuthService with @context, verificationMethod, authentication, and assertionMethod fields - Group controller endpoints by concern (document management, auth, credentials, ZKP) --- src/did/controllers/did.controller.ts | 89 ++++++++++++++++++++++++--- src/did/services/did-auth.service.ts | 71 ++++++++++++++++++--- 2 files changed, 144 insertions(+), 16 deletions(-) diff --git a/src/did/controllers/did.controller.ts b/src/did/controllers/did.controller.ts index af3f4cc..08907d5 100644 --- a/src/did/controllers/did.controller.ts +++ b/src/did/controllers/did.controller.ts @@ -2,6 +2,7 @@ import { Controller, Post, Get, + Patch, Body, Param, HttpException, @@ -29,6 +30,38 @@ export class DidController { private readonly vcRepo: Repository, ) {} + // ── DID Document Management ──────────────────────────────────────────────── + + @Post('register') + @ApiOperation({ summary: 'Register a new DID and anchor its DID document' }) + @ApiResponse({ status: 201, description: 'DID document created' }) + async registerDid( + @Body('did') did: string, + @Body('publicKey') publicKey: string, + ) { + if (!did || !publicKey) { + throw new HttpException( + 'did and publicKey are required', + HttpStatus.BAD_REQUEST, + ); + } + const document = await this.didAuthService.registerDid(did, publicKey); + return document; + } + + @Get('document/:did') + @ApiOperation({ summary: 'Resolve a W3C-compliant DID document' }) + @ApiResponse({ status: 200, description: 'W3C DID document' }) + async resolveDidDocument(@Param('did') did: string) { + const document = await this.didAuthService.resolveDidDocument(did); + if (!document) { + throw new HttpException('DID not found', HttpStatus.NOT_FOUND); + } + return document; + } + + // ── DID Authentication ───────────────────────────────────────────────────── + @Post('auth/challenge') @ApiOperation({ summary: 'Generate a cryptographic nonce challenge for DID authentication', @@ -58,18 +91,48 @@ export class DidController { did, signature, ); - return sessionToken; // Returns standard app token session + return sessionToken; + } + + // ── Verifiable Credentials ───────────────────────────────────────────────── + + @Post('credentials/issue') + @ApiOperation({ summary: 'Issue a KYC verifiable credential for a DID' }) + @ApiResponse({ status: 201, description: 'VC issued successfully' }) + async issueCredential( + @Body('userDid') userDid: string, + @Body('kycTier') kycTier: number, + @Body('fullName') fullName: string, + @Body('dateOfBirth') dateOfBirth: string, + ) { + if (!userDid || kycTier === undefined || !fullName || !dateOfBirth) { + throw new HttpException( + 'userDid, kycTier, fullName, and dateOfBirth are required', + HttpStatus.BAD_REQUEST, + ); + } + const vc = await this.vcIssuerService.issueKycCredential( + userDid, + kycTier, + fullName, + dateOfBirth, + ); + return { + id: vc.id, + did: vc.did, + issuerDid: vc.issuerDid, + credentialType: vc.credentialType, + status: vc.status, + issuedAt: vc.issuedAt, + }; } @Get('credentials/:did') @ApiOperation({ - summary: - 'Retrieve public privacy-preserving schemas for a particular DID profile', + summary: 'Retrieve public credential metadata for a DID (no PII exposed)', }) async getCredentials(@Param('did') did: string) { - // Only return non-PII attributes off the VCs (hiding the encrypted payloads) const vcs = await this.vcRepo.find({ where: { did } }); - return vcs.map((vc) => ({ id: vc.id, credentialType: vc.credentialType, @@ -79,15 +142,23 @@ export class DidController { })); } + @Patch('credentials/:id/revoke') + @ApiOperation({ summary: 'Revoke a verifiable credential by ID' }) + @ApiResponse({ status: 200, description: 'Credential revoked' }) + async revokeCredential(@Param('id') id: string) { + const revoked = await this.vcIssuerService.revokeCredential(id); + return { revoked, vcId: id, revokedAt: new Date().toISOString() }; + } + + // ── Zero-Knowledge Proofs ────────────────────────────────────────────────── + @Post('verify-proof') @ApiOperation({ - summary: - 'Third-party API for ZKP credential verification (KYC/AML Compliance Rules)', + summary: 'Verify a ZKP credential proof without revealing raw KYC data', }) @ApiResponse({ status: 200, - description: - 'Returns boolean validating the zero-knowledge proof constraint', + description: 'Returns boolean validating the zero-knowledge proof', }) async verifyZkp(@Body() payload: ZkProofPayload) { if (!payload.userDid || !payload.vcId || !payload.zkpProofSignature) { diff --git a/src/did/services/did-auth.service.ts b/src/did/services/did-auth.service.ts index 48707bd..92e4efd 100644 --- a/src/did/services/did-auth.service.ts +++ b/src/did/services/did-auth.service.ts @@ -1,4 +1,8 @@ -import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { + Injectable, + UnauthorizedException, + ConflictException, +} from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { DidDocument } from '../entities/did-document.entity'; @@ -12,20 +16,52 @@ export class DidAuthService { private readonly didDocRepo: Repository, ) {} + /** + * Register a new DID and anchor its W3C DID document. + * Returns a minimal W3C DID Core compliant document. + */ + async registerDid( + did: string, + publicKey: string, + ): Promise> { + const existing = await this.didDocRepo.findOne({ where: { did } }); + if (existing) { + throw new ConflictException(`DID '${did}' is already registered`); + } + + const doc = this.didDocRepo.create({ + did, + publicKey, + userId: crypto.randomUUID(), + }); + await this.didDocRepo.save(doc); + + return this.buildW3cDocument(doc); + } + + /** + * Resolve a DID to its W3C-compliant DID document. + */ + async resolveDidDocument( + did: string, + ): Promise | null> { + const doc = await this.didDocRepo.findOne({ where: { did } }); + if (!doc) return null; + return this.buildW3cDocument(doc); + } + async generateChallenge(did: string): Promise { let doc = await this.didDocRepo.findOne({ where: { did } }); - // Auto-onboard for demonstration if no doc exists (in reality, requires registration step) if (!doc) { doc = this.didDocRepo.create({ did, - userId: crypto.randomUUID(), // Mock linking to a real backend user + userId: crypto.randomUUID(), }); } const nonce = crypto.randomBytes(32).toString('hex'); doc.nonce = nonce; - // Set expiry 5 mins from now doc.nonceExpiry = new Date(Date.now() + 5 * 60 * 1000); await this.didDocRepo.save(doc); @@ -43,13 +79,11 @@ export class DidAuthService { } try { - // For did:ethr:0x123..., the address is the last part const address = did.split(':').pop(); if (!address) { throw new UnauthorizedException('Invalid DID format'); } - // We expect the user signed the raw nonce string const recoveredAddress = verifyMessage(doc.nonce, signature); if (recoveredAddress.toLowerCase() !== address.toLowerCase()) { @@ -61,7 +95,6 @@ export class DidAuthService { doc.nonceExpiry = null; await this.didDocRepo.save(doc); - // Return a pseudo session token for the standard auth system to consume const sessionToken = crypto.randomBytes(32).toString('hex'); return { message: 'DID Authentication successful', @@ -72,4 +105,28 @@ export class DidAuthService { throw new UnauthorizedException('Signature verification failed'); } } + + private buildW3cDocument(doc: DidDocument): Record { + return { + '@context': [ + 'https://www.w3.org/ns/did/v1', + 'https://w3id.org/security/suites/secp256k1-2019/v1', + ], + id: doc.did, + verificationMethod: doc.publicKey + ? [ + { + id: `${doc.did}#keys-1`, + type: 'EcdsaSecp256k1VerificationKey2019', + controller: doc.did, + publicKeyHex: doc.publicKey, + }, + ] + : [], + authentication: doc.publicKey ? [`${doc.did}#keys-1`] : [], + assertionMethod: doc.publicKey ? [`${doc.did}#keys-1`] : [], + created: doc.createdAt, + updated: doc.updatedAt, + }; + } }