Skip to content

Commit

Permalink
Include expires in exchange information.
Browse files Browse the repository at this point in the history
  • Loading branch information
dlongley committed Aug 24, 2024
1 parent 16e3df3 commit 2c088c8
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 13 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# bedrock-vc-delivery ChangeLog

## 5.3.0 - 2024-08-dd

### Added
- Include `expires` in exchange information.

## 5.2.0 - 2024-08-22

### Added
Expand Down
44 changes: 34 additions & 10 deletions lib/exchanges.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,18 +88,22 @@ export async function insert({workflowId, exchange}) {
// build exchange record
const now = Date.now();
const meta = {created: now, updated: now};
// possible states are: `pending`, `active`, `complete`, or `invalid`
exchange = {...exchange, sequence: 0, state: 'pending'};
if(exchange.ttl !== undefined) {
// TTL is in seconds
meta.expires = new Date(now + exchange.ttl * 1000);
// TTL is in seconds, convert to `expires`
const expires = new Date(now + exchange.ttl * 1000);
meta.expires = expires;
exchange.expires = expires.toISOString().replace(/\.\d+Z$/, 'Z');
delete exchange.ttl;
}
const {localId: localWorkflowId} = parseLocalId({id: workflowId});
const record = {
localWorkflowId,
// backwards compatibility: enable existing systems to find record
localExchangerId: localWorkflowId,
meta,
// possible states are: `pending`, `active`, `complete`, or `invalid`
exchange: {...exchange, sequence: 0, state: 'pending'}
exchange
};

// insert the exchange and get the updated record
Expand Down Expand Up @@ -129,12 +133,16 @@ export async function insert({workflowId, exchange}) {
* @param {string} options.workflowId - The ID of the workflow that the
* exchange is associated with.
* @param {string} options.id - The ID of the exchange to retrieve.
* @param {boolean} [options.allowExpired=false] - Controls whether an expired
* exchange that is still in the database can be retrieved or not.
* @param {boolean} [options.explain=false] - An optional explain boolean.
*
* @returns {Promise<object | ExplainObject>} Resolves with the record that
* matches the query or an ExplainObject if `explain=true`.
*/
export async function get({workflowId, id, explain = false} = {}) {
export async function get({
workflowId, id, allowExpired = false, explain = false
} = {}) {
assert.string(workflowId, 'workflowId');
assert.string(id, 'id');

Expand All @@ -160,7 +168,16 @@ export async function get({workflowId, id, explain = false} = {}) {
return cursor.explain('executionStats');
}

const record = await collection.findOne(query, {projection});
let record = await collection.findOne(query, {projection});
if(record?.exchange.expires && !allowExpired) {
// ensure `expires` is enforced programmatically even if background job
// has not yet removed the record
const now = new Date();
const expires = new Date(record.exchange.expires);
if(now >= expires) {
record = null;
}
}
if(!record) {
throw new BedrockError('Exchange not found.', {
name: 'NotFoundError',
Expand Down Expand Up @@ -196,8 +213,8 @@ export async function get({workflowId, id, explain = false} = {}) {
}

/**
* Updates a pending exchange with new state, variables, step, and TTL
* information.
* Updates a pending or active exchange with new state, variables, step, and
* TTL, and error information.
*
* @param {object} options - The options to use.
* @param {string} options.workflowId - The ID of the workflow the exchange
Expand Down Expand Up @@ -226,8 +243,15 @@ export async function update({workflowId, exchange, explain = false} = {}) {
if(exchange.step !== undefined) {
update.$set['exchange.step'] = exchange.step;
}
if(exchange.ttl !== undefined) {
update.$set['exchange.ttl'] = exchange.ttl;
// only set `ttl` if expires not previously set / has been cleared
if(exchange.ttl !== undefined && exchange.expires === undefined) {
// TTL is in seconds, convert to expires
const expires = new Date(now + exchange.ttl * 1000);
// unset and previously set `ttl`
update.$unset['exchange.ttl'] = true;
update.$set['meta.expires'] = expires;
update.$set['exchange.expires'] =
expires.toISOString().replace(/\.\d+Z$/, 'Z');
}

const {base, localId: localWorkflowId} = parseLocalId({id: workflowId});
Expand Down
58 changes: 58 additions & 0 deletions test/mocha/20-vcapi.js
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,64 @@ describe('exchange w/ VC-API delivery', () => {
err.data.name.should.equal('NotAllowedError');
}
});

it('should fail when an exchange expires', async function() {
this.timeout(10000);

let now = new Date();
const credentialId = `urn:uuid:${uuid()}`;
const {exchangeId} = await helpers.createCredentialOffer({
// local target user
userId: 'urn:uuid:01cc3771-7c51-47ab-a3a3-6d34b47ae3c4',
credentialDefinition: mockData.credentialDefinition,
credentialId,
preAuthorized: true,
userPinRequired: false,
capabilityAgent,
workflowId,
workflowRootZcap,
// TTL of one second
ttl: 1
});

// exchange state should be pending and not yet expired
{
let err;
try {
const {exchange} = await helpers.getExchange(
{id: exchangeId, capabilityAgent});
should.exist(exchange?.state);
exchange.state.should.equal('pending');
should.exist(exchange.expires);
exchange.expires.should.be.a('string');
const expires = new Date(exchange.expires);
const isValidDate = !isNaN(expires);
isValidDate.should.equal(true);
expires.should.be.greaterThan(now);

// wait for exchange to expire
now = new Date();
await new Promise(
r => setTimeout(r, expires.getTime() - now.getTime()));
} catch(error) {
err = error;
}
should.not.exist(err, err?.message);
}

// getting exchange state should result in 404
{
let err;
try {
await helpers.getExchange({id: exchangeId, capabilityAgent});
} catch(error) {
err = error;
}
should.exist(err);
should.exist(err.data?.name);
err.data.name.should.equal('NotFoundError');
}
});
});

describe('exchange w/ VC-API delivery using credential request ' +
Expand Down
7 changes: 4 additions & 3 deletions test/mocha/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,14 @@ export async function createCredentialOffer({
openId = true, openIdKeyPair,
useCredentialIds = false,
useCredentialConfigurationIds = false,
useCredentialOfferUri = false
useCredentialOfferUri = false,
// 15 minute expiry in seconds
ttl = 60 * 15
} = {}) {
// first, create an exchange with variables based on the local user ID;
// indicate that OID4VCI delivery is permitted
const exchange = {
// 15 minute expiry in seconds
ttl: 60 * 15,
ttl,
// template variables
variables: variables ? {
issuanceDate: (new Date()).toISOString(),
Expand Down

0 comments on commit 2c088c8

Please sign in to comment.