Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
3323fbe
Add DidSyntaxError.
aljones15 Aug 4, 2022
a0d8322
Parse did returns scheme and prefix.
aljones15 Aug 4, 2022
c833a50
Port validators for did and didUrl over.
aljones15 Aug 5, 2022
3128d6a
Add validator for invalidDid and invalidDidUrl to CachedResolver.
aljones15 Aug 5, 2022
e037690
Use DidResolverError.
aljones15 Aug 5, 2022
93a4df4
Update parseDid prefix to method to match did core syntax.
aljones15 Aug 5, 2022
f4bff2e
Add breaking CHANGELOG entry for release.
aljones15 Aug 5, 2022
b811080
Export DidResolverError.
aljones15 Aug 5, 2022
d2de11b
Correct name of DidResolverError & inherit from Error instead of Synt…
aljones15 Aug 8, 2022
98135e8
Export isDidUrl.
aljones15 Aug 8, 2022
e418733
Move did TypeError to validators.
aljones15 Aug 9, 2022
6287e70
Add the ability to manually set the method for a did driver.
aljones15 Aug 10, 2022
82812a7
Add CHANGELOG entry for method parameter of CachedResolver.use.
aljones15 Aug 10, 2022
5e5dc3e
Use DidResolverError w/ code methodNotSupported if no driver for did:…
aljones15 Aug 11, 2022
7a76eae
Add tests for validateDid, isValidDid, & isValidDidUrl.
aljones15 Aug 12, 2022
99a3618
Add tests for invalidDidUrlSyntax.
aljones15 Aug 12, 2022
349129f
Add note on method & expand README.
aljones15 Aug 12, 2022
6f9062b
Capitalize DID & URL in jsdoc comment.
aljones15 Aug 26, 2022
93aa105
Capitalize DID.
aljones15 Aug 26, 2022
dee6f49
Correct typos in changelog.
aljones15 Aug 26, 2022
c580add
Capitalize DIDs.
aljones15 Aug 26, 2022
b9a0aa2
Capitalize DID.
aljones15 Aug 26, 2022
79bbbad
Pluralize components comment.
aljones15 Aug 26, 2022
150cd6f
Capitalize DID.
aljones15 Aug 26, 2022
e465e3b
Capitalize DID URL.
aljones15 Aug 26, 2022
13a9c6c
Bump copyright to 2022.
aljones15 Aug 26, 2022
2237b54
Extend Copyright to 2022.
aljones15 Aug 26, 2022
8c93c99
Bump Copyright to 2022.
aljones15 Aug 26, 2022
debba55
Capitalize DID & URL.
aljones15 Aug 26, 2022
adb593d
Capitalize DID.
aljones15 Aug 26, 2022
d722b55
Capitalize DID.
aljones15 Aug 26, 2022
402a14d
Capitalize DID & URL.
aljones15 Aug 26, 2022
389fa2c
Capitalize DID.
aljones15 Aug 26, 2022
720fe4f
Clarify return statement of validator for did.
aljones15 Aug 26, 2022
f1f6f69
Clarify JSDOC comments.
aljones15 Aug 26, 2022
3486bd2
Use and over & in comments.
aljones15 Aug 26, 2022
d976086
Include scheme in examples for parseDid.
aljones15 Aug 26, 2022
e77d4b7
Use more positive language in README.
aljones15 Dec 7, 2022
50ae168
Sort index.js exports.
aljones15 Dec 7, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# did-io ChangeLog

## 3.0.0 -

### Changed
- **BREAKING**: `parseDid` changed to return `scheme` and `method` instead of
only `prefix`.

### Added
- **BREAKING**: `CachedResolver` now validates DIDs before resolving them.
- `CachedResolver.use` now accepts a second optional method parameter.
- Tests were added for the new validators.

## 2.0.0 - 2022-06-02

### Changed
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@ const resolver = new CachedResolver({max: 100}); // defaults to 100
On its own, the resolver does not know how to fetch or resolve any DID methods.
Support for each one has to be enabled explicitly. It uses a
[Chai](https://www.chaijs.com/)-like plugin architecture, where each driver
is loaded via `.use(driver)`.
is loaded via `.use(driver)`. Optionally method can be passed in as the second
parameter.

```js
import * as didKey from '@digitalbazaar/did-method-key';
Expand Down
40 changes: 25 additions & 15 deletions lib/CachedResolver.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
/*!
* Copyright (c) 2021 Digital Bazaar, Inc. All rights reserved.
* Copyright (c) 2021-2022 Digital Bazaar, Inc. All rights reserved.
*/
import {DidResolverError} from './DidResolverError.js';
import {parseDid} from './did-io.js';
import {validateDid} from './validators.js';
import {LruCache} from '@digitalbazaar/lru-memoize';

export class CachedResolver {
Expand All @@ -20,15 +22,20 @@ export class CachedResolver {
this._cache = new LruCache({max, maxAge, updateAgeOnGet, ...cacheOptions});
this._methods = new Map();
}

use(driver) {
const methodName = driver.method;
/**
* Registers a driver for a did:method.
*
* @param {object} driver - A did method driver.
* @param {string} [method] - An optional method.
*/
use(driver, method) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this change to an arguments object like {driver: d, method: m}?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We've never used that pattern for registering plugins or middleware (I forget what the name of this pattern is) so I just stuck with what we already have. I might add that method parameter exists for one reason: test data this might be over thinking an optional parameter.

const methodName = method || driver.method;
this._methods.set(methodName, driver);
}

/**
* Gets the DID Document, by selecting a registered driver based on the DID
* prefix (DID method).
* method (DID method).
* Either `did` or `url` param is required.
*
* @param {object} options - Options hashmap.
Expand All @@ -42,10 +49,7 @@ export class CachedResolver {
*/
async get({did, url, ...args} = {}) {
did = did || url;
if(!did) {
throw new TypeError('A string "did" or "url" parameter is required.');
}

validateDid({did});
const method = this._methodForDid(did);

return this._cache.memoize({
Expand All @@ -67,7 +71,10 @@ export class CachedResolver {
async generate({method, ...args}) {
const driver = this._methods.get(method);
if(!driver) {
throw new Error(`Driver for DID method "${method}" not found.`);
throw new DidResolverError({
message: `Driver for DID method "${method}" not found.`,
code: 'methodNotSupported'
});
}

return driver.generate(args);
Expand All @@ -80,11 +87,14 @@ export class CachedResolver {
* @private
*/
_methodForDid(did) {
const {prefix} = parseDid({did});
const method = this._methods.get(prefix);
if(!method) {
throw new Error(`Driver for DID ${did} not found.`);
const {method} = parseDid({did});
const methodDriver = this._methods.get(method);
if(!methodDriver) {
throw new DidResolverError({
message: `Driver for DID ${did} not found.`,
code: 'methodNotSupported'
});
}
return method;
return methodDriver;
}
}
20 changes: 20 additions & 0 deletions lib/DidResolverError.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*!
* Copyright (c) 2021-2022 Digital Bazaar, Inc. All rights reserved.
*/

/**
* Error for throwing did syntax related errors.
*
* @param {object} options - Options to use.
* @param {string} options.message - An error message.
* @param {string} options.code - A DID core error.
* @param {object} options.params - Params to be passed to the base Error Class.
*
*/
export class DidResolverError extends Error {
constructor({message, code, params}) {
super(message, params);
this.name = 'DidResolverError';
this.code = code;
}
}
22 changes: 17 additions & 5 deletions lib/did-io.js
Original file line number Diff line number Diff line change
Expand Up @@ -194,22 +194,34 @@ export function _methodById({doc, methodId}) {
}

/**
* Parses the DID into various component (currently, only cares about prefix).
* Parses the DID into scheme and method components.
*
* @example
* parseDid({did: 'did:v1:test:nym'});
* // -> {prefix: 'v1'}
* // -> {scheme: 'did', method: 'v1'}
*
* @param {string} did - DID uri.
*
* @returns {{prefix: string}} Returns the method prefix (without `did:`).
* @returns {{scheme: string, method: string}} Returns the scheme and method.
*/
export function parseDid({did}) {
if(!did) {
throw new TypeError('DID cannot be empty.');
}

const prefix = did.split(':').slice(1, 2).join(':');
const [scheme, method] = did.split(':');

return {prefix};
return {scheme, method};
}

/**
* Determines if a DID has URL characters in it.
*
* @param {object} options - Options to use.
* @param {string} options.did - A DID.
*
* @returns {boolean} Does the DID potentially have a query or a fragment in it?
*/
export function isDidUrl({did}) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This naming is a bit confusing at first glance. I assume this is trying to check between DID Syntax and DID URL Syntax? It looks like the path-abempty, query, and fragment parts are all optional so shouldn't every regular DID be a DID URL too, even if the regex below doesn't pass?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All DIDs are valid DID URLs, but not all DID URLS are valid DIDs. A DID URL can for instance end with :.

return /[\/#?]/.test(did);
}
3 changes: 3 additions & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ export {
approvesMethodFor,
findVerificationMethod,
initKeys,
isDidUrl,
parseDid
} from './did-io.js';

export {CachedResolver} from './CachedResolver.js';

export {DidResolverError} from './DidResolverError.js';

export {VERIFICATION_RELATIONSHIPS} from './constants.js';
83 changes: 83 additions & 0 deletions lib/validators.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*!
* Copyright (c) 2022 Digital Bazaar, Inc. All rights reserved.
*/
import {isDidUrl} from './did-io.js';
import {DidResolverError} from './DidResolverError.js';

/**
* Determines if a DID contains URL characters and
* then validates either the DID URL or DID.
*
* @param {object} options - Options to use.
* @param {string} options.did - A DID.
*
* @throws {DidResolverError} Throws if DID is invalid.
*
* @returns {undefined} Returns on success.
*/
export function validateDid({did}) {
if(!did) {
throw new TypeError('The parameter "did" is required.');
}
if(isDidUrl({did})) {
if(!isValidDidUrl({didUrl: did})) {
throw new DidResolverError({
message: `Invalid DID URL "${did}"`,
code: 'invalidDidUrl'
});
}
return;
}
if(!isValidDid({did})) {
throw new DidResolverError({
message: `Invalid DID "${did}"`,
code: 'invalidDid'
});
}
}

/**
* Validates a DID, but not a DID URL.
* This function comes from the did-test-suite.
*
* @see https://github.com/w3c/did-test-suite/
*
* @param {object} options - Options to use.
* @param {string} options.did - A prospective DID.
*
* @returns {boolean} - Returns true if DID is valid.
*/
export function isValidDid({did}) {
const didRegex1 = new RegExp('^did:(?<method_name>[a-z0-9]+):' +
'(?<method_specific_id>([a-zA-Z0-9\\.\\-_]|%[0-9a-fA-F]{2}|:)+$)');
const didRegex2 = /:$/;
return didRegex1.test(did) && !didRegex2.test(did);
}

/**
* Validates a DID URL including the fragment and queries.
* This function comes from the did-test-suite.
*
* @see https://github.com/w3c/did-test-suite/
*
* @param {object} options - Options to use.
* @param {string} options.didUrl - A prospective DID URL.
*
* @returns {boolean} - Returns true or false.
*/
export function isValidDidUrl({didUrl}) {
const pchar = '[a-zA-Z0-9\\-\\._~]|%[0-9a-fA-F]{2}|[!$&\'()*+,;=:@]';
const didUrlPattern =
'^' +
'did:' +
'([a-z0-9]+)' + // method_name
'(:' + // method-specific-id
'([a-zA-Z0-9\\.\\-_]|%[0-9a-fA-F]{2})+' +
')+' +
'((/(' + pchar + ')+)+)?' + // path-abempty
'(\\?(' + pchar + '|/|\\?)+)?' + // [ "?" query ]
'(#(' + pchar + '|/|\\?)+)?' + // [ "#" fragment ]
'$'
;
return new RegExp(didUrlPattern).test(didUrl);
}
10 changes: 7 additions & 3 deletions test/did-io.spec.js → test/01-did-io.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,13 @@ const MOCK_KEY = {
};

describe('parseDid', () => {
it('should return main did method identifier', async () => {
const {prefix} = parseDid({did: 'did:v1:test:nym:abcd'});
expect(prefix).to.equal('v1');
it('should return did method', async () => {
const {method} = parseDid({did: 'did:v1:test:nym:abcd'});
expect(method).to.equal('v1');
});
it('should return did scheme', async () => {
const {scheme} = parseDid({did: 'did:v1:test:nym:abcd'});
expect(scheme).to.equal('did');
});
});

Expand Down
115 changes: 115 additions & 0 deletions test/02-did-io-validators.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/*!
* Copyright (c) 2022 Digital Bazaar, Inc. All rights reserved.
*/
import chai from 'chai';
import {testDid} from './helpers.js';
import {isValidDid, isValidDidUrl} from '../lib/validators.js';
import {DidResolverError} from '../lib/DidResolverError.js';
import {
typeErrors,
invalidDids,
invalidDidSyntax,
invalidDidUrlSyntax,
invalidDidUrls,
validDids,
validDidUrls,
} from './mock.data.js';

const should = chai.should();

describe('validateDid', () => {
describe('should not throw', () => {
const inputs = new Set([...validDids, ...validDidUrls]);
for(const input of inputs) {
it(`should validate ${input}`, async () => {
const error = testDid(input);
should.not.exist(error, `Expected no error for did ${input}`);
});
}
});
describe('should throw `TypeError`', () => {
for(const input of typeErrors) {
it(`should not validate ${input}`, async () => {
const error = testDid(input);
should.exist(error, `Expected error for did ${input}`);
error.should.be.instanceof(
TypeError,
`Expected a TypeError for ${input}`
);
});
}
});
describe('should throw `invalidDid`', () => {
const inputs = [...invalidDidSyntax, 'did:key:z4345345:'];
for(const input of inputs) {
it(`should not validate ${input}`, async () => {
const error = testDid(input);
should.exist(error, `Expected error for did ${input}`);
error.should.be.instanceof(
DidResolverError,
`Expected a DidResolverError for ${input}`
);
error.code.should.equal(
'invalidDid',
`Expected ${input} to be an invalid did.`
);
});
}
});
describe('should throw `invalidDidUrl`', () => {
for(const invalidDidUrl of invalidDidUrlSyntax) {
it(`should not validate ${invalidDidUrl}`, async () => {
const error = testDid(invalidDidUrl);
should.exist(error, `Expected error for did url ${invalidDidUrl}`);
error.should.be.instanceof(
DidResolverError,
`Expected a DidResolverError for ${invalidDidUrl}`
);
error.code.should.equal(
'invalidDidUrl',
`Expected ${invalidDidUrl} to be an invalid did url`
);
});
}
});
});

describe('isValidDidUrl', () => {
for(const validDidUrl of validDidUrls) {
it(`should validate ${validDidUrl}`, async () => {
const result = isValidDidUrl({didUrl: validDidUrl});
should.exist(result, `Expected result for ${validDidUrl} to exist.`);
result.should.be.a(
'boolean', 'Expected isValidDidUrl to return a boolean');
result.should.equal(true, `Expected ${validDidUrl} to validate`);
});
}
for(const invalidDidUrl of invalidDidUrls) {
it(`should not validate ${invalidDidUrl}`, async () => {
const result = isValidDidUrl({didUrl: invalidDidUrl});
should.exist(result, `Expected result for ${invalidDidUrl} to exist.`);
result.should.be.a(
'boolean', 'Expected isValidDidUrl to return a boolean');
result.should.equal(false, `Expected ${invalidDidUrl} to not validate`);
});
}
});

describe('isValidDid', () => {
for(const validDid of validDids) {
it(`should validate ${validDid}`, async () => {
const result = isValidDid({did: validDid});
should.exist(result, `Expected result for ${validDid} to exist.`);
result.should.be.a('boolean', 'Expected isValidDid to return a boolean');
result.should.equal(true, `Expected ${validDid} to validate`);
});
}
for(const invalidDid of invalidDids) {
it(`should not validate ${invalidDid}`, async () => {
const result = isValidDid({did: invalidDid});
should.exist(result, `Expected result for ${invalidDid} to exist.`);
result.should.be.a('boolean', 'Expected isValidDid to return a boolean');
result.should.equal(false, `Expected ${invalidDid} to not validate`);
});
}
});
Loading