Skip to content

Commit

Permalink
feat(core): support image url in invitations (#463)
Browse files Browse the repository at this point in the history
* feat(core): support image url in invitations

non-standard approach for showing images with connections. Almost all wallets and
implementations support this (ACA-Py, AF.NET, Connect.Me, Lissi, Trinsic, etc...)

Signed-off-by: Timo Glastra <[email protected]>

* docs: add note about divergence from spec

Signed-off-by: Timo Glastra <[email protected]>
  • Loading branch information
TimoGlastra authored Oct 28, 2021
1 parent 336baa0 commit 9fda24e
Show file tree
Hide file tree
Showing 8 changed files with 61 additions and 19 deletions.
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

0 comments on commit 9fda24e

Please sign in to comment.