Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): support image url in invitations #463

Merged
merged 4 commits into from
Oct 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,9 @@ Some features are not yet supported, but are on our roadmap. Check [the roadmap]
- ✅ Basic Message Protocol ([RFC 0095](https://github.com/hyperledger/aries-rfcs/blob/master/features/0095-basic-message/README.md))
- ✅ Mediator Coordination Protocol ([RFC 0211](https://github.com/hyperledger/aries-rfcs/blob/master/features/0211-route-coordination/README.md))
- ✅ Indy Credentials (with `did:sov` support)
- ✅ HTTP Transport
- ✅ HTTP & WebSocket Transport
- ✅ Connection-less Issuance and Verification
- ✅ Smart Auto Acceptance of Connections, Credentials and Proofs
- ✅ WebSocket Transport
- 🚧 Revocation of Indy Credentials
- 🚧 Electron
- ❌ Browser
Expand Down Expand Up @@ -125,6 +124,14 @@ Now that your project is setup and everything seems to be working, it is time to
7. [Proofs](/docs/getting-started/6-proofs.md)
8. [Logging](/docs/getting-started/7-logging.md)

### Divergence from Aries RFCs

Although Aries Framework JavaScript tries to follow the standards as described in the Aries RFCs as much as possible, some features in AFJ slightly diverge from the written spec. Below is an overview of the features that diverge from the spec, their impact and the reasons for diverging.

| Feature | Impact | Reason |
| -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Support for `imageUrl` attribute in connection invitation and connection request | Properties that are not recognized should be ignored, meaning this shouldn't limit interoperability between agents. As the image url is self-attested it could give a false sense of trust. Better, credential based, method for visually identifying an entity are not present yet. | Even though not documented, almost all agents support this feature. Not including this feature means AFJ is lacking in features in comparison to other implementations. |

## Contributing

If you would like to contribute to the framework, please read the [Framework Developers README](/DEVREADME.md) and the [CONTRIBUTING](/CONTRIBUTING.md) guidelines. These documents will provide more information to get you started!
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/agent/AgentConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,4 +107,8 @@ export class AgentConfig {
public get useLegacyDidSovPrefix() {
return this.initConfig.useLegacyDidSovPrefix ?? false
}

public get connectionImageUrl() {
return this.initConfig.connectionImageUrl
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,12 @@ import { ConnectionService } from '../services/ConnectionService'
jest.mock('../repository/ConnectionRepository')
const ConnectionRepositoryMock = ConnectionRepository as jest.Mock<ConnectionRepository>

const connectionImageUrl = 'https://example.com/image.png'

describe('ConnectionService', () => {
const config = getAgentConfig('ConnectionServiceTest', {
endpoints: ['http://agent.com:8080'],
connectionImageUrl,
})

let wallet: Wallet
Expand Down Expand Up @@ -55,15 +58,16 @@ describe('ConnectionService', () => {

describe('createInvitation', () => {
it('returns a connection record with values set', async () => {
expect.assertions(7)
const { connectionRecord: connectionRecord } = await connectionService.createInvitation({ routing: myRouting })
expect.assertions(8)
const { connectionRecord, message } = await connectionService.createInvitation({ routing: myRouting })

expect(connectionRecord.type).toBe('ConnectionRecord')
expect(connectionRecord.role).toBe(ConnectionRole.Inviter)
expect(connectionRecord.state).toBe(ConnectionState.Invited)
expect(connectionRecord.autoAcceptConnection).toBeUndefined()
expect(connectionRecord.id).toEqual(expect.any(String))
expect(connectionRecord.verkey).toEqual(expect.any(String))
expect(message.imageUrl).toBe(connectionImageUrl)
expect(connectionRecord.getTags()).toEqual(
expect.objectContaining({
verkey: connectionRecord.verkey,
Expand Down Expand Up @@ -144,13 +148,14 @@ describe('ConnectionService', () => {

describe('processInvitation', () => {
it('returns a connection record containing the information from the connection invitation', async () => {
expect.assertions(10)
expect.assertions(11)

const recipientKey = 'key-1'
const invitation = new ConnectionInvitationMessage({
label: 'test label',
recipientKeys: [recipientKey],
serviceEndpoint: 'https://test.com/msg',
imageUrl: connectionImageUrl,
})

const connection = await connectionService.processInvitation(invitation, { routing: myRouting })
Expand All @@ -174,6 +179,7 @@ describe('ConnectionService', () => {
expect(connection.alias).toBeUndefined()
expect(connectionAlias.alias).toBe('test-alias')
expect(connection.theirLabel).toBe('test label')
expect(connection.imageUrl).toBe(connectionImageUrl)
})

it('returns a connection record with the autoAcceptConnection parameter from the config', async () => {
Expand Down Expand Up @@ -220,7 +226,7 @@ describe('ConnectionService', () => {

describe('createRequest', () => {
it('returns a connection request message containing the information from the connection record', async () => {
expect.assertions(4)
expect.assertions(5)

const connection = getMockConnection()
mockFunction(connectionRepository.getById).mockReturnValue(Promise.resolve(connection))
Expand All @@ -231,6 +237,7 @@ describe('ConnectionService', () => {
expect(message.label).toBe(config.label)
expect(message.connection.did).toBe('test-did')
expect(message.connection.didDoc).toEqual(connection.didDoc)
expect(message.imageUrl).toBe(connectionImageUrl)
})

it(`throws an error when connection role is ${ConnectionRole.Inviter} and not ${ConnectionRole.Invitee}`, async () => {
Expand Down Expand Up @@ -260,7 +267,7 @@ describe('ConnectionService', () => {

describe('processRequest', () => {
it('returns a connection record containing the information from the connection request', async () => {
expect.assertions(6)
expect.assertions(7)

const connectionRecord = getMockConnection({
state: ConnectionState.Invited,
Expand Down Expand Up @@ -288,6 +295,7 @@ describe('ConnectionService', () => {
did: theirDid,
didDoc: theirDidDoc,
label: 'test-label',
imageUrl: connectionImageUrl,
})

const messageContext = new InboundMessageContext(connectionRequest, {
Expand All @@ -303,6 +311,7 @@ describe('ConnectionService', () => {
expect(processedConnection.theirKey).toBe(theirVerkey)
expect(processedConnection.theirLabel).toBe('test-label')
expect(processedConnection.threadId).toBe(connectionRequest.id)
expect(processedConnection.imageUrl).toBe(connectionImageUrl)
})

it('throws an error when the connection cannot be found by verkey', async () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Transform } from 'class-transformer'
import { ArrayNotEmpty, Equals, IsArray, IsOptional, IsString, ValidateIf } from 'class-validator'
import { ArrayNotEmpty, Equals, IsArray, IsOptional, IsString, IsUrl, ValidateIf } from 'class-validator'
import { parseUrl } from 'query-string'

import { AgentMessage } from '../../../agent/AgentMessage'
Expand All @@ -9,15 +9,19 @@ import { JsonTransformer } from '../../../utils/JsonTransformer'
import { MessageValidator } from '../../../utils/MessageValidator'
import { replaceLegacyDidSovPrefix } from '../../../utils/messageType'

// TODO: improve typing of `DIDInvitationData` and `InlineInvitationData` so properties can't be mixed
export interface InlineInvitationData {
export interface BaseInvitationOptions {
id?: string
label: string
imageUrl?: string
}

export interface InlineInvitationOptions {
recipientKeys: string[]
serviceEndpoint: string
routingKeys?: string[]
imageUrl?: string
}

export interface DIDInvitationData {
export interface DIDInvitationOptions {
did: string
}

Expand All @@ -31,20 +35,20 @@ export class ConnectionInvitationMessage extends AgentMessage {
* Create new ConnectionInvitationMessage instance.
* @param options
*/
public constructor(options: { id?: string; label: string } & (DIDInvitationData | InlineInvitationData)) {
public constructor(options: BaseInvitationOptions & (DIDInvitationOptions | InlineInvitationOptions)) {
super()

if (options) {
this.id = options.id || this.generateId()
this.label = options.label
this.imageUrl = options.imageUrl

if (isDidInvitation(options)) {
this.did = options.did
} else {
this.recipientKeys = options.recipientKeys
this.serviceEndpoint = options.serviceEndpoint
this.routingKeys = options.routingKeys
this.imageUrl = options.imageUrl
}

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
Expand Down Expand Up @@ -91,7 +95,7 @@ export class ConnectionInvitationMessage extends AgentMessage {
public routingKeys?: string[]

@IsOptional()
@IsString()
@IsUrl()
public imageUrl?: string

/**
Expand Down Expand Up @@ -141,6 +145,8 @@ export class ConnectionInvitationMessage extends AgentMessage {
*
* @param invitation invitation object
*/
function isDidInvitation(invitation: InlineInvitationData | DIDInvitationData): invitation is DIDInvitationData {
return (invitation as DIDInvitationData).did !== undefined
function isDidInvitation(
invitation: InlineInvitationOptions | DIDInvitationOptions
): invitation is DIDInvitationOptions {
return (invitation as DIDInvitationOptions).did !== undefined
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { DidDoc } from '../models'

import { Type } from 'class-transformer'
import { Equals, IsInstance, IsString, ValidateNested } from 'class-validator'
import { Equals, IsInstance, IsOptional, IsString, IsUrl, ValidateNested } from 'class-validator'

import { AgentMessage } from '../../../agent/AgentMessage'
import { Connection } from '../models'
Expand All @@ -11,10 +11,11 @@ export interface ConnectionRequestMessageOptions {
label: string
did: string
didDoc?: DidDoc
imageUrl?: string
}

/**
* Message to communicate the DID document to the other agent when creating a connectino
* Message to communicate the DID document to the other agent when creating a connection
*
* @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0160-connection-protocol/README.md#1-connection-request
*/
Expand All @@ -29,6 +30,7 @@ export class ConnectionRequestMessage extends AgentMessage {
if (options) {
this.id = options.id || this.generateId()
this.label = options.label
this.imageUrl = options.imageUrl

this.connection = new Connection({
did: options.did,
Expand All @@ -48,4 +50,8 @@ export class ConnectionRequestMessage extends AgentMessage {
@ValidateNested()
@IsInstance(Connection)
public connection!: Connection

@IsOptional()
@IsUrl()
public imageUrl?: string
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export interface ConnectionRecordProps {
autoAcceptConnection?: boolean
threadId?: string
tags?: CustomConnectionTags
imageUrl?: string
multiUseInvitation: boolean
}

Expand Down Expand Up @@ -60,6 +61,7 @@ export class ConnectionRecord
public invitation?: ConnectionInvitationMessage
public alias?: string
public autoAcceptConnection?: boolean
public imageUrl?: string
public multiUseInvitation!: boolean

public threadId?: string
Expand All @@ -86,6 +88,7 @@ export class ConnectionRecord
this._tags = props.tags ?? {}
this.invitation = props.invitation
this.threadId = props.threadId
this.imageUrl = props.imageUrl
this.multiUseInvitation = props.multiUseInvitation
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ export class ConnectionService {
recipientKeys: service.recipientKeys,
serviceEndpoint: service.serviceEndpoint,
routingKeys: service.routingKeys,
imageUrl: this.config.connectionImageUrl,
})

connectionRecord.invitation = invitation
Expand Down Expand Up @@ -129,6 +130,7 @@ export class ConnectionService {
autoAcceptConnection: config?.autoAcceptConnection,
routing: config.routing,
invitation,
imageUrl: invitation.imageUrl,
tags: {
invitationKey: invitation.recipientKeys && invitation.recipientKeys[0],
},
Expand Down Expand Up @@ -162,6 +164,7 @@ export class ConnectionService {
label: this.config.label,
did: connectionRecord.did,
didDoc: connectionRecord.didDoc,
imageUrl: this.config.connectionImageUrl,
})

await this.updateState(connectionRecord, ConnectionState.Requested)
Expand Down Expand Up @@ -227,6 +230,7 @@ export class ConnectionService {
connectionRecord.theirLabel = message.label
connectionRecord.threadId = message.id
connectionRecord.theirDid = message.connection.did
connectionRecord.imageUrl = message.imageUrl

if (!connectionRecord.theirKey) {
throw new AriesFrameworkError(`Connection with id ${connectionRecord.id} has no recipient keys.`)
Expand Down Expand Up @@ -577,6 +581,7 @@ export class ConnectionService {
autoAcceptConnection?: boolean
multiUseInvitation: boolean
tags?: CustomConnectionTags
imageUrl?: string
}): Promise<ConnectionRecord> {
const { endpoints, did, verkey, routingKeys } = options.routing

Expand Down Expand Up @@ -621,6 +626,7 @@ export class ConnectionService {
alias: options.alias,
theirLabel: options.theirLabel,
autoAcceptConnection: options.autoAcceptConnection,
imageUrl: options.imageUrl,
multiUseInvitation: options.multiUseInvitation,
})

Expand Down
1 change: 1 addition & 0 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export interface InitConfig {
mediatorPickupStrategy?: MediatorPickupStrategy

useLegacyDidSovPrefix?: boolean
connectionImageUrl?: string
}

export interface UnpackedMessage {
Expand Down