Skip to content

Commit

Permalink
allow multi-cred upload/setup
Browse files Browse the repository at this point in the history
  • Loading branch information
jchartrand committed Sep 11, 2023
1 parent 29de49d commit d5f1432
Show file tree
Hide file tree
Showing 5 changed files with 124 additions and 43 deletions.
4 changes: 2 additions & 2 deletions src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ export async function build(opts = {}) {
try {
const data = req.body;
if (!data || !Object.keys(data).length) return res.status(400).send({code: 400, message: 'No data was provided in the body.'})
const walletQuery = await setupExchange(data)
return res.json(walletQuery)
const walletQuerys = await setupExchange(data)
return res.json(walletQuerys)
} catch (error) {
console.log(error);
return res.status(error.code || 500).json(error);
Expand Down
11 changes: 11 additions & 0 deletions src/app.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { expect } from 'chai'
import request from 'supertest';
import { build } from './app.js';
import { getDataForExchangeSetupPost } from './test-fixtures/testData.js';

let app

Expand Down Expand Up @@ -37,7 +38,17 @@ describe('api', () => {
.expect(400, done)
})

it('returns array of wallet queries', async () => {
const testData = getDataForExchangeSetupPost('test')
const response = await request(app)
.post("/exchange")
.send(testData)

expect(response.header["content-type"]).to.have.string("json");
expect(response.status).to.eql(200);
expect(response.body)
expect(response.body.length).to.eql(testData.data.length)
})



Expand Down
15 changes: 15 additions & 0 deletions src/test-fixtures/testData.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import testVC from './testVC.js';

const getDataForExchangeSetupPost = (tenantName) => {
const fakeData = {
tenantName,
exchangeHost: 'http://localhost:4005',
data: [
{ vc: testVC, retrievalId: 'someId', },
{ vc: testVC, retrievalId: 'blah' }
]
}
return fakeData
}

export { getDataForExchangeSetupPost }
37 changes: 37 additions & 0 deletions src/test-fixtures/testVC.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
export default {
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://purl.imsglobal.org/spec/ob/v3p0/context.json",
"https://w3id.org/vc/status-list/2021/v1"
],
"id": "urn:uuid:951b475e-b795-43bc-ba8f-a2d01efd2eb1",
"type": [
"VerifiableCredential",
"OpenBadgeCredential"
],
"issuer": {
"id": "did:key:z6MkhVTX9BF3NGYX6cc7jWpbNnR7cAjH8LUffabZP8Qu4ysC",
"type": "Profile",
"name": "University of Wonderful",
"description": "The most wonderful university",
"url": "https://wonderful.edu/",
"image": {
"id": "https://user-images.githubusercontent.com/947005/133544904-29d6139d-2e7b-4fe2-b6e9-7d1022bb6a45.png",
"type": "Image"
}
},
"issuanceDate": "2020-01-01T00:00:00Z",
"name": "A Simply Wonderful Course",
"credentialSubject": {
"type": "AchievementSubject",
"achievement": {
"id": "http://wonderful.wonderful",
"type": "Achievement",
"criteria": {
"narrative": "Completion of the Wonderful Course - well done you!"
},
"description": "Wonderful.",
"name": "Introduction to Wonderfullness"
}
}
}
100 changes: 59 additions & 41 deletions src/transactionManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,37 +13,25 @@ export const initializeTransactionManager = () => {
}

/**
* @param {Object} data Data needed for the exchange
* @param {Object} [data.vc] optional - an unsigned populated VC
* @param {Object} [data.subjectData] optional - data to populate a VC
* @param {string} data.exchangeHost hostname for the exchange endpoints
* @param {string} [data.tenantName] tenant with which to sign
* @param {string} [data.batchId] batch to which cred belongs; also determines vc template
* @param {Array} data Array of data items, one per credential, with data needed for the exchange
* @param {Object} [item.vc] optional - an unsigned populated VC
* @param {Object} [item.subjectData] optional - data to populate a VC
* @param {string} item.exchangeHost hostname for the exchange endpoints
* @param {string} [item.tenantName] tenant with which to sign
* @param {string} [item.batchId] batch to which cred belongs; also determines vc template
* @param {string} item.retrievalId an identier for ech record, e.g., the recipient's email address
*
* @returns {Object} deeplink/chapi queries with which to open a wallet for this exchange
*/
export const setupExchange = async (data) => {

// Throws an ExchangeError if exchange data is incomplete.
// The error bubbles up to the express handler
verifyExchangeData(data)

data.transactionId = crypto.randomUUID()
data.exchangeId = crypto.randomUUID()

await keyv.set(data.exchangeId, data, expiresAfter);

// directDeepLink bypasses the VPR step and assumes the wallet knows to send a DIDAuth.
const directDeepLink = `https://lcw.app/request.html?issuer=issuer.example.com&auth_type=bearer&challenge=${data.transactionId}&vc_request_url=${data.exchangeHost}/exchange/${data.exchangeId}/${data.transactionId}`

//vprDeepLink = deeplink that calls /exchanges/${exchangeId} to initiate the exchange
// and get back a VPR to which to then send the DIDAuth.
const vprDeepLink = `https://lcw.app/request.html?issuer=issuer.example.com&auth_type=bearer&vc_request_url=${data.exchangeHost}/exchange/${data.exchangeId}`
return [
{ type: "directDeepLink", url: directDeepLink },
{ type: "vprDeepLink", url: vprDeepLink },
{ type: "chapi", query: getDIDAuthVPR(data.exchangeId)}
]
export const setupExchange = async (exchangeData) => {
verifyExchangeData(exchangeData)
// sets up an exchange ID in keyv for each record, and returns an array of objects
// where each object contains a choice of wallet queries for the exchange.
// A wallet query is either a deeplink, or a VPR for use with CHAPI.
const exchangeHost = exchangeData.exchangeHost;
const tenantName = exchangeData.tenantName;
const processRecord = getProcessRecordFnForExchangeHostAndTenant(exchangeHost, tenantName)
return await Promise.all(exchangeData.data.map(processRecord))
}


Expand All @@ -63,7 +51,7 @@ export const getDIDAuthVPR = async (exchangeId) => {
{
"type": "VerifiableCredentialApiExchangeService",
"serviceEndpoint": `${exchangeData.exchangeHost}/exchange/${exchangeData.exchangeId}/${exchangeData.transactionId}`
// "serviceEndpoint": "https://playground.chapi.io/exchanges/eyJjcmVkZW50aWFsIjoiaHR0cHM6Ly9wbGF5Z3JvdW5kLmNoYXBpLmlvL2V4YW1wbGVzL2pmZjIvamZmMi5qc29uIiwiaXNzdWVyIjoiZGIvdmMifQ/esOGVHG8d44Q"
// "serviceEndpoint": "https://playground.chapi.io/exchanges/eyJjcmVkZW50aWFsIjoiaHR0cHM6Ly9wbGF5Z3JvdW5kLmNoYXBpLmlvL2V4YW1wbGVzL2pmZjIvamZmMi5qc29uIiwiaXNzdWVyIjoiZGIvdmMifQ/esOGVHG8d44Q"
},
{
"type": "CredentialHandlerService"
Expand All @@ -75,6 +63,29 @@ export const getDIDAuthVPR = async (exchangeId) => {
}
}

const getProcessRecordFnForExchangeHostAndTenant = (exchangeHost, tenantName) => {
// returns a function for processing incoming records, bound to the specific exchangeHost and tenant
return async (record) => {
record.tenantName = tenantName
record.exchangeHost = exchangeHost
record.transactionId = crypto.randomUUID()
record.exchangeId = crypto.randomUUID()

await keyv.set(record.exchangeId, record, expiresAfter);

// directDeepLink bypasses the VPR step and assumes the wallet knows to send a DIDAuth.
const directDeepLink = `https://lcw.app/request.html?issuer=issuer.example.com&auth_type=bearer&challenge=${record.transactionId}&vc_request_url=${exchangeHost}/exchange/${record.exchangeId}/${record.transactionId}`

//vprDeepLink = deeplink that calls /exchanges/${exchangeId} to initiate the exchange
// and get back a VPR to which to then send the DIDAuth.
const vprDeepLink = `https://lcw.app/request.html?issuer=issuer.example.com&auth_type=bearer&vc_request_url=${exchangeHost}/exchange/${record.exchangeId}`
//
const chapiVPR = await getDIDAuthVPR(record.exchangeId)
const retrievalId = record.retrievalId;

return { retrievalId, directDeepLink, vprDeepLink, chapiVPR }
}
}
/**
*
* This is the "old" version of the vpr, which might have been superseded by the above vpr,
Expand Down Expand Up @@ -110,7 +121,7 @@ export const getVPR = async (exchangeId) => {
*/
export const retrieveStoredData = async (exchangeId, transactionId, didAuthVP) => {
const storedData = await getExchangeData(exchangeId)
const didAuthVerified = await verifyDIDAuth({presentation: didAuthVP, challenge: storedData.transactionId})
const didAuthVerified = await verifyDIDAuth({ presentation: didAuthVP, challenge: storedData.transactionId })
const transactionIdMatches = transactionId === storedData.transactionId
if (didAuthVerified && transactionIdMatches) {
return storedData
Expand All @@ -137,21 +148,28 @@ const getExchangeData = async exchangeId => {
* @param {string} data.exchangeHost hostname for the exchange endpoints
* @param {string} [data.tenantName] tenant with which to sign
* @param {string} [data.batchId] batch to which cred belongs; also determines vc template
* @param {string} item.retrievalId an identier for each record, e.g., the recipient's email address
* @throws {ExchangeIdError} Unknown exchangeID
*/
const verifyExchangeData = data => {
if (! data.vc || data.subjectData ) {
throw ExchangeError("Incomplete exchange data - you must provide either a vc or subjectData", 400)
}
if (! data.exchangeHost) {
throw ExchangeError("Incomplete exchange data - you must provide an exchangeHost", 400)
const verifyExchangeData = exchangeData => {
const batchId = exchangeData.batchId
if (!exchangeData.exchangeHost) {
throw new ExchangeError("Incomplete exchange data - you must provide an exchangeHost", 400)
}
if (data.subjectData && !data.batchId) {
throw ExchangeError("Incomplete exchange data - if you provide subjectData, you must also provide a batchId", 400)
}
if (!data.tenantName) {
throw ExchangeError("Incomplete exchange data - you must provide a tenant name", 400)
if (!exchangeData.tenantName) {
throw new ExchangeError("Incomplete exchange data - you must provide a tenant name", 400)
}
exchangeData.data.forEach(credData => {
if (!credData.vc || credData.subjectData) {
throw new ExchangeError("Incomplete exchange data - you must provide either a vc or subjectData", 400)
}
if (credData.subjectData && !batchId) {
throw new ExchangeError("Incomplete exchange data - if you provide subjectData, you must also provide a batchId", 400)
}
if (!credData.retrievalId) {
throw new ExchangeError("Incomplete exchange data - every submitted record must have it's own retrievalId.", 400)
}
})
}


Expand Down

0 comments on commit d5f1432

Please sign in to comment.