Skip to content

Commit

Permalink
Merge pull request #105 from jolocom/feat/exports
Browse files Browse the repository at this point in the history
Export/Import support
  • Loading branch information
mnzaki authored Jun 15, 2021
2 parents 2346eba + e265ca0 commit a24cc42
Show file tree
Hide file tree
Showing 12 changed files with 446 additions and 127 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@jolocom/sdk",
"version": "1.2.0",
"version": "1.3.0-rc2",
"main": "js/index.js",
"files": [
"js/"
Expand All @@ -25,7 +25,7 @@
"@babel/preset-typescript": "^7.10.4",
"@babel/runtime": "^7.11.2",
"@jolocom/local-resolver-registrar": "^1.0.1",
"@jolocom/sdk-storage-typeorm": "^4.2.0",
"@jolocom/sdk-storage-typeorm": "^4.3.0-rc0",
"@types/jest": "^26.0.10",
"@types/node": "^13.9.8",
"@types/node-fetch": "^2.5.5",
Expand Down
13 changes: 12 additions & 1 deletion src/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ import { CredentialOfferRequest } from 'jolocom-lib/js/interactionTokens/credent
import { CredentialsReceive } from 'jolocom-lib/js/interactionTokens/credentialsReceive'
import { Authentication } from 'jolocom-lib/js/interactionTokens/authentication'
import { CredentialIssuer } from './credentials'
import { DeleteAgentOptions } from './types'
import { DeleteAgentOptions, ExportAgentOptions, IExportedAgent } from './types'

/**
* The `Agent` class mainly provides an abstraction around the {@link
Expand Down Expand Up @@ -212,6 +212,7 @@ export class Agent {
this.resolver,
)

// ???? TODO FIXME
await this.storage.store.identity(identityWallet.identity)

// This sets the didMethod so that it doesn't return a different value if
Expand Down Expand Up @@ -692,4 +693,14 @@ export class Agent {
public async delete(options?: DeleteAgentOptions) {
await this.sdk.deleteAgent(this.idw.did, options)
}


public async export(opts?: ExportAgentOptions): Promise<IExportedAgent> {
return this.sdk.exportAgent(this, opts)
}

public async import(exagent: IExportedAgent, opts?: ExportAgentOptions): Promise<void> {
if (this.idw.did !== exagent.did) throw new SDKError(ErrorCode.Unknown)
await this.sdk.importAgent(exagent, opts)
}
}
176 changes: 141 additions & 35 deletions src/credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ import {
} from '@jolocom/protocol-ts'
import { jsonpath } from './util'
import { QueryOptions, IStorage, CredentialQuery } from './storage'
import { JolocomLib } from 'jolocom-lib'
import { SignedCredential } from 'jolocom-lib/js/credentials/signedCredential/signedCredential'
import { Agent } from './agent'
import { ObjectKeeper, CredentialMetadataSummary, IdentitySummary } from './types'
import { IResolver } from 'jolocom-lib/js/didMethods/types'
import { validateJsonLd } from 'jolocom-lib/js/linkedData'
import { SDKError, ErrorCode } from './errors'

export interface DisplayVal {
label?: string
Expand Down Expand Up @@ -51,6 +53,17 @@ export class CredentialType {
this.issuerProfile = metadata?.issuer
}

summary(): CredentialMetadataSummary {
return {
type: this.type[1],
issuer: this.issuerProfile!,
renderInfo: {
renderAs: this.renderAs
},
credential: this.definition
}
}

display(claim: ClaimEntry): CredentialDisplay {
const display: CredentialDisplay['display'] = {
properties: [],
Expand Down Expand Up @@ -102,24 +115,117 @@ export class CredentialType {
}
}

export class CredentialTypeKeeper
implements
ObjectKeeper<
CredentialType,
CredentialMetadataSummary,
CredentialQuery
> {

constructor(
protected credKeeper: CredentialKeeper,
protected storage: IStorage,
) { }

buildId(issuer: string, credentialType: string | string[]): string {
if (typeof credentialType === 'string') {
return `${issuer}${credentialType}`
}

return `${issuer}${credentialType[credentialType.length - 1]}`
}

getFullCredentialTypeList(credType: string | string[]): string[] {
if (!credType) throw new Error('credential type required')
// NOTE: 'VerifiableCredential' currently implied in the lib/protocol
if (Array.isArray(credType)) {
if (credType[0] !== 'VerifiableCredential') {
return ['VerifiableCredential', ...credType]
} else {
return credType
}
} else {
return ['VerifiableCredential', credType]
}
}

getByIssuerAndType(issuerDid: string, credType: string | string[]) {
const fullCredType = this.getFullCredentialTypeList(credType)
return this.get(this.buildId(issuerDid, fullCredType), issuerDid, fullCredType)
}

async get(id: string, issuerDid?: string, fullCredType?: string | string[]) {
const meta = await this.storage.get.credentialMetadataById(id)
// NOTE: sometimes there's no issuer data stored...
issuerDid = issuerDid || meta.issuer?.did
fullCredType = this.getFullCredentialTypeList(fullCredType || meta.type)

if (!meta.issuer?.publicProfile) {
try {
meta.issuer = await this.storage.get.publicProfile(issuerDid)
} catch(err) {
console.error(`could not lookup issuer ${issuerDid}`, err)
// pass
}
}
return new CredentialType(fullCredType, meta)
}

async create(meta: CredentialMetadataSummary) {
const fullCredType = this.getFullCredentialTypeList(meta.type)
await this.storage.store.credentialMetadata(meta)
if (meta.issuer?.publicProfile) {
await this.storage.store.issuerProfile(meta.issuer)
}
return new CredentialType(fullCredType, meta)
}

async forCredential(cred: SignedCredential): Promise<CredentialType> {
return this.getByIssuerAndType(cred.issuer, cred.type)
}

async export(query?: CredentialQuery, options?: QueryOptions): Promise<CredentialMetadataSummary[]> {
const creds = await this.credKeeper.query(query, options)
const credTypes = await Promise.all(creds.map(c => this.forCredential(c)))
return credTypes.map(credType => credType.summary())
}

async import(data: CredentialMetadataSummary[]): Promise<[CredentialMetadataSummary, SDKError][]> {
const rejected: [CredentialMetadataSummary, SDKError][] = []
await Promise.all(data.map(async credMeta => {
try {
await this.create(credMeta)
} catch (err) {
console.error("credential metadata import failed", credMeta, err)
// TODO better error breakdown
err = err instanceof SDKError ? err : new SDKError(ErrorCode.Unknown, err)
rejected.push([credMeta, err])
}
}))
return rejected
}
}

export class CredentialKeeper
implements
ObjectKeeper<
SignedCredential,
ISignedCredCreationArgs<any>,
CredentialQuery
CredentialQuery,
ISignedCredentialAttrs
> {
protected storage: IStorage
protected resolver: IResolver

readonly types: CredentialTypeKeeper

private _applyFilter: () => CredentialQuery | undefined

constructor(
storage: IStorage,
resolver: IResolver,
protected storage: IStorage,
protected resolver: IResolver,
filter?: CredentialQuery | (() => CredentialQuery),
) {
this.storage = storage
this.resolver = resolver
this.types = new CredentialTypeKeeper(this, this.storage)
this._applyFilter = typeof filter === 'function' ? filter : () => filter
}

Expand All @@ -145,6 +251,32 @@ export class CredentialKeeper
})
}

async export(query?: CredentialQuery, options?: QueryOptions): Promise<ISignedCredentialAttrs[]> {
const creds = await this.query(query, options)
//const credTypes = await Promise.all(creds.map(c => this.getCredentialType(c)))
//const metas = credTypes.map(credType => credType.summary())

// NOTE: reversing here to make imports reproduce the same table order
return creds.reverse().map(c => c.toJSON())
}

async import(data: ISignedCredentialAttrs[]): Promise<[ISignedCredentialAttrs, SDKError][]> {
const rejected: [ISignedCredentialAttrs, SDKError][] = []
await Promise.all(data.map(async credJson => {
try {
const signer = await this.resolver.resolve(credJson.issuer)
const cred = await JolocomLib.parseAndValidate.signedCredential(credJson, signer)
await this.storage.store.verifiableCredential(cred)
} catch (err) {
console.error("credential import failed", err)
// TODO better error breakdown
err = err instanceof SDKError ? err : new SDKError(ErrorCode.Unknown, err)
rejected.push([credJson, err])
}
}))
return rejected
}

async delete(attrs?: CredentialQuery) {
// we use this.find to apply the filter if any
const creds = await this.query(attrs)
Expand All @@ -154,31 +286,8 @@ export class CredentialKeeper
return true
}

async storeCredentialType(metadata: CredentialMetadataSummary) {
await this.storage.store.credentialMetadata(metadata)
await this.storage.store.issuerProfile(metadata.issuer)
}

async getCredentialType(cred: SignedCredential): Promise<CredentialType> {
const metadata = await this.storage.get.credentialMetadata(cred)

if (!metadata.issuer) {
try {
metadata.issuer = await this.storage.get.publicProfile(cred.issuer)
} catch(err) {
console.error(`could not lookup issuer ${cred.issuer}`, err)
// pass
}
}

return new CredentialType(
cred.type,
metadata
)
}

async display(cred: SignedCredential): Promise<CredentialDisplay> {
const credType = await this.getCredentialType(cred)
const credType = await this.types.forCredential(cred)
return credType.display(cred.claim)
}

Expand All @@ -192,14 +301,11 @@ export class CredentialKeeper
}

export class CredentialIssuer extends CredentialKeeper {
private agent: Agent

constructor(
agent: Agent,
private agent: Agent,
filter?: CredentialQuery | (() => CredentialQuery),
) {
super(agent.storage, agent.resolver, filter)
this.agent = agent
}

/**
Expand Down
89 changes: 86 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SoftwareKeyProvider } from 'jolocom-lib'
import { SoftwareKeyProvider, JolocomLib } from 'jolocom-lib'
import { SDKError, ErrorCode } from './errors'
export { SDKError, ErrorCode }

Expand All @@ -14,8 +14,8 @@ import { Identity } from 'jolocom-lib/js/identity/identity'
import { Agent } from './agent'
import { TransportKeeper } from './transports'
import { CredentialKeeper } from './credentials'
import { DeleteAgentOptions } from './types'
import { getDeleteAgentOptions } from './util'
import { DeleteAgentOptions, ExportAgentOptions, ExportedAgentData, IExportedAgent, EXPORT_SCHEMA_VERSION } from './types'
import { getDeleteAgentOptions, getExportAgentOptions } from './util'
export { Agent } from './agent'

export * from './types'
Expand Down Expand Up @@ -223,6 +223,89 @@ export class JolocomSDK {
return agent
}

/**
* Export Agent as a serializable JSON object
*
* @param agent - the agent to export
* @param options - export options
*
* @category Agent
*/
public async exportAgent(agent: Agent, options?: ExportAgentOptions): Promise<IExportedAgent> {
options = getExportAgentOptions(options)

let exagent: IExportedAgent = {
version: EXPORT_SCHEMA_VERSION,
did: agent.idw.did,
timestamp: Date.now(),
data: ''
}
const encryptedWalletInfo = await agent.storage.get.encryptedWallet(exagent.did)
const interxnTokens = await agent.storage.get.interactionTokens()
const exportedData: ExportedAgentData = {
encryptedWallet: encryptedWalletInfo?.encryptedWallet,
}

if (options.credentials) {
exportedData.credentials = await agent.credentials.export()
exportedData.credentialsMetadata = await agent.credentials.types.export()
}

if (options.interactions) {
exportedData.interactionTokens = interxnTokens.map(t => t.encode())
}

const agentData = Buffer.from(JSON.stringify(exportedData))
exagent.data = agentData.toString('base64')

return exagent
}


/**
* Import a previously exported Agent, adding its data to the database and
* loading it immediately
*
* @param exagent - the exported agent to export
* @param options - import options, including password
*
* @category Agent
*/
public async importAgent(exagent: IExportedAgent, options?: ExportAgentOptions): Promise<Agent> {
options = getExportAgentOptions(options)
const agentData: ExportedAgentData = JSON.parse(Buffer.from(exagent.data, 'base64').toString())

let encryptedWallet = await this.storage.get.encryptedWallet(exagent.did)
if (!encryptedWallet) {
if (!agentData.encryptedWallet) throw new SDKError(ErrorCode.NoWallet)
await this.storage.store.encryptedWallet({
id: exagent.did,
timestamp: exagent.timestamp,
encryptedWallet: agentData.encryptedWallet
})
}
const agent = await this.loadAgent(options.password, exagent.did)

// TODO: check for rejected imports
if (agentData.credentialsMetadata)
await agent.credentials.types.import(agentData.credentialsMetadata)
if (agentData.credentials)
await agent.credentials.import(agentData.credentials)
if (agentData.interactions) {
throw new Error('todo') // TODO
}
if (agentData.interactionTokens) {
// TODO add batch insert support on storage
await Promise.all(agentData.interactionTokens.map(jwt => {
const token = JolocomLib.parse.interactionToken.fromJWT(jwt)
return agent.storage.store.interactionToken(token)
}))
// TODO return rejected stuff?
}

return agent
}

/**
* Create an Agent instance with an Identity loaded from storage or create a
* new Identity if not found.
Expand Down
Loading

0 comments on commit a24cc42

Please sign in to comment.