diff --git a/docs/Docs.md b/docs/Docs.md index 72e65b877..a2fa76620 100644 --- a/docs/Docs.md +++ b/docs/Docs.md @@ -117,8 +117,7 @@ console.log(`🚀 Server ready at ${info.url}`) ## Typescript ```typescript -const providers = await core.identityManager.getIdentityProviders() -const identity = await core.identityManager.createIdentity(providers[0].type) +const identity = await agent.identityManager.createIdentity() ``` ## GraphQL @@ -346,10 +345,12 @@ const memberVc = memberClaims[0]?.credential ```typescript const input = { - issuer: ['did:ethr:567', 'did:ethr:659'], - subject: 'did:example:1234', - type: 'member', - value: 'status', + where: [ + { column: 'issuer', value: ['did:ethr:567', 'did:ethr:659'] }, + { column: 'subject', value: ['did:example:1234'] }, + { column: 'type', value: ['member'] }, + { column: 'value', value: ['status'] }, + ], } ``` @@ -643,7 +644,7 @@ Fields import { Identity } from 'daf-core' const dbConnection = await agent.dbConnection const identity = await dbConnection.getRepository(Identity).findOne('did:example:123', { - relations: ['receivedClaims'], + relations: ['issuedClaims'], }) console.log(identity.receivedClaims) // [Claim(type: 'name', value: 'Alice'), ...] @@ -652,14 +653,19 @@ console.log(identity.receivedClaims) // [Claim(type: 'name', value: 'Alice'), .. ### GraphQL ```graphql -query { - identity(did: "did:example:123") { +query identity($did: String!) { + identity(did: $did) { + did + shortDid name: latestClaimValue(type: "name") - profileImage: latestClaimValue(type: "profileImage") - receivedClaims { - type - value + profilePicture: latestClaimValue(type: "profilePicture") + } + issuedClaims: claims(input: { where: [{ column: issuer, value: [$did] }] }) { + subject { + did } + type + value } } ``` @@ -702,7 +708,7 @@ const messages = await dbConnection.getRepository(Message).find({ ```graphql query { - messages(input: { type: "sdr", options: { take: 5 } }) { + messages(input: { where: [{ column: type, value: "sdr" }], take: 5 }) { id from { did @@ -747,7 +753,14 @@ const presentations = await dbConnection.getRepository(Presentation).find({ ```graphql query { - presentations(input: { type: "VerifiablePresentation,KYC", issuer: "did:web:example.com" }) { + presentations( + input: { + where: [ + { column: issuer, value: "did:web:example.com" } + { column: type, value: "VerifiablePresentation,KYC" } + ] + } + ) { audience { did } @@ -801,7 +814,14 @@ const credentials = await dbConnection.getRepository(Credential).find({ ```graphql query { - credentials(input: { subject: "did:web:example.com", expirationDateLessThan: "2020-12-12T10:00:00Z" }) { + credentials( + input: { + where: [ + { column: subject, value: "did:web:example.com" } + { column: expirationDate, value: "2020-12-12T10:00:00Z", op: LessThan } + ] + } + ) { issuer { did } @@ -870,7 +890,7 @@ console.log(claims) ```graphql query { - claims(input: { type: "address" }) { + claims(input: { where: [{ column: type, value: "address" }] }) { type value isObj diff --git a/packages/daf-core/src/entities/credential.ts b/packages/daf-core/src/entities/credential.ts index c77d3b5bc..a52ed2119 100644 --- a/packages/daf-core/src/entities/credential.ts +++ b/packages/daf-core/src/entities/credential.ts @@ -74,6 +74,9 @@ export class Credential extends BaseEntity { ) subject?: Identity + @Column({ nullable: true }) + id?: String + @Column() issuanceDate: Date diff --git a/packages/daf-core/src/entities/identity.ts b/packages/daf-core/src/entities/identity.ts index b75a1e756..826dd6d2c 100644 --- a/packages/daf-core/src/entities/identity.ts +++ b/packages/daf-core/src/entities/identity.ts @@ -1,4 +1,12 @@ -import { Entity, Column, PrimaryColumn, BaseEntity, OneToMany, ManyToMany } from 'typeorm' +import { + Entity, + Column, + PrimaryColumn, + BaseEntity, + OneToMany, + CreateDateColumn, + UpdateDateColumn, +} from 'typeorm' import { Key } from './key' import { Message } from './message' import { Presentation } from './presentation' @@ -14,6 +22,12 @@ export class Identity extends BaseEntity { @Column({ nullable: true }) provider: string + @CreateDateColumn() + saveDate: Date + + @UpdateDateColumn() + updateDate: Date + @Column({ nullable: true }) controllerKeyId: string diff --git a/packages/daf-core/src/entities/message.ts b/packages/daf-core/src/entities/message.ts index 1dff7df65..2b3879594 100644 --- a/packages/daf-core/src/entities/message.ts +++ b/packages/daf-core/src/entities/message.ts @@ -4,11 +4,13 @@ import { BaseEntity, ManyToOne, ManyToMany, - PrimaryGeneratedColumn, + PrimaryColumn, JoinTable, CreateDateColumn, UpdateDateColumn, + BeforeInsert, } from 'typeorm' +import { blake2bHex } from 'blakejs' import { Identity } from './identity' import { Presentation } from './presentation' import { Credential } from './credential' @@ -30,7 +32,14 @@ export class Message extends BaseEntity { } } - @PrimaryGeneratedColumn('uuid') + @BeforeInsert() + setId() { + if (!this.id) { + this.id = blake2bHex(this.raw) + } + } + + @PrimaryColumn() id: string @CreateDateColumn() diff --git a/packages/daf-core/src/entities/presentation.ts b/packages/daf-core/src/entities/presentation.ts index ad1984625..3f375819e 100644 --- a/packages/daf-core/src/entities/presentation.ts +++ b/packages/daf-core/src/entities/presentation.ts @@ -35,7 +35,7 @@ export class Presentation extends BaseEntity { identity => identity.issuedPresentations, { cascade: ['insert'], - eager: true + eager: true, }, ) issuer: Identity @@ -45,11 +45,14 @@ export class Presentation extends BaseEntity { identity => identity.receivedPresentations, { cascade: ['insert'], - eager: true + eager: true, }, ) audience: Identity + @Column({ nullable: true }) + id?: String + @Column() issuanceDate: Date diff --git a/packages/daf-core/src/graphql/graphql-base-type-defs.ts b/packages/daf-core/src/graphql/graphql-base-type-defs.ts index 536f16746..028967c6b 100644 --- a/packages/daf-core/src/graphql/graphql-base-type-defs.ts +++ b/packages/daf-core/src/graphql/graphql-base-type-defs.ts @@ -4,7 +4,7 @@ export const baseTypeDefs = ` type Mutation type Identity { - did: ID! + did: String! provider: String } diff --git a/packages/daf-core/src/graphql/graphql-core.ts b/packages/daf-core/src/graphql/graphql-core.ts index 5ededa384..de83548a4 100644 --- a/packages/daf-core/src/graphql/graphql-core.ts +++ b/packages/daf-core/src/graphql/graphql-core.ts @@ -1,4 +1,17 @@ -import { In } from 'typeorm' +import { + Not, + LessThan, + LessThanOrEqual, + MoreThan, + MoreThanOrEqual, + Equal, + Like, + Between, + In, + Any, + IsNull, + FindManyOptions, +} from 'typeorm' import { Agent } from '../agent' import { Message } from '../entities/message' import { Presentation } from '../entities/presentation' @@ -10,125 +23,126 @@ export interface Context { agent: Agent } -export interface FindOptions { +export interface Order { + column: string + direction: 'ASC' | 'DESC' +} + +export interface Where { + column: string + value?: string[] + not?: boolean + op?: + | 'LessThan' + | 'LessThanOrEqual' + | 'MoreThan' + | 'MoreThanOrEqual' + | 'Equal' + | 'Like' + | 'Between' + | 'In' + | 'Any' + | 'IsNull' +} +export interface FindInput { + where?: Where[] + order?: Order[] take?: number skip?: number } -const messages = async ( - _: any, - { - input, - }: { - input?: { - options?: FindOptions - from?: string[] - to?: string[] - type?: string[] - threadId?: string[] - } - }, - ctx: Context, -) => { - const options = { - where: {}, - } - - if (input?.from) options.where['from'] = In(input.from) - if (input?.to) options.where['to'] = In(input.to) - if (input?.type) options.where['type'] = In(input.type) - if (input?.threadId) options.where['threadId'] = In(input.threadId) - if (input?.options?.skip) options['skip'] = input.options.skip - if (input?.options?.take) options['take'] = input.options.take - - return (await ctx.agent.dbConnection).getRepository(Message).find(options) +interface TypeOrmOrder { + [x: string]: 'ASC' | 'DESC' } -const presentations = async ( - _: any, - { - input, - }: { - input?: { - options?: FindOptions - issuer?: string[] - audience?: string[] - type?: string[] - context?: string[] - } - }, - ctx: Context, -) => { - const options = { +function transformFindInput(input: FindInput): FindManyOptions { + const result: FindManyOptions = { where: {}, } - - if (input?.issuer) options.where['issuer'] = In(input.issuer) - if (input?.audience) options.where['audience'] = In(input.audience) - if (input?.type) options.where['type'] = In(input.type) - if (input?.context) options.where['context'] = In(input.context) - if (input?.options?.skip) options['skip'] = input.options.skip - if (input?.options?.take) options['take'] = input.options.take - - return (await ctx.agent.dbConnection).getRepository(Presentation).find(options) -} - -const credentials = async ( - _: any, - { - input, - }: { - input?: { - options?: FindOptions - issuer?: string[] - subject?: string[] - type?: string[] - context?: string[] + if (input?.skip) result['skip'] = input.skip + if (input?.take) result['take'] = input.take + if (input?.order) { + const order: TypeOrmOrder = {} + for (const item of input.order) { + order[item.column] = item.direction } - }, - ctx: Context, -) => { - const options = { - where: {}, + result['order'] = order } + if (input?.where) { + const where = {} + for (const item of input.where) { + switch (item.op) { + case 'Any': + where[item.column] = Any(item.value) + break + case 'Between': + if (item.value?.length != 2) throw Error('Operation Between requires two values') + where[item.column] = Between(item.value[0], item.value[1]) + break + case 'Equal': + if (item.value?.length != 1) throw Error('Operation Equal requires one value') + where[item.column] = Equal(item.value[0]) + break + case 'IsNull': + where[item.column] = IsNull() + break + case 'LessThan': + if (item.value?.length != 1) throw Error('Operation LessThan requires one value') + where[item.column] = LessThan(item.value[0]) + break + case 'LessThanOrEqual': + if (item.value?.length != 1) throw Error('Operation LessThanOrEqual requires one value') + where[item.column] = LessThanOrEqual(item.value[0]) + break + case 'Like': + if (item.value?.length != 1) throw Error('Operation Like requires one value') + where[item.column] = Like(item.value[0]) + break + case 'MoreThan': + if (item.value?.length != 1) throw Error('Operation MoreThan requires one value') + where[item.column] = MoreThan(item.value[0]) + break + case 'MoreThanOrEqual': + if (item.value?.length != 1) throw Error('Operation MoreThanOrEqual requires one value') + where[item.column] = MoreThanOrEqual(item.value[0]) + break + case 'In': + default: + where[item.column] = In(item.value) + } + if (item.not === true) { + where[item.column] = Not(where[item.column]) + } + } + result['where'] = where + } + return result +} - if (input?.issuer) options.where['issuer'] = In(input.issuer) - if (input?.subject) options.where['audience'] = In(input.subject) - if (input?.type) options.where['type'] = In(input.type) - if (input?.context) options.where['context'] = In(input.context) - if (input?.options?.skip) options['skip'] = input.options.skip - if (input?.options?.take) options['take'] = input.options.take +export interface FindArgs { + input?: FindInput +} - return (await ctx.agent.dbConnection).getRepository(Credential).find(options) +const messages = async (_: any, args: FindArgs, ctx: Context) => { + return (await ctx.agent.dbConnection).getRepository(Message).find(transformFindInput(args.input)) +} +const messagesCount = async (_: any, args: FindArgs, ctx: Context) => { + return (await ctx.agent.dbConnection).getRepository(Message).count(transformFindInput(args.input)) } -const claims = async ( - _: any, - { - input, - }: { - input?: { - options?: FindOptions - issuer?: string[] - subject?: string[] - type?: string[] - value?: string[] - } - }, - ctx: Context, -) => { - const options = { - relations: ['credential'], - where: {}, - } +const presentations = async (_: any, args: FindArgs, ctx: Context) => { + const options = transformFindInput(args.input) + return (await ctx.agent.dbConnection).getRepository(Presentation).find(options) +} - if (input?.issuer) options.where['issuer'] = In(input.issuer) - if (input?.subject) options.where['subject'] = In(input.subject) - if (input?.type) options.where['type'] = In(input.type) - if (input?.value) options.where['value'] = In(input.value) - if (input?.options?.skip) options['skip'] = input.options.skip - if (input?.options?.take) options['take'] = input.options.take +const credentials = async (_: any, args: FindArgs, ctx: Context) => { + const options = transformFindInput(args.input) + return (await ctx.agent.dbConnection).getRepository(Credential).find(options) +} +const claims = async (_: any, args: FindArgs, ctx: Context) => { + const options = transformFindInput(args.input) + options['relations'] = ['credential'] return (await ctx.agent.dbConnection).getRepository(Claim).find(options) } @@ -149,6 +163,7 @@ export const resolvers = { message: async (_: any, { id }, ctx: Context) => (await ctx.agent.dbConnection).getRepository(Message).findOne(id), messages, + messagesCount, presentation: async (_: any, { hash }, ctx: Context) => (await ctx.agent.dbConnection).getRepository(Presentation).findOne(hash), presentations, @@ -167,54 +182,6 @@ export const resolvers = { ( await (await ctx.agent.dbConnection).getRepository(Identity).findOne(identity.did) ).getLatestClaimValue(ctx.agent.dbConnection, { type: args.type }), - sentMessages: async (identity: Identity, args, ctx: Context) => - ( - await (await ctx.agent.dbConnection) - .getRepository(Identity) - .findOne(identity.did, { relations: ['sentMessages'] }) - ).sentMessages, - receivedMessages: async (identity: Identity, args, ctx: Context) => - ( - await (await ctx.agent.dbConnection) - .getRepository(Identity) - .findOne(identity.did, { relations: ['receivedMessages'] }) - ).receivedMessages, - issuedPresentations: async (identity: Identity, args, ctx: Context) => - ( - await (await ctx.agent.dbConnection) - .getRepository(Identity) - .findOne(identity.did, { relations: ['issuedPresentations'] }) - ).issuedPresentations, - receivedPresentations: async (identity: Identity, args, ctx: Context) => - ( - await (await ctx.agent.dbConnection) - .getRepository(Identity) - .findOne(identity.did, { relations: ['receivedPresentations'] }) - ).receivedPresentations, - issuedCredentials: async (identity: Identity, args, ctx: Context) => - ( - await (await ctx.agent.dbConnection) - .getRepository(Identity) - .findOne(identity.did, { relations: ['issuedCredentials'] }) - ).issuedCredentials, - receivedCredentials: async (identity: Identity, args, ctx: Context) => - ( - await (await ctx.agent.dbConnection) - .getRepository(Identity) - .findOne(identity.did, { relations: ['receivedCredentials'] }) - ).receivedCredentials, - issuedClaims: async (identity: Identity, args, ctx: Context) => - ( - await (await ctx.agent.dbConnection) - .getRepository(Identity) - .findOne(identity.did, { relations: ['issuedClaims'] }) - ).issuedClaims, - receivedClaims: async (identity: Identity, args, ctx: Context) => - ( - await (await ctx.agent.dbConnection) - .getRepository(Identity) - .findOne(identity.did, { relations: ['receivedClaims'] }) - ).receivedClaims, }, Credential: { @@ -274,53 +241,184 @@ export const resolvers = { } export const typeDefs = ` + enum WhereOperation { + Not + LessThan + LessThanOrEqual + MoreThan + MoreThanOrEqual + Equal + Like + Between + In + Any + IsNull + } + + enum OrderDirection { + ASC + DESC + } + + enum MessagesColumns { + saveDate + updateDate + createdAt + expiresAt + threadId + type + replyTo + replyUrl + from + to + } + + input MessagesWhere { + column: MessagesColumns! + value: [String] + not: Boolean + op: WhereOperation + } + + input MessagesOrder { + column: MessagesColumns! + direction: OrderDirection! + } - input FindOptions { + input MessagesInput { + where: [MessagesWhere] + order: [MessagesOrder] take: Int skip: Int } + + + enum IdentitiesColumns { + saveDate + updateDate + did + provider + } + + input IdentitiesWhere { + column: IdentitiesColumns! + value: [String] + not: Boolean + op: WhereOperation + } + + input IdentitiesOrder { + column: IdentitiesColumns! + direction: OrderDirection! + } + input IdentitiesInput { - options: FindOptions + where: [IdentitiesWhere] + order: [IdentitiesOrder] + take: Int + skip: Int } - input MessagesInput { - from: [String] - to: [String] - type: [String] - threadId: [String] - options: FindOptions + + enum PresentationsColumns { + id + issuanceDate + expirationDate + context + type + issuer + audience + } + + input PresentationsWhere { + column: PresentationsColumns! + value: [String] + not: Boolean + op: WhereOperation + } + + input PresentationsOrder { + column: PresentationsColumns! + direction: OrderDirection! } input PresentationsInput { - issuer: [String] - audience: [String] - type: [String] - context: [String] - options: FindOptions + where: [PresentationsWhere] + order: [PresentationsOrder] + take: Int + skip: Int + } + + + enum CredentialsColumns { + id + issuanceDate + expirationDate + context + type + issuer + subject + } + + input CredentialsWhere { + column: CredentialsColumns! + value: [String] + not: Boolean + op: WhereOperation + } + + input CredentialsOrder { + column: CredentialsColumns! + direction: OrderDirection! } input CredentialsInput { - issuer: [String] - subject: [String] - type: [String] - context: [String] - options: FindOptions + where: [CredentialsWhere] + order: [CredentialsOrder] + take: Int + skip: Int } - input ClaimsInput { - issuer: [ID] - subject: [ID] - type: [String] + + enum ClaimsColumns { + issuanceDate + expirationDate + context + credentialType + type + value + issuer + subject + } + + input ClaimsWhere { + column: ClaimsColumns! value: [String] - options: FindOptions + not: Boolean + op: WhereOperation + } + + input ClaimsOrder { + column: ClaimsColumns! + direction: OrderDirection! } + input ClaimsInput { + where: [ClaimsWhere] + order: [ClaimsOrder] + take: Int + skip: Int + } + + + extend type Query { - identity(did: ID!): Identity + identity(did: String!): Identity identities(input: IdentitiesInput): [Identity] message(id: ID!): Message messages(input: MessagesInput): [Message] + messagesCount(input: MessagesInput): Int presentation(hash: ID!): Presentation presentations(input: PresentationsInput): [Presentation] credential(hash: ID!): Credential @@ -341,14 +439,8 @@ export const typeDefs = ` extend type Identity { shortDid: String! latestClaimValue(type: String): String - sentMessages: [Message] - receivedMessages: [Message] - issuedPresentations: [Presentation] - receivedPresentations: [Presentation] - issuedCredentials: [Credential] - receivedCredentials: [Credential] - issuedClaims: [Claim] - receivedClaims: [Claim] + saveDate: Date! + updateDate: Date! } scalar Object @@ -380,6 +472,7 @@ export const typeDefs = ` type Presentation { hash: ID! + id: String raw: String! issuer: Identity! audience: Identity! @@ -393,6 +486,7 @@ export const typeDefs = ` type Credential { hash: ID! + id: String raw: String! issuer: Identity! subject: Identity diff --git a/packages/daf-selective-disclosure/src/graphql.ts b/packages/daf-selective-disclosure/src/graphql.ts index 0e50d1110..f12fa330d 100644 --- a/packages/daf-selective-disclosure/src/graphql.ts +++ b/packages/daf-selective-disclosure/src/graphql.ts @@ -95,7 +95,7 @@ export const typeDefs = ` } extend type Message { - sdr(did: ID): [CredentialRequest] + sdr(did: String): [CredentialRequest] } extend type Mutation { diff --git a/packages/daf-w3c/src/action-handler.ts b/packages/daf-w3c/src/action-handler.ts index d7906db2c..bb657b182 100644 --- a/packages/daf-w3c/src/action-handler.ts +++ b/packages/daf-w3c/src/action-handler.ts @@ -93,6 +93,7 @@ export interface CredentialInput { '@context'?: string[] context?: string[] type: string[] + id?: string issuer: string expirationDate?: string credentialSubject: CredentialSubjectInput @@ -102,6 +103,7 @@ export interface PresentationInput { '@context'?: string[] context?: string[] type: string[] + id?: string issuer: string audience: string tag?: string @@ -115,7 +117,6 @@ const transformCredentialInput = (input: CredentialInput): VerifiableCredentialP delete credentialSubject.id const result: VerifiableCredentialPayload = { sub: input.credentialSubject.id, - // exp: input.expirationDate, vc: { '@context': input['@context'] || input['context'], type: input.type, @@ -123,6 +124,14 @@ const transformCredentialInput = (input: CredentialInput): VerifiableCredentialP }, } + if (input.expirationDate) { + result['exp'] = new Date(input.expirationDate).getUTCSeconds() + } + + if (input.id) { + result['jti'] = input.id + } + return result } @@ -130,7 +139,6 @@ const transformPresentationInput = (input: PresentationInput): PresentationPaylo // TODO validate input const result: PresentationPayload = { aud: input.audience, - // exp: input.expirationDate, vp: { '@context': input['@context'] || input['context'], type: input.type, @@ -138,5 +146,17 @@ const transformPresentationInput = (input: PresentationInput): PresentationPaylo }, } + if (input.expirationDate) { + result['exp'] = new Date(input.expirationDate).getUTCSeconds() + } + + if (input.id) { + result['jti'] = input.id + } + + if (input.tag) { + result['tag'] = input.tag + } + return result } diff --git a/packages/daf-w3c/src/graphql.ts b/packages/daf-w3c/src/graphql.ts index 691027c67..2731a3feb 100644 --- a/packages/daf-w3c/src/graphql.ts +++ b/packages/daf-w3c/src/graphql.ts @@ -50,6 +50,7 @@ export const typeDefs = ` scalar CredentialSubject input SignCredentialInput { + id: String issuer: String! context: [String]! type: [String]! @@ -57,6 +58,8 @@ export const typeDefs = ` } input SignPresentationInput { + id: String + tag: String issuer: String! audience: String! context: [String]! diff --git a/packages/daf-w3c/src/message-handler.ts b/packages/daf-w3c/src/message-handler.ts index 738c06d5f..bb4204dc9 100644 --- a/packages/daf-w3c/src/message-handler.ts +++ b/packages/daf-w3c/src/message-handler.ts @@ -94,6 +94,10 @@ export function createCredential(payload: VerifiableCredentialPayload, jwt: stri vc.raw = jwt + if (payload.jti) { + vc.id = payload.jti + } + if (payload.nbf || payload.iat) { vc.issuanceDate = timestampToDate(payload.nbf || payload.iat) } @@ -125,6 +129,10 @@ export function createPresentation( vp.raw = jwt + if (payload.jti) { + vp.id = payload.jti + } + if (payload.nbf || payload.iat) { vp.issuanceDate = timestampToDate(payload.nbf || payload.iat) }