Skip to content
Commits on Source (3)
......@@ -3,6 +3,13 @@
All notable changes to this project will be documented in this file. See
[Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.30.0](https://code.vereign.com/svdh/ocm-engine/compare/v1.29.0...v1.30.0) (2024-10-30)
### Features
* ocm did svdx generation for sender OP[#250](https://code.vereign.com/svdh/ocm-engine/issues/250) ([dcb9778](https://code.vereign.com/svdh/ocm-engine/commit/dcb97780f262ff18d74f391141d81277f19aa58a))
## [1.29.0](https://code.vereign.com/svdh/ocm-engine/compare/v1.28.0...v1.29.0) (2024-10-07)
......
FROM node:18.19.1 AS base
FROM node:18.19.1-buster-slim AS base
RUN apt update -y && apt install yarn python3 make build-essential -y
FROM base AS deps
......@@ -6,6 +6,7 @@ WORKDIR /app
COPY package.json .
COPY yarn.lock .
COPY .npmrc .
RUN yarn install --frozen-lockfile
......@@ -31,8 +32,7 @@ COPY --from=builder /app/dist .
COPY --from=builder /app/dist/apps/agent/package.json .
COPY --from=builder /app/dist/apps/agent/yarn.lock .
RUN yarn install --frozen-lockfile
COPY --from=deps /app/node_modules ./node_modules
# Expose required ports
EXPOSE 8080
......
......@@ -11,6 +11,8 @@ import {
ledgersSchema,
ipfsConfig,
ipfsSchema,
catalogConfig,
catalogSchema,
} from "@ocm-engine/config";
import Joi from "joi";
......@@ -19,6 +21,7 @@ const validationSchema = Joi.object({
auth: authSchema,
ledgers: ledgersSchema,
ipfs: ipfsSchema,
catalog: catalogSchema,
});
@Module({
......@@ -26,7 +29,7 @@ const validationSchema = Joi.object({
AskarDynamicModule.forRootAsync(),
ConfigModule.forRoot({
isGlobal: true,
load: [agentConfig, authConfig, ledgersConfig, ipfsConfig],
load: [agentConfig, authConfig, ledgersConfig, ipfsConfig, catalogConfig],
validationSchema,
}),
],
......
......@@ -48,6 +48,7 @@ import { anoncreds } from "@hyperledger/anoncreds-nodejs";
import { AskarModule } from "@credo-ts/askar";
import { ariesAskar } from "@hyperledger/aries-askar-nodejs";
import { Key as AskarKey, KeyAlgs } from "@hyperledger/aries-askar-shared";
import crypto from "crypto";
import {
catchError,
filter,
......@@ -203,6 +204,11 @@ export const generateDidWeb = async ({
console.log(JSON.stringify(didDocumentInstance.toJSON(), null, 2));
};
export const generateRandomDidSvdx = async (agent: Agent) => {
const seed = crypto.randomBytes(46).toString("hex");
return generateDidSvdx({ agent, seed });
};
export const generateDidSvdx = async ({
seed,
agent,
......@@ -235,6 +241,8 @@ export const generateDidSvdx = async ({
if (!didResult.didState.didDocument) {
throw new Error("Could not create did svdx");
}
return didResult;
};
export const generateDidFromKey = (key: Key): string => {
......
......@@ -17,6 +17,7 @@ import { AgentDidsService } from "../askar/services/agent.dids.service";
import { AgentJsonldService } from "../askar/services/agent.jsonld.service";
import { AgentOobService } from "../askar/services/agent.oob.service";
import { AgentProofsService } from "../askar/services/agent.proofs.service";
import { AgentOcmService } from "../askar/services/agent.ocm.service";
import {
CreateCredentialDefinitionRequestDto,
OfferCredentialRequestDto,
......@@ -35,6 +36,7 @@ import {
OfferJsonCredentialRequests,
SignJsonCredentialRequests,
DidRecordDto,
RequestSenderEmailVcDto,
} from "@ocm-engine/dtos";
import { AllExceptionsHandler } from "./exception.handler";
import { DidResolutionResult } from "@credo-ts/core";
......@@ -53,6 +55,7 @@ export class RestController {
private readonly agentJsonldService: AgentJsonldService,
private readonly agentOobService: AgentOobService,
private readonly agentProofsService: AgentProofsService,
private readonly agentOcmService: AgentOcmService,
) {}
@Get("/invitations")
......@@ -272,4 +275,9 @@ export class RestController {
async resolveDid(@Body() dto: IdReqDto): Promise<DidResolutionResult> {
return this.agentDidsService.resolve(dto.id);
}
@Post("/ocm/request-sender-email-vc")
async requestSenderEmailVC(@Body() dto: RequestSenderEmailVcDto) {
return this.agentOcmService.requestSenderEmailVC(dto);
}
}
......@@ -8,6 +8,8 @@ import { AgentDidsService } from "./services/agent.dids.service";
import { AgentJsonldService } from "./services/agent.jsonld.service";
import { AgentOobService } from "./services/agent.oob.service";
import { AgentProofsService } from "./services/agent.proofs.service";
import { AgentOcmService } from "./services/agent.ocm.service";
import { CatalogClient } from "./clients/catalog.client";
import { ConfigModule } from "@nestjs/config";
import { LedgersModule } from "@ocm-engine/ledgers";
......@@ -23,6 +25,8 @@ import { LedgersModule } from "@ocm-engine/ledgers";
AgentJsonldService,
AgentOobService,
AgentProofsService,
AgentOcmService,
CatalogClient,
AskarService,
],
exports: [
......@@ -34,6 +38,8 @@ import { LedgersModule } from "@ocm-engine/ledgers";
AgentJsonldService,
AgentOobService,
AgentProofsService,
AgentOcmService,
CatalogClient,
AskarService,
],
})
......
import { Injectable, Logger } from "@nestjs/common";
import { IConfCatalog } from "@ocm-engine/config";
import { ConfigService } from "@nestjs/config";
import axios, { AxiosError } from "axios";
@Injectable()
export class CatalogClient {
private readonly logger = new Logger(CatalogClient.name);
private catalogConfig: IConfCatalog;
constructor(private readonly configService: ConfigService) {
this.catalogConfig = this.configService.get<IConfCatalog>("catalog")!;
}
searchAccount = async (email: string): Promise<string | null> => {
this.logger.debug(`searchAccount`, email);
try {
const response = await axios.post(
`${this.catalogConfig.catalogUrl}/v1/accounts/search`,
{
email: email,
},
);
return response.data.did;
} catch (e) {
this.logger.debug("searchAccount failed", e);
if (e instanceof AxiosError && e.status === 404) {
return null;
} else {
throw e;
}
}
};
createAccount = async (email: string, didSvdx: string): Promise<void> => {
this.logger.debug(`createAccount`, email);
try {
await axios.post(`${this.catalogConfig.catalogUrl}/v1/accounts`, {
email: email,
did: didSvdx,
});
} catch (e) {
this.logger.debug("createAccount failed", e);
throw e;
}
};
}
import { Injectable, Logger } from "@nestjs/common";
import { EntityNotFoundError, RequestSenderEmailVcDto } from "@ocm-engine/dtos";
import { CatalogClient } from "../clients/catalog.client";
import { generateRandomDidSvdx } from "../../agent.utils";
import { AskarService } from "./askar.service";
import { IConfCatalog } from "@ocm-engine/config";
import { ConfigService } from "@nestjs/config";
import {
ClaimFormat,
JsonTransformer,
W3cCredential,
W3cCredentialService,
} from "@credo-ts/core";
import { uuid } from "@credo-ts/core/build/utils/uuid";
@Injectable()
export class AgentOcmService {
private readonly logger = new Logger(AgentOcmService.name);
private activated = false;
constructor(
private readonly askar: AskarService,
private readonly catalogClient: CatalogClient,
private readonly configService: ConfigService,
) {
const catalogConfig = this.configService.get<IConfCatalog>("catalog")!;
this.activated = !!catalogConfig.catalogUrl;
}
requestSenderEmailVC = async (
dto: RequestSenderEmailVcDto,
): Promise<{
email: string;
did: string;
vc: object;
}> => {
this.ensureActivated();
this.logger.debug(`requestSenderEmailVC`, dto.email);
const didSvdx = await this.catalogClient.searchAccount(dto.email);
if (didSvdx) {
const record = await this.askar.agent.genericRecords.findById(
"svdx_vc_" + dto.email,
);
if (!record) {
// todo regenerate in that case?
throw new Error(
`VC not found in storage, even though the catalog has some data about it. did: ${didSvdx}, email: ${dto.email}`,
);
}
const vc = record.content;
return {
email: dto.email,
did: didSvdx,
vc,
};
}
const newDidSvdx = await generateRandomDidSvdx(this.askar.agent);
if (!newDidSvdx.didState.did) {
throw new Error("Svdx did generation failed");
}
const didRecords = await this.askar.agent.dids.getCreatedDids({
method: "svdx",
did: newDidSvdx.didState.did,
});
if (!didRecords.length) {
throw new EntityNotFoundError("Agent does not have did:svdx");
}
const svdxDid = didRecords[0];
const verificationMethodList =
svdxDid.didDocument?.verificationMethod || [];
if (!verificationMethodList.length) {
throw new EntityNotFoundError(
"DidDocument does not exists or contains no verification methods",
);
}
const verificationMethod = verificationMethodList[0];
const credentialRecordId = uuid();
const credential = JsonTransformer.fromJSON(
{
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://w3id.org/security/suites/jws-2020/v1",
"https://www.vereign.com/.well-known/svdx-account-schema",
],
type: ["VerifiableCredential"],
id: svdxDid.did + "?uuid=" + credentialRecordId,
issuer: svdxDid.did,
issuanceDate: new Date().toISOString(),
credentialSubject: {
type: "svdx:SvdxAccount",
"svdx:email": dto.email,
"svdx:did": newDidSvdx.didState.did,
},
},
W3cCredential,
);
const w3cServ =
this.askar.agent.context.dependencyManager.resolve(W3cCredentialService);
const vc = await w3cServ.signCredential(this.askar.agent.context, {
format: ClaimFormat.LdpVc,
credential,
proofType: "Ed25519Signature2018",
verificationMethod: verificationMethod.id,
});
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const jsonVC = vc.toJson();
// Save credential
await this.askar.agent.genericRecords.save({
id: "svdx_vc_" + dto.email,
content: jsonVC,
});
await this.catalogClient.createAccount(dto.email, svdxDid.did);
return {
email: dto.email,
did: svdxDid.did,
vc: jsonVC,
};
};
private ensureActivated() {
if (!this.activated) {
throw new Error(
"Please provide the catalog URL on startup to activate this endpoint.",
);
}
}
}
import { registerAs } from "@nestjs/config";
import * as process from "process";
import { IConfCatalog } from "../interfaces/catalog.config.interface";
export const catalogConfig = registerAs(
"catalog",
(): IConfCatalog => ({
catalogUrl: process.env["CATALOG_URL"]!,
}),
);
......@@ -2,13 +2,16 @@ export * from "./config/agent.config";
export * from "./config/auth.config";
export * from "./config/ledgers.config";
export * from "./config/ipfs.config";
export * from "./config/catalog.config";
export * from "./interfaces/agent.config.interface";
export * from "./interfaces/auth.config.interface";
export * from "./interfaces/ledgers.config.interface";
export * from "./interfaces/ipfs.config.interface";
export * from "./interfaces/catalog.config.interface";
export * from "./schemas/agent.schema";
export * from "./schemas/auth.schema";
export * from "./schemas/ledgers.schema";
export * from "./schemas/ipfs.schema";
export * from "./schemas/catalog.schema";
export interface IConfCatalog {
catalogUrl: string;
}
import Joi from "joi";
export const catalogSchema = Joi.object({
CATALOG_URL: Joi.string(),
});
import { IsNotEmpty, IsEmail } from "class-validator";
export class RequestSenderEmailVcDto {
@IsEmail()
@IsNotEmpty()
email: string;
}
......@@ -26,6 +26,7 @@ export * from "./dtos/requests/offer.credential.request.dto";
export * from "./dtos/requests/request.proof.dto";
export * from "./dtos/requests/make.basic.message.request.dto";
export * from "./dtos/requests/create.invitation.request.dto";
export * from "./dtos/requests/request.sender.email.vc.dto";
export * from "./dtos/credo/w3c/credential/w3c.credential.dto";
export * from "./dtos/credo/w3c/credential/w3c.credential-schema.dto";
......
......@@ -4041,9 +4041,9 @@
"@types/node" "*"
 
"@types/node@*":
version "22.7.4"
resolved "https://registry.npmjs.org/@types/node/-/node-22.7.4.tgz#e35d6f48dca3255ce44256ddc05dee1c23353fcc"
integrity sha512-y+NPi1rFzDs1NdQHHToqeiX2TIS79SWEAw9GYhkkx8bD0ChpfqC+n2j5OXOCpzfojBEBt6DnEnnG9MY0zk1XLg==
version "22.7.5"
resolved "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz#cfde981727a7ab3611a481510b473ae54442b92b"
integrity sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==
dependencies:
undici-types "~6.19.2"
 
......@@ -6910,9 +6910,9 @@ ejs@^3.1.10, ejs@^3.1.7:
jake "^10.8.5"
 
electron-to-chromium@^1.5.28:
version "1.5.32"
resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.32.tgz#4a05ee78e29e240aabaf73a67ce9fe73f52e1bc7"
integrity sha512-M+7ph0VGBQqqpTT2YrabjNKSQ2fEl9PVx6AK3N558gDH9NO8O6XN9SXXFWRo9u9PbEg/bWq+tjXQr+eXmxubCw==
version "1.5.33"
resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.33.tgz#8f64698661240e70fdbc4b032e6085e391f05e09"
integrity sha512-+cYTcFB1QqD4j4LegwLfpCNxifb6dDFUAwk6RsLusCwIaZI6or2f+q8rs5tTB2YC53HhOlIbEaqHMAAC8IOIwA==
 
emittery@^0.13.1:
version "0.13.1"
......@@ -12868,9 +12868,9 @@ rc-select@~14.15.0, rc-select@~14.15.2:
rc-virtual-list "^3.5.2"
 
rc-slider@~11.1.6:
version "11.1.6"
resolved "https://registry.npmjs.org/rc-slider/-/rc-slider-11.1.6.tgz#7ce762ff76e5ea8f4a54431ebe582df5c498629b"
integrity sha512-LACAaXM0hi+4x4ErDGZLy7weIQwmBIVbIgPE+eDHiHkyzMvKjWHraCG8/B22Y/tCQUPAsP02wBhKhth7mH2PIw==
version "11.1.7"
resolved "https://registry.npmjs.org/rc-slider/-/rc-slider-11.1.7.tgz#3de333b1ec84d53a7bda2f816bb4779423628f09"
integrity sha512-ytYbZei81TX7otdC0QvoYD72XSlxvTihNth5OeZ6PMXyEDq/vHdWFulQmfDGyXK1NwKwSlKgpvINOa88uT5g2A==
dependencies:
"@babel/runtime" "^7.10.1"
classnames "^2.2.5"
......