@interop/did-web-resolver 1.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
- # `did:web` Resolver _(did-web-resolver)_
1
+ # `did:web` Resolver _(@interop/did-web-resolver)_
2
2
 
3
3
  [![Node.js CI](https://github.com/interop-alliance/did-web-resolver/workflows/Node.js%20CI/badge.svg)](https://github.com/interop-alliance/did-web-resolver/actions?query=workflow%3A%22Node.js+CI%22)
4
- [![NPM Version](https://img.shields.io/npm/v/interop/did-web-resolver)](https://www.npmjs.com/package/@interop/did-web-resolver)
4
+ [![NPM Version](https://img.shields.io/npm/v/@interop/did-web-resolver.svg)](https://npm.im/@interop/did-web-resolver)
5
5
 
6
6
  > A did:web method Decentralized Identifier (DID) resolver for the did-io library.
7
7
 
@@ -21,7 +21,7 @@ TBD
21
21
  ## Background
22
22
 
23
23
  A `did:web` method driver for use with in-browser and server-side on Node.js
24
- with the [`did-io`](https://github.com/digitalbazaar/did-io) resolver library.
24
+ with the [`did-io`](https://github.com/digitalcredentials/did-io) resolver library.
25
25
 
26
26
  Draft spec (W3C CCG Work Item):
27
27
 
@@ -37,9 +37,9 @@ Other implementations:
37
37
 
38
38
  ```js
39
39
  import { Ed25519VerificationKey2020 }
40
- from '@digitalbazaar/ed25519-verification-key-2020'
40
+ from '@digitalcredentials/ed25519-verification-key-2020'
41
41
  import { X25519KeyAgreementKey2020 }
42
- from '@digitalbazaar/x25519-key-agreement-key-2020'
42
+ from '@digitalcredentials/x25519-key-agreement-key-2020'
43
43
  import { CryptoLD } from 'crypto-ld'
44
44
 
45
45
  import * as didWeb from '@interop/did-web-resolver'
@@ -51,7 +51,7 @@ cryptoLd.use(X25519KeyAgreementKey2020)
51
51
  const didWebDriver = didWeb.driver({ cryptoLd })
52
52
 
53
53
  // Optionally use it with the CachedResolver from did-io
54
- import {CachedResolver} from '@digitalbazaar/did-io';
54
+ import {CachedResolver} from '@digitalcredentials/did-io';
55
55
  const resolver = new CachedResolver()
56
56
  resolver.use(didWebDriver)
57
57
  ```
package/build-dist.sh ADDED
@@ -0,0 +1,14 @@
1
+ mkdir ./dist/esm
2
+ cat >dist/esm/index.js <<!EOF
3
+ import cjsModule from '../index.js';
4
+ export const driver = cjsModule.driver;
5
+ export const DidWebResolver = cjsModule.DidWebResolver;
6
+ export const didFromUrl = cjsModule.didFromUrl;
7
+ export const urlFromDid = cjsModule.urlFromDid;
8
+ !EOF
9
+
10
+ cat >dist/esm/package.json <<!EOF
11
+ {
12
+ "type": "module"
13
+ }
14
+ !EOF
@@ -0,0 +1,336 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var httpClient = require('@digitalcredentials/http-client');
6
+ var didIo = require('@digitalcredentials/did-io');
7
+ var ed25519Context = require('ed25519-signature-2020-context');
8
+ var x25519Context = require('x25519-key-agreement-2020-context');
9
+ var didContext = require('did-context');
10
+
11
+ function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
12
+
13
+ function _interopNamespace(e) {
14
+ if (e && e.__esModule) return e;
15
+ var n = Object.create(null);
16
+ if (e) {
17
+ Object.keys(e).forEach(function (k) {
18
+ if (k !== 'default') {
19
+ var d = Object.getOwnPropertyDescriptor(e, k);
20
+ Object.defineProperty(n, k, d.get ? d : {
21
+ enumerable: true,
22
+ get: function () { return e[k]; }
23
+ });
24
+ }
25
+ });
26
+ }
27
+ n["default"] = e;
28
+ return Object.freeze(n);
29
+ }
30
+
31
+ var didIo__namespace = /*#__PURE__*/_interopNamespace(didIo);
32
+ var ed25519Context__default = /*#__PURE__*/_interopDefaultLegacy(ed25519Context);
33
+ var x25519Context__default = /*#__PURE__*/_interopDefaultLegacy(x25519Context);
34
+ var didContext__default = /*#__PURE__*/_interopDefaultLegacy(didContext);
35
+
36
+ const { VERIFICATION_RELATIONSHIPS } = didIo__namespace;
37
+
38
+ const DEFAULT_KEY_MAP = {
39
+ capabilityInvocation: 'Ed25519VerificationKey2020',
40
+ authentication: 'Ed25519VerificationKey2020',
41
+ assertionMethod: 'Ed25519VerificationKey2020',
42
+ capabilityDelegation: 'Ed25519VerificationKey2020',
43
+ keyAgreement: 'X25519KeyAgreementKey2020'
44
+ };
45
+
46
+ function didFromUrl ({ url } = {}) {
47
+ if (!url) {
48
+ throw new TypeError('Cannot convert url to did, missing url.')
49
+ }
50
+ if (url.startsWith('http:')) {
51
+ throw new TypeError('did:web does not support non-HTTPS URLs.')
52
+ }
53
+
54
+ let parsedUrl;
55
+ try {
56
+ parsedUrl = new URL(url);
57
+ } catch (error) {
58
+ throw new TypeError(`Invalid url: "${url}".`)
59
+ }
60
+
61
+ const { host, pathname } = parsedUrl;
62
+
63
+ let pathComponent = '';
64
+ if (pathname && pathname !== '/' && pathname !== '/.well-known/did.json') {
65
+ pathComponent = pathname.split('/').map(encodeURIComponent).join(':');
66
+ }
67
+
68
+ return 'did:web:' + encodeURIComponent(host) + pathComponent
69
+ }
70
+
71
+ function urlFromDid ({ did } = {}) {
72
+ if (!did) {
73
+ throw new TypeError('Cannot convert did to url, missing did.')
74
+ }
75
+ if (!did.startsWith('did:web:')) {
76
+ throw new TypeError(`DID Method not supported: "${did}".`)
77
+ }
78
+
79
+ const [didUrl, hashFragment] = did.split('#');
80
+ // eslint-disable-next-line no-unused-vars
81
+ // const [didResource, query] = didUrl.split('?')
82
+
83
+ // eslint-disable-next-line no-unused-vars
84
+ const [_did, _web, urlNoProtocol] = didUrl.split(':');
85
+
86
+ let parsedUrl;
87
+ try {
88
+ // URI-decode the url (in case it contained a port number,
89
+ // for example, `did:web:localhost%3A8080`
90
+ parsedUrl = new URL('https://' + decodeURIComponent(urlNoProtocol));
91
+ } catch (error) {
92
+ throw new TypeError(`Cannot construct url from did: "${did}".`)
93
+ }
94
+
95
+ if (!parsedUrl.pathname || parsedUrl.pathname === '/') {
96
+ parsedUrl.pathname = '/.well-known/did.json';
97
+ } else {
98
+ const pathFragments = parsedUrl.pathname.split('/');
99
+ parsedUrl.pathname = pathFragments.map(decodeURIComponent).join('/');
100
+ }
101
+
102
+ if (hashFragment) {
103
+ parsedUrl.hash = hashFragment;
104
+ }
105
+ return parsedUrl.toString()
106
+ }
107
+
108
+ /**
109
+ * Initializes the DID Document's keys/proof methods.
110
+ *
111
+ * @example
112
+ * didDocument.id = 'did:ex:123';
113
+ * const {didDocument, keyPairs} = await initKeys({
114
+ * didDocument,
115
+ * cryptoLd,
116
+ * keyMap: {
117
+ * capabilityInvocation: someExistingKey,
118
+ * authentication: 'Ed25519VerificationKey2020',
119
+ * assertionMethod: 'Ed25519VerificationKey2020',
120
+ * keyAgreement: 'X25519KeyAgreementKey2019'
121
+ * }
122
+ * });.
123
+ *
124
+ * @param {object} options - Options hashmap.
125
+ * @param {object} options.didDocument - DID Document.
126
+ * @typedef {object} CryptoLD
127
+ * @param {CryptoLD} [options.cryptoLd] - CryptoLD driver instance,
128
+ * initialized with the key types this DID Document intends to support.
129
+ * @param {object} [options.keyMap] - Map of keys (or key types) by purpose.
130
+ *
131
+ * @returns {Promise<{didDocument: object, keyPairs: Map}>} Resolves with the
132
+ * DID Document initialized with keys, as well as the map of the corresponding
133
+ * key pairs (by key id).
134
+ */
135
+ async function initKeys ({ didDocument, cryptoLd, keyMap = {} } = {}) {
136
+ const doc = { ...didDocument };
137
+ if (!doc.id) {
138
+ throw new TypeError(
139
+ 'DID Document "id" property is required to initialize keys.')
140
+ }
141
+
142
+ const keyPairs = new Map();
143
+
144
+ // Set the defaults for the created keys (if needed)
145
+ const options = { controller: doc.id };
146
+
147
+ for (const purpose in keyMap) {
148
+ if (!VERIFICATION_RELATIONSHIPS.has(purpose)) {
149
+ throw new Error(`Unsupported key purpose: "${purpose}".`)
150
+ }
151
+
152
+ let key;
153
+ if (typeof keyMap[purpose] === 'string') {
154
+ if (!cryptoLd) {
155
+ throw new Error('Please provide an initialized CryptoLD instance.')
156
+ }
157
+ key = await cryptoLd.generate({ type: keyMap[purpose], ...options });
158
+ } else {
159
+ // An existing key has been provided
160
+ key = keyMap[purpose];
161
+ }
162
+
163
+ doc[purpose] = [key.export({ publicKey: true })];
164
+ keyPairs.set(key.id, key);
165
+ }
166
+
167
+ return { didDocument: doc, keyPairs }
168
+ }
169
+
170
+ class DidWebResolver {
171
+ /**
172
+ * @param cryptoLd {CryptoLD}
173
+ * @param keyMap {object}
174
+ * @param [logger] {object} Logger object (with .log, .error, .warn,
175
+ * etc methods).
176
+ */
177
+ constructor ({ cryptoLd, keyMap = DEFAULT_KEY_MAP, logger = console } = {}) {
178
+ this.method = 'web'; // did:web:... (used for didIo resolver harness)
179
+ this.cryptoLd = cryptoLd;
180
+ this.keyMap = keyMap;
181
+ this.logger = logger;
182
+ }
183
+
184
+ /**
185
+ * Generates a new DID Document and initializes various authentication
186
+ * and authorization proof purpose keys.
187
+ *
188
+ * @example
189
+ * const url = 'https://example.com'
190
+ * const { didDocument, didKeys } = await didWeb.generate({url})
191
+ * didDocument.id
192
+ * // -> 'did:web:example.com'
193
+ *
194
+ *
195
+ * Either an `id` or a `url` is required:
196
+ * @param [id] {string} - A did:web DID. If absent, will be converted from url
197
+ * @param [url] {string}
198
+ *
199
+ * @param [keyMap=DEFAULT_KEY_MAP] {object} A hashmap of key types by purpose.
200
+ *
201
+ * @parma [cryptoLd] {object} CryptoLD instance with support for supported
202
+ * crypto suites installed.
203
+ *
204
+ * @returns {Promise<{didDocument: object, keyPairs: Map,
205
+ * methodFor: Function}>} Resolves with the generated DID Document, along
206
+ * with the corresponding key pairs used to generate it (for storage in a
207
+ * KMS).
208
+ */
209
+ async generate ({ id, url, keyMap = this.keyMap, cryptoLd = this.cryptoLd } = {}) {
210
+ const did = id || didFromUrl({ url });
211
+
212
+ // Compose the DID Document
213
+ let didDocument = {
214
+ '@context': [
215
+ didContext__default["default"].constants.DID_CONTEXT_URL,
216
+ ed25519Context__default["default"].constants.CONTEXT_URL,
217
+ x25519Context__default["default"].constants.CONTEXT_URL
218
+ ],
219
+ id: did
220
+ };
221
+
222
+ const result = await initKeys({ didDocument, cryptoLd, keyMap });
223
+ const keyPairs = result.keyPairs;
224
+ didDocument = result.didDocument;
225
+
226
+ // Convenience function that returns the public/private key pair instance
227
+ // for a given purpose (authentication, assertionMethod, keyAgreement, etc).
228
+ const methodFor = ({ purpose }) => {
229
+ const { id: methodId } = didIo__namespace.findVerificationMethod({
230
+ doc: didDocument, purpose
231
+ });
232
+ return keyPairs.get(methodId)
233
+ };
234
+
235
+ return { didDocument, keyPairs, methodFor }
236
+ }
237
+
238
+ /**
239
+ * Fetches a DID Document for a given DID.
240
+ *
241
+ * @example
242
+ * // In Node.js tests, use an agent to avoid self-signed certificate errors
243
+ * const agent = new https.agent({rejectUnauthorized: false});
244
+ *
245
+ * @param {string} [did] For example, 'did:web:example.com'
246
+ * @param {string} [url]
247
+ * @param {https.Agent} [agent] Optional agent used to customize network
248
+ * behavior in Node.js (such as `rejectUnauthorized: false`).
249
+ * @param {object} [logger] Logger object (with .log, .error, .warn,
250
+ * etc methods).
251
+ *
252
+ * @throws {Error}
253
+ *
254
+ * @returns {Promise<object>} Plain parsed JSON object of the DID Document.
255
+ */
256
+ async get ({ did, url, agent, logger = this.logger }) {
257
+ const didUrl = url || urlFromDid({ did });
258
+ if (!didUrl) {
259
+ throw new TypeError('A DID or a URL is required.')
260
+ }
261
+
262
+ const [urlAuthority, keyIdFragment] = didUrl.split('#');
263
+
264
+ let didDocument;
265
+ try {
266
+ logger.info(`Fetching "${urlAuthority}" via http client.`);
267
+ const result = await httpClient.httpClient.get(urlAuthority, { agent });
268
+ didDocument = result.data;
269
+ } catch (e) {
270
+ // status is HTTP status code
271
+ // data is JSON error from the server if available
272
+ const { data, status } = e;
273
+ logger.error(`Http ${status} error:`, data);
274
+ throw e
275
+ }
276
+ if (didDocument && keyIdFragment) {
277
+ // resolve an individual key
278
+ // Keys are expected to have format: <did:web:...>#<keyIdFragment>
279
+ const didAuthority = didFromUrl({ url: urlAuthority });
280
+ const methodId = `${didAuthority}#${keyIdFragment}`;
281
+
282
+ const key = didIo__namespace.findVerificationMethod({ doc: didDocument, methodId });
283
+ if (!key) {
284
+ throw new Error(`Key id ${methodId} not found.`)
285
+ }
286
+
287
+ const keyPair = await this.cryptoLd.from(key);
288
+
289
+ return keyPair.export({ publicKey: true, includeContext: true })
290
+ }
291
+
292
+ return didDocument
293
+ }
294
+
295
+ /**
296
+ * Returns the public key (verification method) object for a given DID
297
+ * Document and purpose. Useful in conjunction with a `.get()` call.
298
+ *
299
+ * @example
300
+ * const didDocument = await didKeyDriver.get({did});
301
+ * const authKeyData = didDriver.publicMethodFor({
302
+ * didDocument, purpose: 'authentication'
303
+ * });
304
+ * // You can then create a suite instance object to verify signatures etc.
305
+ * const authPublicKey = await cryptoLd.from(authKeyData);
306
+ * const {verify} = authPublicKey.verifier();
307
+ *
308
+ * @param {object} options - Options hashmap.
309
+ * @param {object} options.didDocument - DID Document (retrieved via a
310
+ * `.get()` or from some other source).
311
+ * @param {string} options.purpose - Verification method purpose, such as
312
+ * 'authentication', 'assertionMethod', 'keyAgreement' and so on.
313
+ *
314
+ * @returns {object} Returns the public key object (obtained from the DID
315
+ * Document), without a `@context`.
316
+ */
317
+ publicMethodFor ({ didDocument, purpose } = {}) {
318
+ if (!didDocument) {
319
+ throw new TypeError('The "didDocument" parameter is required.')
320
+ }
321
+ if (!purpose) {
322
+ throw new TypeError('The "purpose" parameter is required.')
323
+ }
324
+
325
+ const method = didIo__namespace.findVerificationMethod({ doc: didDocument, purpose });
326
+ if (!method) {
327
+ throw new Error(`No verification method found for purpose "${purpose}"`)
328
+ }
329
+ return method
330
+ }
331
+ }
332
+
333
+ exports.DidWebResolver = DidWebResolver;
334
+ exports.didFromUrl = didFromUrl;
335
+ exports.initKeys = initKeys;
336
+ exports.urlFromDid = urlFromDid;
@@ -0,0 +1,5 @@
1
+ import cjsModule from '../index.js';
2
+ export const driver = cjsModule.driver;
3
+ export const DidWebResolver = cjsModule.DidWebResolver;
4
+ export const didFromUrl = cjsModule.didFromUrl;
5
+ export const urlFromDid = cjsModule.urlFromDid;
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "module"
3
+ }
package/dist/index.js ADDED
@@ -0,0 +1,14 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var DidWebResolver = require('./DidWebResolver.js');
6
+
7
+ const driver = options => {
8
+ return new DidWebResolver.DidWebResolver(options)
9
+ };
10
+
11
+ exports.DidWebResolver = DidWebResolver.DidWebResolver;
12
+ exports.didFromUrl = DidWebResolver.didFromUrl;
13
+ exports.urlFromDid = DidWebResolver.urlFromDid;
14
+ exports.driver = driver;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@interop/did-web-resolver",
3
3
  "description": "A did:web method Decentralized Identifier (DID) resolver for the did-io library.",
4
- "version": "1.0.0",
4
+ "version": "2.1.0",
5
5
  "author": {
6
6
  "name": "Dmitri Zagidulin",
7
7
  "url": "https://github.com/dmitrizagidulin/"
@@ -14,35 +14,55 @@
14
14
  "homepage": "https://github.com/interop-alliance/did-web-driver",
15
15
  "bugs": "https://github.com/interop-alliance/did-web-driver/issues",
16
16
  "scripts": {
17
+ "rollup": "rollup -c rollup.config.js",
18
+ "build": "npm run clear && npm run rollup && ./build-dist.sh",
19
+ "clear": "rimraf dist/ && mkdir dist",
20
+ "prepare": "npm run build",
21
+ "rebuild": "npm run clear && npm run build",
17
22
  "test": "npm run standard && npm run test-node",
18
23
  "test-node": "cross-env NODE_ENV=test mocha -r esm --preserve-symlinks -t 10000 test/**/*.spec.js",
19
24
  "test-karma": "karma start test/karma.conf.js",
20
25
  "nyc": "cross-env NODE_ENV=test nyc npm run test-node",
21
26
  "standard": "standard --fix"
22
27
  },
28
+ "files": [
29
+ "dist",
30
+ "src",
31
+ "rollup.config.js",
32
+ "build-dist.sh",
33
+ "README.md",
34
+ "LICENSE"
35
+ ],
36
+ "main": "dist/index.js",
37
+ "module": "dist/esm/index.js",
38
+ "exports": {
39
+ ".": {
40
+ "require": "./dist/index.js",
41
+ "import": "./dist/esm/index.js"
42
+ }
43
+ },
23
44
  "dependencies": {
24
- "@digitalbazaar/did-io": "^1.0.0",
25
- "@digitalbazaar/http-client": "^1.1.0",
26
- "did-context": "^3.0.1",
45
+ "@digitalcredentials/did-io": "^1.0.2",
46
+ "@digitalcredentials/http-client": "^1.2.2",
47
+ "did-context": "^3.1.1",
27
48
  "ed25519-signature-2020-context": "^1.1.0",
28
- "esm": "^3.2.25",
29
- "whatwg-url": "^8.5.0",
30
49
  "x25519-key-agreement-2020-context": "^1.0.0"
31
50
  },
32
51
  "devDependencies": {
33
- "@babel/core": "^7.13.16",
34
- "@babel/plugin-transform-modules-commonjs": "^7.13.8",
35
- "@babel/plugin-transform-runtime": "^7.13.15",
36
- "@babel/preset-env": "^7.13.15",
37
- "@babel/runtime": "^7.13.17",
38
- "@digitalbazaar/ed25519-verification-key-2020": "^2.1.1",
39
- "@digitalbazaar/x25519-key-agreement-key-2020": "^1.2.0",
40
- "babel-loader": "^8.2.2",
52
+ "@babel/core": "^7.16.7",
53
+ "@babel/plugin-transform-modules-commonjs": "^7.16.7",
54
+ "@babel/plugin-transform-runtime": "^7.16.7",
55
+ "@babel/preset-env": "^7.16.7",
56
+ "@babel/runtime": "^7.16.7",
57
+ "@digitalcredentials/ed25519-verification-key-2020": "^3.2.2",
58
+ "@digitalcredentials/x25519-key-agreement-key-2020": "^2.0.2",
59
+ "babel-loader": "^8.2.3",
41
60
  "chai": "^4.3.4",
42
61
  "cross-env": "^7.0.3",
43
- "crypto-ld": "^5.1.0",
62
+ "crypto-ld": "^6.0.0",
63
+ "esm": "^3.2.25",
44
64
  "dirty-chai": "^2.0.1",
45
- "karma": "^6.3.2",
65
+ "karma": "^6.3.9",
46
66
  "karma-babel-preprocessor": "^8.0.1",
47
67
  "karma-chai": "^0.1.0",
48
68
  "karma-chrome-launcher": "^3.1.0",
@@ -50,13 +70,14 @@
50
70
  "karma-mocha-reporter": "^2.2.5",
51
71
  "karma-sourcemap-loader": "^0.3.8",
52
72
  "karma-webpack": "^5.0.0",
53
- "mocha": "^8.3.2",
73
+ "mocha": "^8.4.0",
54
74
  "nyc": "^15.1.0",
55
- "sinon": "^10.0.0",
56
- "standard": "^16.0.3",
57
- "webpack": "^5.35.1"
75
+ "sinon": "^12.0.1",
76
+ "standard": "^16.0.4",
77
+ "rimraf": "^3.0.2",
78
+ "rollup": "^2.62.0",
79
+ "webpack": "^5.65.0"
58
80
  },
59
- "main": "src/main.js",
60
81
  "nyc": {
61
82
  "reporter": [
62
83
  "html",
@@ -0,0 +1,15 @@
1
+ import pkg from './package.json'
2
+
3
+ export default [
4
+ {
5
+ input: './src/index.js',
6
+ output: [
7
+ {
8
+ dir: 'dist',
9
+ format: 'cjs',
10
+ preserveModules: true
11
+ }
12
+ ],
13
+ external: Object.keys(pkg.dependencies).concat(['crypto', 'util'])
14
+ }
15
+ ]
@@ -1,5 +1,5 @@
1
- import { httpClient } from '@digitalbazaar/http-client'
2
- import * as didIo from '@digitalbazaar/did-io'
1
+ import { httpClient } from '@digitalcredentials/http-client'
2
+ import * as didIo from '@digitalcredentials/did-io'
3
3
  import ed25519Context from 'ed25519-signature-2020-context'
4
4
  import x25519Context from 'x25519-key-agreement-2020-context'
5
5
  import didContext from 'did-context'
@@ -47,17 +47,33 @@ export function urlFromDid ({ did } = {}) {
47
47
  throw new TypeError(`DID Method not supported: "${did}".`)
48
48
  }
49
49
 
50
+ const [didUrl, hashFragment] = did.split('#')
50
51
  // eslint-disable-next-line no-unused-vars
51
- const [_did, _web, host, ...pathFragments] = did.split(':')
52
+ // const [didResource, query] = didUrl.split('?')
52
53
 
53
- let pathname = ''
54
- if (pathFragments.length === 0) {
55
- pathname = '/.well-known/did.json'
54
+ // eslint-disable-next-line no-unused-vars
55
+ const [_did, _web, urlNoProtocol] = didUrl.split(':')
56
+
57
+ let parsedUrl
58
+ try {
59
+ // URI-decode the url (in case it contained a port number,
60
+ // for example, `did:web:localhost%3A8080`
61
+ parsedUrl = new URL('https://' + decodeURIComponent(urlNoProtocol))
62
+ } catch (error) {
63
+ throw new TypeError(`Cannot construct url from did: "${did}".`)
64
+ }
65
+
66
+ if (!parsedUrl.pathname || parsedUrl.pathname === '/') {
67
+ parsedUrl.pathname = '/.well-known/did.json'
56
68
  } else {
57
- pathname = '/' + pathFragments.map(decodeURIComponent).join('/')
69
+ const pathFragments = parsedUrl.pathname.split('/')
70
+ parsedUrl.pathname = pathFragments.map(decodeURIComponent).join('/')
58
71
  }
59
72
 
60
- return 'https://' + decodeURIComponent(host) + pathname
73
+ if (hashFragment) {
74
+ parsedUrl.hash = hashFragment
75
+ }
76
+ return parsedUrl.toString()
61
77
  }
62
78
 
63
79
  /**
@@ -123,10 +139,17 @@ export async function initKeys ({ didDocument, cryptoLd, keyMap = {} } = {}) {
123
139
  }
124
140
 
125
141
  export class DidWebResolver {
126
- constructor ({ cryptoLd, keyMap = DEFAULT_KEY_MAP } = {}) {
127
- this.method = 'web' // did:web:...
142
+ /**
143
+ * @param cryptoLd {CryptoLD}
144
+ * @param keyMap {object}
145
+ * @param [logger] {object} Logger object (with .log, .error, .warn,
146
+ * etc methods).
147
+ */
148
+ constructor ({ cryptoLd, keyMap = DEFAULT_KEY_MAP, logger = console } = {}) {
149
+ this.method = 'web' // did:web:... (used for didIo resolver harness)
128
150
  this.cryptoLd = cryptoLd
129
151
  this.keyMap = keyMap
152
+ this.logger = logger
130
153
  }
131
154
 
132
155
  /**
@@ -190,32 +213,90 @@ export class DidWebResolver {
190
213
  * // In Node.js tests, use an agent to avoid self-signed certificate errors
191
214
  * const agent = new https.agent({rejectUnauthorized: false});
192
215
  *
193
- * @param {string} [did]
216
+ * @param {string} [did] For example, 'did:web:example.com'
194
217
  * @param {string} [url]
195
218
  * @param {https.Agent} [agent] Optional agent used to customize network
196
219
  * behavior in Node.js (such as `rejectUnauthorized: false`).
220
+ * @param {object} [logger] Logger object (with .log, .error, .warn,
221
+ * etc methods).
197
222
  *
198
223
  * @throws {Error}
199
224
  *
200
225
  * @returns {Promise<object>} Plain parsed JSON object of the DID Document.
201
226
  */
202
- async get ({ did, url, agent }) {
203
- url = url || urlFromDid({ did })
204
- if (!url) {
227
+ async get ({ did, url, agent, logger = this.logger }) {
228
+ const didUrl = url || urlFromDid({ did })
229
+ if (!didUrl) {
205
230
  throw new TypeError('A DID or a URL is required.')
206
231
  }
207
232
 
208
- let result
233
+ const [urlAuthority, keyIdFragment] = didUrl.split('#')
234
+
235
+ let didDocument
209
236
  try {
210
- result = await httpClient.get(url, { agent })
237
+ logger.info(`Fetching "${urlAuthority}" via http client.`)
238
+ const result = await httpClient.get(urlAuthority, { agent })
239
+ didDocument = result.data
211
240
  } catch (e) {
212
241
  // status is HTTP status code
213
242
  // data is JSON error from the server if available
214
243
  const { data, status } = e
215
- console.error(`Http ${status} error:`, data)
244
+ logger.error(`Http ${status} error:`, data)
216
245
  throw e
217
246
  }
247
+ if (didDocument && keyIdFragment) {
248
+ // resolve an individual key
249
+ // Keys are expected to have format: <did:web:...>#<keyIdFragment>
250
+ const didAuthority = didFromUrl({ url: urlAuthority })
251
+ const methodId = `${didAuthority}#${keyIdFragment}`
252
+
253
+ const key = didIo.findVerificationMethod({ doc: didDocument, methodId })
254
+ if (!key) {
255
+ throw new Error(`Key id ${methodId} not found.`)
256
+ }
257
+
258
+ const keyPair = await this.cryptoLd.from(key)
259
+
260
+ return keyPair.export({ publicKey: true, includeContext: true })
261
+ }
262
+
263
+ return didDocument
264
+ }
265
+
266
+ /**
267
+ * Returns the public key (verification method) object for a given DID
268
+ * Document and purpose. Useful in conjunction with a `.get()` call.
269
+ *
270
+ * @example
271
+ * const didDocument = await didKeyDriver.get({did});
272
+ * const authKeyData = didDriver.publicMethodFor({
273
+ * didDocument, purpose: 'authentication'
274
+ * });
275
+ * // You can then create a suite instance object to verify signatures etc.
276
+ * const authPublicKey = await cryptoLd.from(authKeyData);
277
+ * const {verify} = authPublicKey.verifier();
278
+ *
279
+ * @param {object} options - Options hashmap.
280
+ * @param {object} options.didDocument - DID Document (retrieved via a
281
+ * `.get()` or from some other source).
282
+ * @param {string} options.purpose - Verification method purpose, such as
283
+ * 'authentication', 'assertionMethod', 'keyAgreement' and so on.
284
+ *
285
+ * @returns {object} Returns the public key object (obtained from the DID
286
+ * Document), without a `@context`.
287
+ */
288
+ publicMethodFor ({ didDocument, purpose } = {}) {
289
+ if (!didDocument) {
290
+ throw new TypeError('The "didDocument" parameter is required.')
291
+ }
292
+ if (!purpose) {
293
+ throw new TypeError('The "purpose" parameter is required.')
294
+ }
218
295
 
219
- return result.data
296
+ const method = didIo.findVerificationMethod({ doc: didDocument, purpose })
297
+ if (!method) {
298
+ throw new Error(`No verification method found for purpose "${purpose}"`)
299
+ }
300
+ return method
220
301
  }
221
302
  }
@@ -1,35 +0,0 @@
1
- name: Node.js CI
2
-
3
- on: [push]
4
-
5
- jobs:
6
- test-node:
7
- runs-on: ubuntu-latest
8
- strategy:
9
- matrix:
10
- node-version: [14.x]
11
- steps:
12
- - uses: actions/checkout@v2
13
- - name: Use Node.js ${{ matrix.node-version }}
14
- uses: actions/setup-node@v1
15
- with:
16
- node-version: ${{ matrix.node-version }}
17
- - run: npm install
18
- - name: Run test with Node.js ${{ matrix.node-version }}
19
- run: npm run test-node
20
- env:
21
- CI: true
22
- lint:
23
- runs-on: ubuntu-latest
24
- strategy:
25
- matrix:
26
- node-version: [14.x]
27
- steps:
28
- - uses: actions/checkout@v2
29
- - name: Use Node.js ${{ matrix.node-version }}
30
- uses: actions/setup-node@v1
31
- with:
32
- node-version: ${{ matrix.node-version }}
33
- - run: npm install
34
- - name: Run Standard.js linter
35
- run: npm run standard
@@ -1,38 +0,0 @@
1
- <component name="ProjectCodeStyleConfiguration">
2
- <code_scheme name="Project" version="173">
3
- <option name="RIGHT_MARGIN" value="80" />
4
- <JSCodeStyleSettings version="0">
5
- <option name="USE_SEMICOLON_AFTER_STATEMENT" value="false" />
6
- <option name="FORCE_SEMICOLON_STYLE" value="true" />
7
- <option name="SPACE_BEFORE_GENERATOR_MULT" value="true" />
8
- <option name="USE_DOUBLE_QUOTES" value="false" />
9
- <option name="FORCE_QUOTE_STYlE" value="true" />
10
- <option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
11
- <option name="SPACES_WITHIN_IMPORTS" value="true" />
12
- </JSCodeStyleSettings>
13
- <codeStyleSettings language="HTML">
14
- <indentOptions>
15
- <option name="INDENT_SIZE" value="2" />
16
- <option name="CONTINUATION_INDENT_SIZE" value="2" />
17
- <option name="TAB_SIZE" value="2" />
18
- </indentOptions>
19
- </codeStyleSettings>
20
- <codeStyleSettings language="JavaScript">
21
- <option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
22
- <option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
23
- <option name="ALIGN_MULTILINE_FOR" value="false" />
24
- <option name="SPACE_BEFORE_METHOD_PARENTHESES" value="true" />
25
- <option name="TERNARY_OPERATION_SIGNS_ON_NEXT_LINE" value="true" />
26
- <option name="KEEP_SIMPLE_BLOCKS_IN_ONE_LINE" value="true" />
27
- <option name="KEEP_SIMPLE_METHODS_IN_ONE_LINE" value="true" />
28
- <indentOptions>
29
- <option name="INDENT_SIZE" value="2" />
30
- <option name="CONTINUATION_INDENT_SIZE" value="2" />
31
- <option name="TAB_SIZE" value="2" />
32
- </indentOptions>
33
- </codeStyleSettings>
34
- <codeStyleSettings language="Markdown">
35
- <option name="SOFT_MARGINS" value="80" />
36
- </codeStyleSettings>
37
- </code_scheme>
38
- </component>
@@ -1,5 +0,0 @@
1
- <component name="ProjectCodeStyleConfiguration">
2
- <state>
3
- <option name="USE_PER_PROJECT_SETTINGS" value="true" />
4
- </state>
5
- </component>
@@ -1,12 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <module type="WEB_MODULE" version="4">
3
- <component name="NewModuleRootManager">
4
- <content url="file://$MODULE_DIR$">
5
- <excludeFolder url="file://$MODULE_DIR$/temp" />
6
- <excludeFolder url="file://$MODULE_DIR$/.tmp" />
7
- <excludeFolder url="file://$MODULE_DIR$/tmp" />
8
- </content>
9
- <orderEntry type="inheritedJdk" />
10
- <orderEntry type="sourceFolder" forTests="false" />
11
- </component>
12
- </module>
@@ -1,6 +0,0 @@
1
- <component name="InspectionProjectProfileManager">
2
- <profile version="1.0">
3
- <option name="myName" value="Project Default" />
4
- <inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
5
- </profile>
6
- </component>
@@ -1,6 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="JavaScriptLibraryMappings">
4
- <includedPredefinedLibrary name="Node.js Core" />
5
- </component>
6
- </project>
package/.idea/modules.xml DELETED
@@ -1,8 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="ProjectModuleManager">
4
- <modules>
5
- <module fileurl="file://$PROJECT_DIR$/.idea/did-web-resolver.iml" filepath="$PROJECT_DIR$/.idea/did-web-resolver.iml" />
6
- </modules>
7
- </component>
8
- </project>
package/.idea/vcs.xml DELETED
@@ -1,6 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="VcsDirectoryMappings">
4
- <mapping directory="$PROJECT_DIR$" vcs="Git" />
5
- </component>
6
- </project>
package/.travis.yml DELETED
@@ -1,10 +0,0 @@
1
- language: node_js
2
- node_js:
3
- - "12"
4
-
5
- sudo: false
6
-
7
- notifications:
8
- email:
9
- on_success: change
10
- on_failure: change
package/CHANGELOG.md DELETED
@@ -1,18 +0,0 @@
1
- # did-web-driver ChangeLog
2
-
3
- ## 1.0.0 - 2021-04-24
4
-
5
- ### Changed
6
- - **BREAKING** Update to latest DID Core context
7
- - **BREAKING** Update to use crypto-ld v5 API, latest crypto suites
8
- - Add support for X25519KeyAgreementKey suite
9
-
10
- ## 0.2.0 - 2020-08-01
11
-
12
- ### Changed
13
- - **BREAKING**: Update to use crypto-ld v4 API
14
-
15
- ## 0.0.1
16
-
17
- ### Added
18
- - Initial implementation.
package/src/main.js DELETED
@@ -1,3 +0,0 @@
1
- // translate `index.js` to CommonJS
2
- require = require('esm')(module) // eslint-disable-line no-native-reassign, no-global-assign
3
- module.exports = require('./index.js')
@@ -1,40 +0,0 @@
1
- module.exports = (config) => {
2
- const bundler = process.env.BUNDLER || 'webpack'
3
- const frameworks = ['mocha']
4
- const files = ['**/*.spec.js']
5
- const reporters = ['mocha']
6
- const browsers = ['ChromeHeadless']
7
- const client = {
8
- mocha: {
9
- timeout: 2000
10
- }
11
- }
12
- // main bundle preprocessors
13
- const preprocessors = []
14
- preprocessors.push(bundler)
15
- preprocessors.push('sourcemap')
16
-
17
- return config.set({
18
- frameworks,
19
- files,
20
- reporters,
21
- basePath: '',
22
- port: 9876,
23
- colors: true,
24
- browsers,
25
- client,
26
- singleRun: true,
27
- preprocessors: {
28
- 'unit/*.js': preprocessors
29
- },
30
- webpack: {
31
- devtool: 'inline-source-map',
32
- mode: 'development',
33
- node: {
34
- Buffer: false,
35
- crypto: false,
36
- setImmediate: false
37
- }
38
- }
39
- })
40
- }
@@ -1,165 +0,0 @@
1
- import chai from 'chai'
2
- import dirtyChai from 'dirty-chai'
3
-
4
- import { DidWebResolver, urlFromDid, didFromUrl } from '../../src'
5
-
6
- import { Ed25519VerificationKey2020 }
7
- from '@digitalbazaar/ed25519-verification-key-2020'
8
- import { X25519KeyAgreementKey2020 }
9
- from '@digitalbazaar/x25519-key-agreement-key-2020'
10
- import { CryptoLD } from 'crypto-ld'
11
- chai.use(dirtyChai)
12
- chai.should()
13
- const { expect } = chai
14
-
15
- const cryptoLd = new CryptoLD()
16
- cryptoLd.use(Ed25519VerificationKey2020)
17
- cryptoLd.use(X25519KeyAgreementKey2020)
18
-
19
- describe('DidWebDriver', () => {
20
- describe('constructor', () => {
21
- it('should exist', () => {
22
- expect(new DidWebResolver()).to.exist()
23
- })
24
- })
25
-
26
- describe('generate()', () => {
27
- let didWeb
28
-
29
- beforeEach(async () => {
30
- didWeb = new DidWebResolver({ cryptoLd })
31
- })
32
-
33
- it('should generate using default key map', async () => {
34
- const url = 'https://example.com'
35
- const { didDocument, keyPairs } = await didWeb.generate({ url })
36
-
37
- expect(didDocument).to.have.property('@context')
38
- expect(didDocument.id).to.equal('did:web:example.com')
39
- expect(didDocument.capabilityInvocation[0].type)
40
- .to.equal('Ed25519VerificationKey2020')
41
- expect(didDocument.authentication[0].type)
42
- .to.equal('Ed25519VerificationKey2020')
43
- expect(didDocument.assertionMethod[0].type)
44
- .to.equal('Ed25519VerificationKey2020')
45
- expect(didDocument.capabilityDelegation[0].type)
46
- .to.equal('Ed25519VerificationKey2020')
47
-
48
- expect(keyPairs).to.exist()
49
- })
50
-
51
- it('should return methodFor convenience function', async () => {
52
- const url = 'https://example.com'
53
- const { methodFor } = await didWeb.generate({ url })
54
-
55
- const keyAgreementKey = methodFor({ purpose: 'keyAgreement' })
56
-
57
- expect(keyAgreementKey).to.have.property('type', 'X25519KeyAgreementKey2020')
58
- expect(keyAgreementKey).to.have.property('controller', 'did:web:example.com')
59
- expect(keyAgreementKey).to.have.property('publicKeyMultibase')
60
- expect(keyAgreementKey).to.have.property('privateKeyMultibase')
61
- })
62
- })
63
-
64
- describe('urlFromDid()', () => {
65
- it('should error on missing did', () => {
66
- let error
67
- try {
68
- urlFromDid()
69
- } catch (e) {
70
- error = e
71
- }
72
- expect(error.message).to.equal('Cannot convert did to url, missing did.')
73
- })
74
-
75
- it('should error on non-did:web dids', () => {
76
- let error
77
- try {
78
- urlFromDid({ did: 'did:example:1234' })
79
- } catch (e) {
80
- error = e
81
- }
82
- expect(error.message)
83
- .to.equal('DID Method not supported: "did:example:1234".')
84
- })
85
-
86
- it('should convert first id fragment to pathname plus default path', () => {
87
- expect(urlFromDid({ did: 'did:web:example.com' }))
88
- .to.equal('https://example.com/.well-known/did.json')
89
- })
90
-
91
- it('should url-decode host', () => {
92
- expect(urlFromDid({ did: 'did:web:localhost%3A8080' }))
93
- .to.equal('https://localhost:8080/.well-known/did.json')
94
- })
95
-
96
- it('should url-decode path fragments', () => {
97
- expect(urlFromDid({ did: 'did:web:example.com:path:some%2Bsubpath' }))
98
- .to.equal('https://example.com/path/some+subpath')
99
- })
100
- })
101
-
102
- describe('didFromUrl', () => {
103
- it('should error on missing url', () => {
104
- let error
105
- try {
106
- didFromUrl()
107
- } catch (e) {
108
- error = e
109
- }
110
- expect(error.message).to.equal('Cannot convert url to did, missing url.')
111
- })
112
-
113
- it('should error on http URLs', () => {
114
- let error
115
- try {
116
- didFromUrl({ url: 'http://example.com' })
117
- } catch (e) {
118
- error = e
119
- }
120
- expect(error.message).to.equal('did:web does not support non-HTTPS URLs.')
121
- })
122
-
123
- it('should error on invalid URLs', () => {
124
- let error
125
- try {
126
- didFromUrl({ url: 'non-url' })
127
- } catch (e) {
128
- error = e
129
- }
130
- expect(error.message).to.equal('Invalid url: "non-url".')
131
- })
132
-
133
- it('should convert host to did identifier', () => {
134
- expect(didFromUrl({ url: 'https://localhost' }))
135
- .to.equal('did:web:localhost')
136
- expect(didFromUrl({ url: 'https://example.com' }))
137
- .to.equal('did:web:example.com')
138
- })
139
-
140
- it('should url-encode host', () => {
141
- expect(didFromUrl({ url: 'https://localhost:8080' }))
142
- .to.equal('did:web:localhost%3A8080')
143
- })
144
-
145
- it('should leave off the default / path', () => {
146
- expect(didFromUrl({ url: 'https://example.com/' }))
147
- .to.equal('did:web:example.com')
148
- })
149
-
150
- it('should encode path / separators as :', () => {
151
- expect(didFromUrl({ url: 'https://example.com/path/subpath/did.json' }))
152
- .to.equal('did:web:example.com:path:subpath:did.json')
153
- })
154
-
155
- it('should drop the default /.well-known/did.json pathname', () => {
156
- expect(didFromUrl({ url: 'https://example.com/.well-known/did.json' }))
157
- .to.equal('did:web:example.com')
158
- })
159
-
160
- it('should url-encode path fragments', () => {
161
- expect(didFromUrl({ url: 'https://example.com/path/some+subpath' }))
162
- .to.equal('did:web:example.com:path:some%2Bsubpath')
163
- })
164
- })
165
- })