Skip to content

Commit

Permalink
feat(coordinate-mediation): implement did-comm coordinate-mediation v…
Browse files Browse the repository at this point in the history
…3.0 (#1282)

Implement coordinate mediation from v3 (https://didcomm.org/coordinate-mediation/3.0/)
 - add recipient update
 - add recipient update response
 - add recipient query
 - add recipient
add grant or deny control mechanism
 - add default logic
 - add TODO for injection of grant/deny mechanism logic
add datastore for:
 - mediation
 - recipient did
 - mediation policy
define types:
 - IDataStore
 - IMediation
 - IMediationPolicy
 - IRecipientDid
add tests for new functionality
 - modify mediate test suite
 - add recipient update test suite
 - add recipient query test suite
documentation:
 - add documentation on introduced methods
cli:
 - add mediate allow-from command
 - add mediate deny-from command
 - add mediate list
 - add mediate remove

---------

Co-authored-by: Paul Parker
  • Loading branch information
radleylewis authored Dec 3, 2023
1 parent d89a4dd commit 462735d
Show file tree
Hide file tree
Showing 39 changed files with 3,142 additions and 67 deletions.
2 changes: 1 addition & 1 deletion __tests__/localAgent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ const setup = async (options?: IAgentOptions): Promise<boolean> => {
new SdrMessageHandler(),
],
}),
new DIDComm([new DIDCommHttpTransport()]),
new DIDComm({ transports: [new DIDCommHttpTransport()]}),
new CredentialPlugin(),
new CredentialIssuerEIP712(),
new CredentialIssuerLD({
Expand Down
2 changes: 1 addition & 1 deletion __tests__/restAgent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ const setup = async (options?: IAgentOptions): Promise<boolean> => {
new SdrMessageHandler(),
],
}),
new DIDComm([new DIDCommHttpTransport()]),
new DIDComm({ transports: [new DIDCommHttpTransport()]}),
// intentionally use the deprecated name to test compatibility
new CredentialIssuer(),
new CredentialIssuerEIP712(),
Expand Down
1 change: 1 addition & 0 deletions packages/cli/default/client.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,4 @@ agent:
- createSelectiveDisclosureRequest
- getVerifiableCredentialsForSdr
- validatePresentationAgainstSdr

2 changes: 2 additions & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,11 @@
"@veramo/message-handler": "workspace:^",
"@veramo/remote-client": "workspace:^",
"@veramo/remote-server": "workspace:^",
"@veramo/kv-store": "workspace:^",
"@veramo/selective-disclosure": "workspace:^",
"@veramo/url-handler": "workspace:^",
"@veramo/utils": "workspace:^",
"@veramo/mediation-manager": "workspace:^",
"blessed": "^0.1.81",
"commander": "^11.0.0",
"console-table-printer": "^2.11.2",
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/createCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { presentation } from './presentation.js'
import { explore } from './explore/index.js'
import { sdr } from './sdr.js'
import { server } from './server.js'
import { mediate } from './mediate.js'

const requireCjs = module.createRequire(import.meta.url)
const { version } = requireCjs('../package.json')
Expand All @@ -30,5 +31,6 @@ const veramo = new Command('veramo')
.addCommand(presentation)
.addCommand(sdr)
.addCommand(server)
.addCommand(mediate)

export { veramo }
201 changes: 201 additions & 0 deletions packages/cli/src/mediate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
import { Command } from 'commander'
import inquirer from 'inquirer'

import { getAgent } from './setup.js'
import { PreMediationRequestPolicy, RecipientDid, RequesterDid } from '@veramo/mediation-manager'

type ConfiguredAgent = Awaited<ReturnType<typeof getAgent>>

const ALLOW = 'ALLOW'
const DENY = 'DENY'

type Options = Partial<{
granted: boolean
denied: boolean
allowFrom: boolean
denyFrom: boolean
interactive: boolean
fileJson: string
}>

type UpdatePolicyParams = {
dids: string[]
agent: ConfiguredAgent
policy?: PreMediationRequestPolicy
remove?: boolean
}

/**
* private functions
**/

const updatePolicies = async (options: UpdatePolicyParams): Promise<void> => {
const { dids, agent, policy, remove = false } = options
if (remove) {
return dids.forEach(
async (requesterDid) => await agent.mediationManagerRemoveMediationPolicy({ requesterDid }),
)
}
if (!policy) throw new Error('No policy provided')
return dids.forEach(
async (requesterDid) => await agent.mediationManagerSaveMediationPolicy({ requesterDid, policy }),
)
}

const promptForDids = async (action: string): Promise<string[]> => {
const { dids } = await inquirer.prompt<{ dids: string }>({
type: 'input',
name: 'dids',
message: `Enter the dids you want to ${action.toLowerCase()} separated by spaces:`,
})
return dids.split(' ')
}

/**
* cli action functions
**/

const policy = (policy: PreMediationRequestPolicy) => {
return async function (
{ fileJson, interactive }: Pick<Options, 'fileJson' | 'interactive'>,
cmd: Command,
): Promise<void> {
try {
if (fileJson && interactive) throw new Error('Please specify only one input method')

const agent = await getAgent(cmd.optsWithGlobals().config)
console.log('AGENT CREATED')

if (fileJson) {
const jsonData = await import(fileJson, { assert: { type: 'json' } })
const dids = jsonData.default
await updatePolicies({ dids, agent, policy })
} else if (interactive) {
const dids = await promptForDids(policy)
await updatePolicies({ dids, agent, policy })
} else {
const dids = cmd.args
await updatePolicies({ dids, agent, policy })
}

console.log('Mediation policies updated')
} catch (e) {
console.error(e.message)
}
}
}

async function readPolicies(options: Pick<Options, 'interactive' | 'fileJson'>, cmd: Command): Promise<void> {
const agent = await getAgent(cmd.optsWithGlobals().config)
let dids: string[]
if (options.interactive) dids = await promptForDids('read')
else if (options.fileJson) dids = (await import(options.fileJson, { assert: { type: 'json' } })).default
else dids = cmd.args
if (!dids || !dids.length) throw new Error('No dids provided')
const policies: Record<RequesterDid, RecipientDid | null> = {}
for await (const requesterDid of dids) {
policies[requesterDid] = await agent.mediationManagerGetMediationPolicy({ requesterDid })
}
console.log('POLICIES')
console.table(policies)
}

async function listPolicies(options: Pick<Options, 'allowFrom' | 'denyFrom'>, cmd: Command): Promise<void> {
try {
const agent = await getAgent(cmd.optsWithGlobals().config)
const res = await agent.mediationManagerListMediationPolicies()
console.log('POLICIES')
if (options.allowFrom) return console.table(Object.entries(res).filter(([, policy]) => policy === ALLOW))
if (options.denyFrom) return console.table(Object.entries(res).filter(([, policy]) => policy === DENY))
else console.table(res)
} catch (e) {
console.error(e.message)
}
}

async function listResponses(
{ granted, denied }: Pick<Options, 'granted' | 'denied'>,
cmd: Command,
): Promise<void> {
try {
const agent = await getAgent(cmd.optsWithGlobals().config)
const res = await agent.mediationManagerGetAllMediations()
console.log('MEDIATIONS')
if (granted) return console.table(Object.entries(res).filter(([, response]) => response === 'GRANTED'))
if (denied) return console.table(Object.entries(res).filter(([, response]) => response === 'DENIED'))
else console.table(res)
} catch (e) {
console.error(e.message)
}
}

async function removePolicies(
{ fileJson, interactive }: Pick<Options, 'fileJson' | 'interactive'>,
cmd: Command,
): Promise<void> {
try {
const agent = await getAgent(cmd.optsWithGlobals().config)

if (fileJson) {
const jsonData = await import(fileJson, { assert: { type: 'json' } })
const dids = jsonData.default
await updatePolicies({ dids, remove: true, agent })
} else if (interactive) {
const dids = await promptForDids('Remove')
await updatePolicies({ dids, remove: true, agent })
} else {
const dids = cmd.args
await updatePolicies({ dids, remove: true, agent })
}

console.log('Mediation policies removed')
} catch (e) {
console.error(e.message)
}
}

const mediate = new Command('mediate').description('Mediate allow or deny policy on dids')

mediate
.command('allow-from')
.description('add dids that should be allowed for mediation')
.option('-f, --file-json <string>', 'read dids from json file')
.option('-i, --interactive', 'interactively input dids')
.action(policy(ALLOW))

mediate
.command('deny-from')
.description('deny dids that should be denied for mediation')
.option('-f, --file-json <string>', 'read dids from json file')
.option('-i, --interactive', 'interactively input dids')
.action(policy(DENY))

mediate
.command('read')
.description('read mediation policy for a specific did')
.option('-i, --interactive', 'interactively input dids')
.option('-f, --file-json <string>', 'read dids from json file')
.action(readPolicies)

mediate
.command('list-policies')
.description('list mediation policies')
.option('-a, --allow-from', 'list allow policies')
.option('-d, --deny-from', 'list deny policies')
.action(listPolicies)

mediate
.command('list-responses')
.description('list mediation responses')
.option('-a, --granted', 'list granted policies')
.option('-d, --denied', 'list denied policies')
.action(listResponses)

mediate
.command('remove')
.description('remove mediation policies')
.option('-f, --file-json <string>', 'read dids from json file')
.option('-i, --interactive', 'interactively input dids')
.action(removePolicies)

export { mediate }
7 changes: 6 additions & 1 deletion packages/cli/src/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ import {
IResolver,
TAgent,
} from '@veramo/core-types'
import {
IMediationManager,

} from "@veramo/mediation-manager"
import { ISelectiveDisclosure } from '@veramo/selective-disclosure'
import { IDIDComm } from '@veramo/did-comm'
import { IDIDDiscovery } from '@veramo/did-discovery'
Expand Down Expand Up @@ -57,7 +61,8 @@ export type EnabledInterfaces = IDIDManager &
IDIDComm &
ICredentialPlugin &
ISelectiveDisclosure &
IDIDDiscovery
IDIDDiscovery &
IMediationManager

export type ConfiguredAgent = TAgent<EnabledInterfaces>

Expand Down
19 changes: 10 additions & 9 deletions packages/data-store-json/src/data-store-json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,6 @@ export class DataStoreJson implements IAgentPlugin {
dataStoreDeleteVerifiableCredential: this.dataStoreDeleteVerifiableCredential.bind(this),
dataStoreSaveVerifiablePresentation: this.dataStoreSaveVerifiablePresentation.bind(this),
dataStoreGetVerifiablePresentation: this.dataStoreGetVerifiablePresentation.bind(this),
//dataStoreDeleteVerifiablePresentation: this.dataStoreDeleteVerifiablePresentation.bind(this),

// IDataStoreORM methods
dataStoreORMGetIdentifiers: this.dataStoreORMGetIdentifiers.bind(this),
Expand Down Expand Up @@ -295,13 +294,15 @@ export class DataStoreJson implements IAgentPlugin {
expirationDate = new Date(vp.expirationDate)
}

const credentials: VerifiableCredential[] = asArray(vp.verifiableCredential).map((cred: W3CVerifiableCredential) => {
if (typeof cred === 'string') {
return normalizeCredential(cred)
} else {
return <VerifiableCredential>cred
}
})
const credentials: VerifiableCredential[] = asArray(vp.verifiableCredential).map(
(cred: W3CVerifiableCredential) => {
if (typeof cred === 'string') {
return normalizeCredential(cred)
} else {
return <VerifiableCredential>cred
}
},
)

const presentation: PresentationTableEntry = {
hash,
Expand Down Expand Up @@ -617,7 +618,7 @@ function buildQuery<T extends Partial<Record<PossibleColumns, any>>>(
filteredCollection = filteredCollection.slice(input.skip)
}
if (input.take) {
const start = input.skip && input.skip - 1 || 0
const start = (input.skip && input.skip - 1) || 0
const end = start + input.take
filteredCollection = filteredCollection.slice(start, end)
}
Expand Down
12 changes: 6 additions & 6 deletions packages/data-store/src/data-store-orm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export class DataStoreORM implements IAgentPlugin {

private async identifiersQuery(
args: FindArgs<TIdentifiersColumns>,
context: AuthorizedDIDContext,
context?: AuthorizedDIDContext,
): Promise<SelectQueryBuilder<Identifier>> {
const where = createWhereObject(args)
let qb = (await getConnectedDb(this.dbConnection))
Expand All @@ -101,7 +101,7 @@ export class DataStoreORM implements IAgentPlugin {
args: FindArgs<TIdentifiersColumns>,
context: AuthorizedDIDContext,
): Promise<PartialIdentifier[]> {
const identifiers = await (await this.identifiersQuery(args, context)).getMany()
const identifiers = await (await this.identifiersQuery(args)).getMany()
return identifiers.map((i) => {
const identifier: PartialIdentifier = i as PartialIdentifier
if (identifier.controllerKeyId === null) {
Expand Down Expand Up @@ -416,11 +416,11 @@ function decorateQB(

if (input?.order) {
for (const item of input.order) {
qb = qb.addSelect(qb.connection.driver.escape(tableName) + '.' + qb.connection.driver.escape(item.column), item.column)
qb = qb.orderBy(
qb.connection.driver.escape(item.column),
item.direction,
qb = qb.addSelect(
qb.connection.driver.escape(tableName) + '.' + qb.connection.driver.escape(item.column),
item.column,
)
qb = qb.orderBy(qb.connection.driver.escape(item.column), item.direction)
}
}
return qb
Expand Down
12 changes: 11 additions & 1 deletion packages/data-store/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,16 @@ export const Entities = [
PrivateKey,
PreMigrationKey,
]

/**
* Helper function to concatenate multiple arrays of TypeORM entities.
*
* This array CAN be used when creating a TypeORM connection.
*
* @public
*/
export const entitiesConcat = (...entityArrays: unknown[][]) =>
entityArrays.reduce((acc, entityArray) => acc.concat(entityArray), [])
export {
KeyType,
Key,
Expand All @@ -56,7 +66,7 @@ export {
PrivateKey,
PreMigrationKey,
}
export { migrations } from './migrations/index.js'
export { migrations, migrationConcat } from './migrations/index.js'

// re-export the interfaces that were moved to core for backward compatibility
export { IDataStore, IDataStoreORM } from '@veramo/core-types'
5 changes: 2 additions & 3 deletions packages/data-store/src/migrations/1.createDatabase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ const debug = Debug('veramo:data-store:initial-migration')
* @public
*/
export class CreateDatabase1447159020001 implements MigrationInterface {

name = 'CreateDatabase1447159020001' // Used in case this class gets minified, which would change the classname

async up(queryRunner: QueryRunner): Promise<void> {
Expand All @@ -21,7 +20,7 @@ export class CreateDatabase1447159020001 implements MigrationInterface {
// "CREATE UNIQUE INDEX \"IDX_6098cca69c838d91e55ef32fe1\" ON \"identifier\" (\"alias\", \"provider\")",
await queryRunner.createTable(
new Table({
name: migrationGetTableName(queryRunner,'identifier'),
name: migrationGetTableName(queryRunner, 'identifier'),
columns: [
{ name: 'did', type: 'varchar', isPrimary: true },
{ name: 'provider', type: 'varchar', isNullable: true },
Expand All @@ -44,7 +43,7 @@ export class CreateDatabase1447159020001 implements MigrationInterface {
// "CREATE TABLE \"key\" (\"kid\" varchar PRIMARY KEY NOT NULL, \"kms\" varchar NOT NULL, \"type\" varchar NOT NULL, \"publicKeyHex\" varchar NOT NULL, \"privateKeyHex\" varchar NOT NULL, \"meta\" text, \"identifierDid\" varchar, CONSTRAINT \"FK_3f40a9459b53adf1729dbd3b787\" FOREIGN KEY (\"identifierDid\") REFERENCES \"identifier\" (\"did\") ON DELETE NO ACTION ON UPDATE NO ACTION)",
await queryRunner.createTable(
new Table({
name: migrationGetTableName(queryRunner,'key'),
name: migrationGetTableName(queryRunner, 'key'),
columns: [
{ name: 'kid', type: 'varchar', isPrimary: true },
{ name: 'kms', type: 'varchar' },
Expand Down
Loading

0 comments on commit 462735d

Please sign in to comment.