Skip to content
Snippets Groups Projects
Commit 1190cb4e authored by Berend Sliedrecht's avatar Berend Sliedrecht
Browse files

feat(ssi): credentials module


Signed-off-by: default avatarBerend Sliedrecht <berend@animo.id>
parent 9f9f628a
No related branches found
No related tags found
No related merge requests found
Showing
with 552 additions and 35 deletions
import type { BaseEventInput } from './baseEvents.js';
import {
CredentialExchangeRecord,
JsonTransformer,
} from '@aries-framework/core';
import { BaseEvent } from './baseEvents.js';
export type EventDidcommAnonCredsCredentialsGetAllInput = BaseEventInput;
export class EventDidcommAnonCredsCredentialsGetAll extends BaseEvent<
Array<CredentialExchangeRecord>
> {
public static token = 'didcomm.anoncreds.credentials.getAll';
public get instance() {
return this.data.map((d) =>
JsonTransformer.fromJSON(d, CredentialExchangeRecord),
);
}
public static fromEvent(e: EventDidcommAnonCredsCredentialsGetAll) {
return new EventDidcommAnonCredsCredentialsGetAll(
e.data,
e.tenantId,
e.id,
e.type,
e.timestamp,
);
}
}
export type EventDidcommAnonCredsCredentialsGetByIdInput = BaseEventInput<{
credentialRecordId: string;
}>;
export class EventDidcommAnonCredsCredentialsGetById extends BaseEvent<CredentialExchangeRecord | null> {
public static token = 'didcomm.anoncreds.credentials.getById';
public get instance() {
return this.data
? JsonTransformer.fromJSON(this.data, CredentialExchangeRecord)
: null;
}
public static fromEvent(e: EventDidcommAnonCredsCredentialsGetById) {
return new EventDidcommAnonCredsCredentialsGetById(
e.data,
e.tenantId,
e.id,
e.type,
e.timestamp,
);
}
}
export type EventDidcommAnonCredsCredentialsOfferInput = BaseEventInput<{
connectionId: string;
credentialDefinitionId: string;
attributes: Array<{ name: string; value: string; mimeType?: string }>;
}>;
export class EventDidcommAnonCredsCredentialsOffer extends BaseEvent<CredentialExchangeRecord> {
public static token = 'didcomm.anoncreds.credentials.offer';
public get instance() {
return JsonTransformer.fromJSON(this.data, CredentialExchangeRecord);
}
public static fromEvent(e: EventDidcommAnonCredsCredentialsOffer) {
return new EventDidcommAnonCredsCredentialsOffer(
e.data,
e.tenantId,
e.id,
e.type,
e.timestamp,
);
}
}
export type EventDidcommAnonCredsCredentialsOfferToSelfInput = Omit<
EventDidcommAnonCredsCredentialsOfferInput,
'connectionId'
>;
export class EventDidcommAnonCredsCredentialsOfferToSelf extends BaseEvent<CredentialExchangeRecord> {
public static token = 'didcomm.anoncreds.credentials.offerToSelf';
public get instance() {
return JsonTransformer.fromJSON(this.data, CredentialExchangeRecord);
}
public static fromEvent(e: EventDidcommAnonCredsCredentialsOfferToSelf) {
return new EventDidcommAnonCredsCredentialsOfferToSelf(
e.data,
e.tenantId,
e.id,
e.type,
e.timestamp,
);
}
}
......@@ -9,3 +9,4 @@ export * from './events/didEvents.js';
export * from './events/tenantEvents.js';
export * from './events/schemaEvents.js';
export * from './events/credentialDefinitionEvents.js';
export * from './events/credentialEvents.js';
......@@ -3,7 +3,11 @@ import type { InitConfig } from '@aries-framework/core';
import type { IndyVdrPoolConfig } from '@aries-framework/indy-vdr';
import type { OnApplicationShutdown } from '@nestjs/common';
import { AnonCredsModule } from '@aries-framework/anoncreds';
import {
AnonCredsCredentialFormatService,
AnonCredsModule,
LegacyIndyCredentialFormatService,
} from '@aries-framework/anoncreds';
import { AnonCredsRsModule } from '@aries-framework/anoncreds-rs';
import { AskarModule } from '@aries-framework/askar';
import {
......@@ -19,6 +23,7 @@ import {
LogLevel,
PeerDidRegistrar,
PeerDidResolver,
V2CredentialProtocol,
WebDidResolver,
} from '@aries-framework/core';
import {
......@@ -94,6 +99,14 @@ export class AgentService implements OnApplicationShutdown {
}),
credentials: new CredentialsModule({
autoAcceptCredentials: autoAcceptCredential,
credentialProtocols: [
new V2CredentialProtocol({
credentialFormats: [
new AnonCredsCredentialFormatService(),
new LegacyIndyCredentialFormatService(),
],
}),
],
}),
anoncredsRs: new AnonCredsRsModule({ anoncreds }),
......
import {
CredentialExchangeRecord,
CredentialState,
} from '@aries-framework/core';
import { Test } from '@nestjs/testing';
import { mockConfigModule } from '../../../config/__tests__/mockConfig.js';
import { AgentModule } from '../../agent.module.js';
import { AnonCredsCredentialsController } from '../anoncredsCredentials.controller.js';
import { AnonCredsCredentialsService } from '../anoncredsCredentials.service.js';
describe('AnonCredsCredentialsController', () => {
let credentialsController: AnonCredsCredentialsController;
let credentialsService: AnonCredsCredentialsService;
beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
imports: [mockConfigModule(), AgentModule],
controllers: [AnonCredsCredentialsController],
providers: [AnonCredsCredentialsService],
}).compile();
credentialsService = moduleRef.get(AnonCredsCredentialsService);
credentialsController = moduleRef.get(AnonCredsCredentialsController);
});
describe('get all', () => {
it('should get all the credential records of the agent', async () => {
const result: Array<CredentialExchangeRecord> = [];
jest.spyOn(credentialsService, 'getAll').mockResolvedValue(result);
const event = await credentialsController.getAll({
tenantId: 'some-id',
});
expect(event.data).toStrictEqual(result);
});
});
describe('get by id', () => {
it('should get a credential record by id', async () => {
const result: CredentialExchangeRecord | null = null;
jest.spyOn(credentialsService, 'getById').mockResolvedValue(result);
const event = await credentialsController.getById({
tenantId: 'some-id',
credentialRecordId: 'some-id',
});
expect(event.data).toStrictEqual(result);
});
});
describe('offer', () => {
it('should offer a credential', async () => {
const result: CredentialExchangeRecord = new CredentialExchangeRecord({
state: CredentialState.Done,
threadId: 'some-id',
protocolVersion: 'v2',
});
jest.spyOn(credentialsService, 'offer').mockResolvedValue(result);
const event = await credentialsController.offer({
tenantId: 'some-id',
connectionId: 'some-id',
credentialDefinitionId: 'some-id',
attributes: [
{ name: 'Name', value: 'Berend', mimeType: 'application/text' },
{ name: 'Age', value: '25' },
],
});
expect(event.data).toStrictEqual(result);
});
});
describe('offer to self', () => {
it('should offer a credential to self', async () => {
const result: CredentialExchangeRecord = new CredentialExchangeRecord({
state: CredentialState.Done,
threadId: 'some-id',
protocolVersion: 'v2',
});
jest.spyOn(credentialsService, 'offerToSelf').mockResolvedValue(result);
const event = await credentialsController.offerToSelf({
tenantId: 'some-id',
credentialDefinitionId: 'some-id',
attributes: [
{ name: 'Name', value: 'Berend', mimeType: 'application/text' },
{ name: 'Age', value: '25' },
],
});
expect(event.data).toStrictEqual(result);
});
});
});
import { Controller } from '@nestjs/common';
import { MessagePattern } from '@nestjs/microservices';
import {
EventDidcommAnonCredsCredentialsGetAll,
EventDidcommAnonCredsCredentialsGetAllInput,
EventDidcommAnonCredsCredentialsGetById,
EventDidcommAnonCredsCredentialsGetByIdInput,
EventDidcommAnonCredsCredentialsOffer,
EventDidcommAnonCredsCredentialsOfferInput,
EventDidcommAnonCredsCredentialsOfferToSelfInput,
EventDidcommAnonCredsCredentialsOfferToSelf,
} from '@ocm/shared';
import { AnonCredsCredentialsService } from './anoncredsCredentials.service.js';
@Controller('anoncredsCredentials')
export class AnonCredsCredentialsController {
public constructor(private credentialsService: AnonCredsCredentialsService) {}
@MessagePattern(EventDidcommAnonCredsCredentialsGetAll.token)
public async getAll(
options: EventDidcommAnonCredsCredentialsGetAllInput,
): Promise<EventDidcommAnonCredsCredentialsGetAll> {
return new EventDidcommAnonCredsCredentialsGetAll(
await this.credentialsService.getAll(options),
options.tenantId,
);
}
@MessagePattern(EventDidcommAnonCredsCredentialsGetById.token)
public async getById(
options: EventDidcommAnonCredsCredentialsGetByIdInput,
): Promise<EventDidcommAnonCredsCredentialsGetById> {
return new EventDidcommAnonCredsCredentialsGetById(
await this.credentialsService.getById(options),
options.tenantId,
);
}
@MessagePattern(EventDidcommAnonCredsCredentialsOffer.token)
public async offer(
options: EventDidcommAnonCredsCredentialsOfferInput,
): Promise<EventDidcommAnonCredsCredentialsOffer> {
return new EventDidcommAnonCredsCredentialsOffer(
await this.credentialsService.offer(options),
options.tenantId,
);
}
@MessagePattern(EventDidcommAnonCredsCredentialsOfferToSelf.token)
public async offerToSelf(
options: EventDidcommAnonCredsCredentialsOfferToSelfInput,
): Promise<EventDidcommAnonCredsCredentialsOfferToSelf> {
return new EventDidcommAnonCredsCredentialsOfferToSelf(
await this.credentialsService.offerToSelf(options),
options.tenantId,
);
}
}
import { Module } from '@nestjs/common';
import { AgentModule } from '../agent.module.js';
import { AnonCredsCredentialsController } from './anoncredsCredentials.controller.js';
import { AnonCredsCredentialsService } from './anoncredsCredentials.service.js';
@Module({
imports: [AgentModule],
providers: [AnonCredsCredentialsService],
controllers: [AnonCredsCredentialsController],
})
export class AnonCredsCredentialsModule {}
import type {
EventDidcommAnonCredsCredentialsGetAllInput,
EventDidcommAnonCredsCredentialsGetByIdInput,
EventDidcommAnonCredsCredentialsOfferInput,
EventDidcommAnonCredsCredentialsOfferToSelfInput,
} from '@ocm/shared';
import {
AutoAcceptCredential,
type CredentialExchangeRecord,
} from '@aries-framework/core';
import { Injectable } from '@nestjs/common';
import { MetadataTokens } from '../../common/constants.js';
import { WithTenantService } from '../withTenantService.js';
@Injectable()
export class AnonCredsCredentialsService {
public constructor(private withTenantService: WithTenantService) {}
public async getAll({
tenantId,
}: EventDidcommAnonCredsCredentialsGetAllInput): Promise<
Array<CredentialExchangeRecord>
> {
return this.withTenantService.invoke(tenantId, (t) =>
t.credentials.getAll(),
);
}
public async getById({
tenantId,
credentialRecordId,
}: EventDidcommAnonCredsCredentialsGetByIdInput): Promise<CredentialExchangeRecord | null> {
return this.withTenantService.invoke(tenantId, (t) =>
t.credentials.findById(credentialRecordId),
);
}
public async offer({
tenantId,
connectionId,
credentialDefinitionId,
attributes,
}: EventDidcommAnonCredsCredentialsOfferInput): Promise<CredentialExchangeRecord> {
return this.withTenantService.invoke(tenantId, (t) =>
t.credentials.offerCredential({
protocolVersion: 'v2',
connectionId,
credentialFormats: {
anoncreds: { credentialDefinitionId, attributes },
},
}),
);
}
public async offerToSelf({
tenantId,
credentialDefinitionId,
attributes,
}: EventDidcommAnonCredsCredentialsOfferToSelfInput): Promise<CredentialExchangeRecord> {
return this.withTenantService.invoke(tenantId, async (t) => {
const connections = await t.connections.getAll();
const connection = connections.find((c) => {
const metadata = c.metadata.get<{ withSelf: boolean }>(
MetadataTokens.GAIA_X_CONNECTION_METADATA_KEY,
);
return metadata && metadata.withSelf === true;
});
if (!connection) {
throw new Error(
'Cannot offer a credential to yourself as there is no connection',
);
}
if (!connection.isReady) {
throw new Error('Connection with yourself is not ready, yet');
}
return t.credentials.offerCredential({
protocolVersion: 'v2',
autoAcceptCredential: AutoAcceptCredential.Always,
connectionId: connection.id,
credentialFormats: {
anoncreds: { credentialDefinitionId, attributes },
},
});
});
}
}
......@@ -30,11 +30,11 @@ describe('ConnectionsController', () => {
const result: Array<ConnectionRecord> = [];
jest.spyOn(connectionsService, 'getAll').mockResolvedValue(result);
const connectionsEvent = await connectionsController.getAll({
const event = await connectionsController.getAll({
tenantId: 'some-id',
});
expect(connectionsEvent.data).toStrictEqual(result);
expect(event.data).toStrictEqual(result);
});
});
......@@ -43,12 +43,12 @@ describe('ConnectionsController', () => {
const result: ConnectionRecord | null = null;
jest.spyOn(connectionsService, 'getById').mockResolvedValue(result);
const connectionsEvent = await connectionsController.getById({
const event = await connectionsController.getById({
id: 'id',
tenantId: 'some-id',
});
expect(connectionsEvent.data).toStrictEqual(result);
expect(event.data).toStrictEqual(result);
});
});
......@@ -63,12 +63,11 @@ describe('ConnectionsController', () => {
.spyOn(connectionsService, 'createConnectionWithSelf')
.mockResolvedValue(result);
const connectionsEvent =
await connectionsController.createConnectionWithSelf({
tenantId: 'some-id',
});
const event = await connectionsController.createConnectionWithSelf({
tenantId: 'some-id',
});
expect(connectionsEvent.data).toStrictEqual(result);
expect(event.data).toStrictEqual(result);
});
});
});
......@@ -24,15 +24,13 @@ import { WithTenantService } from '../withTenantService.js';
@Injectable()
export class ConnectionsService {
public agent: AppAgent;
public withTenantService: WithTenantService;
private agent: AppAgent;
public constructor(
agentService: AgentService,
withTenantService: WithTenantService,
private withTenantService: WithTenantService,
) {
this.agent = agentService.agent;
this.withTenantService = withTenantService;
}
public async getAll({
......@@ -104,6 +102,7 @@ export class ConnectionsService {
MetadataTokens.GAIA_X_CONNECTION_METADATA_KEY,
{
trusted: true,
withSelf: true,
},
);
......
......@@ -12,11 +12,7 @@ import { WithTenantService } from '../withTenantService.js';
@Injectable()
export class CredentialDefinitionsService {
public withTenantService: WithTenantService;
public constructor(withTenantService: WithTenantService) {
this.withTenantService = withTenantService;
}
public constructor(private withTenantService: WithTenantService) {}
public async getAll({
tenantId,
......
......@@ -9,16 +9,10 @@ import { WithTenantService } from '../withTenantService.js';
@Injectable()
export class DidsService {
private withTenantService: WithTenantService;
private configService: ConfigService;
public constructor(
withTenantService: WithTenantService,
configService: ConfigService,
) {
this.withTenantService = withTenantService;
this.configService = configService;
}
private withTenantService: WithTenantService,
private configService: ConfigService,
) {}
public async resolve({ did, tenantId }: EventDidsResolveInput) {
return this.withTenantService.invoke(tenantId, async (t) => {
......
......@@ -12,11 +12,7 @@ import { WithTenantService } from '../withTenantService.js';
@Injectable()
export class SchemasService {
public withTenantService: WithTenantService;
public constructor(withTenantService: WithTenantService) {
this.withTenantService = withTenantService;
}
public constructor(private withTenantService: WithTenantService) {}
public async getAll({
tenantId,
......
......@@ -6,7 +6,7 @@ import { AgentService } from '../agent.service.js';
@Injectable()
export class TenantsService {
public agent: AppAgent;
private agent: AppAgent;
public constructor(agentService: AgentService) {
this.agent = agentService.agent;
......
......@@ -5,6 +5,7 @@ import { TerminusModule } from '@nestjs/terminus';
import { HealthController } from '@ocm/shared';
import { AgentModule } from './agent/agent.module.js';
import { AnonCredsCredentialsModule } from './agent/anoncredsCredentials/anoncredsCredentials.module.js';
import { ConnectionsModule } from './agent/connections/connections.module.js';
import { CredentialDefinitionsModule } from './agent/credentialDefinitions/credentialDefinitions.module.js';
import { SchemasModule } from './agent/schemas/schemas.module.js';
......@@ -26,6 +27,7 @@ import { validationSchema } from './config/validation.js';
CredentialDefinitionsModule,
DidsModule,
SchemasModule,
AnonCredsCredentialsModule,
TenantsModule,
],
controllers: [HealthController],
......
import type { INestApplication } from '@nestjs/common';
import type { ClientProxy } from '@nestjs/microservices';
import type {
EventDidcommAnonCredsCredentialsGetAllInput,
EventDidcommAnonCredsCredentialsGetByIdInput,
EventDidcommAnonCredsCredentialsOfferToSelfInput,
} from '@ocm/shared';
import { AutoAcceptCredential } from '@aries-framework/core';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { Test } from '@nestjs/testing';
import {
EventDidcommAnonCredsCredentialsGetAll,
EventDidcommAnonCredsCredentialsGetById,
EventDidcommAnonCredsCredentialsOfferToSelf,
} from '@ocm/shared';
import { firstValueFrom } from 'rxjs';
import { AgentModule } from '../src/agent/agent.module.js';
import { AnonCredsCredentialsModule } from '../src/agent/anoncredsCredentials/anoncredsCredentials.module.js';
import { ConnectionsModule } from '../src/agent/connections/connections.module.js';
import { ConnectionsService } from '../src/agent/connections/connections.service.js';
import { CredentialDefinitionsModule } from '../src/agent/credentialDefinitions/credentialDefinitions.module.js';
import { CredentialDefinitionsService } from '../src/agent/credentialDefinitions/credentialDefinitions.service.js';
import { DidsModule } from '../src/agent/dids/dids.module.js';
import { DidsService } from '../src/agent/dids/dids.service.js';
import { SchemasModule } from '../src/agent/schemas/schemas.module.js';
import { SchemasService } from '../src/agent/schemas/schemas.service.js';
import { TenantsModule } from '../src/agent/tenants/tenants.module.js';
import { TenantsService } from '../src/agent/tenants/tenants.service.js';
import { mockConfigModule } from '../src/config/__tests__/mockConfig.js';
describe('Credentials', () => {
const TOKEN = 'CREDENTIALS_CLIENT_SERVICE';
let app: INestApplication;
let client: ClientProxy;
let tenantId: string;
let issuerDid: string;
let credentialDefinitionId: string;
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
imports: [
mockConfigModule(3004, true),
AgentModule,
ConnectionsModule,
SchemasModule,
CredentialDefinitionsModule,
AnonCredsCredentialsModule,
TenantsModule,
DidsModule,
ClientsModule.register([{ name: TOKEN, transport: Transport.NATS }]),
],
}).compile();
app = moduleRef.createNestApplication();
app.connectMicroservice({ transport: Transport.NATS });
await app.startAllMicroservices();
await app.init();
client = app.get(TOKEN);
await client.connect();
const tenantsService = app.get(TenantsService);
const { id } = await tenantsService.create(TOKEN);
tenantId = id;
const connectionsService = app.get(ConnectionsService);
await connectionsService.createConnectionWithSelf({ tenantId });
const didsService = app.get(DidsService);
const [did] = await didsService.registerDidIndyFromSeed({
tenantId,
seed: '12312367897123300000000000000000',
});
issuerDid = did;
const schemaService = app.get(SchemasService);
const { schemaId } = await schemaService.register({
issuerDid,
tenantId,
name: 'test-schema-name',
version: `1.${new Date().getTime()}`,
attributeNames: ['Name', 'Age'],
});
const credentialDefinitionService = app.get(CredentialDefinitionsService);
const { credentialDefinitionId: cdi } =
await credentialDefinitionService.register({
tenantId,
issuerDid,
schemaId,
tag: `default-${new Date().getTime()}`,
});
credentialDefinitionId = cdi;
});
afterAll(async () => {
await app.close();
client.close();
});
it(EventDidcommAnonCredsCredentialsGetAll.token, async () => {
const response$ = client.send<
EventDidcommAnonCredsCredentialsGetAll,
EventDidcommAnonCredsCredentialsGetAllInput
>(EventDidcommAnonCredsCredentialsGetAll.token, { tenantId });
const response = await firstValueFrom(response$);
const eventInstance =
EventDidcommAnonCredsCredentialsGetAll.fromEvent(response);
expect(eventInstance.instance).toEqual(expect.arrayContaining([]));
});
it(EventDidcommAnonCredsCredentialsGetById.token, async () => {
const response$ = client.send<
EventDidcommAnonCredsCredentialsGetById,
EventDidcommAnonCredsCredentialsGetByIdInput
>(EventDidcommAnonCredsCredentialsGetById.token, {
tenantId,
credentialRecordId: 'some-id',
});
const response = await firstValueFrom(response$);
const eventInstance =
EventDidcommAnonCredsCredentialsGetById.fromEvent(response);
expect(eventInstance.instance).toEqual(null);
});
it(EventDidcommAnonCredsCredentialsOfferToSelf.token, async () => {
const attributes = [
{ name: 'Name', value: 'Berend' },
{ name: 'Age', value: '25' },
];
const response$ = client.send<
EventDidcommAnonCredsCredentialsOfferToSelf,
EventDidcommAnonCredsCredentialsOfferToSelfInput
>(EventDidcommAnonCredsCredentialsOfferToSelf.token, {
tenantId,
credentialDefinitionId,
attributes,
});
const response = await firstValueFrom(response$);
const eventInstance =
EventDidcommAnonCredsCredentialsOfferToSelf.fromEvent(response);
expect(eventInstance.instance).toMatchObject({
autoAcceptCredential: AutoAcceptCredential.Always,
});
});
});
......@@ -3,7 +3,7 @@ import config from '../jest.config.js';
/** @type {import('jest').Config} */
export default {
...config,
testTimeout: 24000,
testTimeout: 36000,
rootDir: '.',
testRegex: '.*\\.e2e-spec\\.ts$',
};
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment