@interop/did-web-resolver 1.1.0 → 2.2.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
@@ -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,16 +51,47 @@ 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
  ```
58
58
 
59
59
  ### Generating a new DID
60
60
 
61
+ #### Generating from seed
62
+
63
+ If you have a deterministic secret seed (created with [`did-cli`](https://github.com/digitalcredentials/did-cli),
64
+ for example) and would like to generate a `did:web` document from it:
65
+
61
66
  ```js
62
- const { didDocument, keyPairs, methodFor } = await didWebDriver.generate()
67
+ const url = 'https://example.com'
68
+ const seed = 'z1AhV1bADy7RepJ64mvH7Kk7htFNGc7EA1WA5nGzLSTWc6o'
63
69
 
70
+ const { didDocument, keyPairs, methodFor } = await didWebDriver.generate({ url, seed })
71
+ // didDocument
72
+ {
73
+ '@context': [
74
+ 'https://www.w3.org/ns/did/v1',
75
+ 'https://w3id.org/security/suites/ed25519-2020/v1',
76
+ 'https://w3id.org/security/suites/x25519-2020/v1'
77
+ ],
78
+ id: 'did:web:example.com',
79
+ assertionMethod: [{
80
+ id: 'did:web:example.com#z6MkmDMjfkjs9XPCN1LfoQQRHz1mJ8PEdiVYC66XKhj3wGyB',
81
+ type: 'Ed25519VerificationKey2020',
82
+ controller: 'did:web:example.com',
83
+ publicKeyMultibase: 'z6MkmDMjfkjs9XPCN1LfoQQRHz1mJ8PEdiVYC66XKhj3wGyB'
84
+ }]
85
+ }
86
+ ```
87
+
88
+ #### Generating new random keys
89
+
90
+ Invoking `generate()` by itself will create new keypairs for each proof purpose.
91
+
92
+ ```js
93
+ const { didDocument, keyPairs, methodFor } = await didWebDriver.generate()
94
+ // didDocument
64
95
  {
65
96
  '@context': [
66
97
  'https://www.w3.org/ns/did/v1',
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,379 @@
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
+ var bnid = require('@digitalcredentials/bnid');
11
+
12
+ function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
13
+
14
+ function _interopNamespace(e) {
15
+ if (e && e.__esModule) return e;
16
+ var n = Object.create(null);
17
+ if (e) {
18
+ Object.keys(e).forEach(function (k) {
19
+ if (k !== 'default') {
20
+ var d = Object.getOwnPropertyDescriptor(e, k);
21
+ Object.defineProperty(n, k, d.get ? d : {
22
+ enumerable: true,
23
+ get: function () { return e[k]; }
24
+ });
25
+ }
26
+ });
27
+ }
28
+ n["default"] = e;
29
+ return Object.freeze(n);
30
+ }
31
+
32
+ var didIo__namespace = /*#__PURE__*/_interopNamespace(didIo);
33
+ var ed25519Context__default = /*#__PURE__*/_interopDefaultLegacy(ed25519Context);
34
+ var x25519Context__default = /*#__PURE__*/_interopDefaultLegacy(x25519Context);
35
+ var didContext__default = /*#__PURE__*/_interopDefaultLegacy(didContext);
36
+
37
+ const { VERIFICATION_RELATIONSHIPS } = didIo__namespace;
38
+
39
+ const DEFAULT_KEY_MAP = {
40
+ capabilityInvocation: 'Ed25519VerificationKey2020',
41
+ authentication: 'Ed25519VerificationKey2020',
42
+ assertionMethod: 'Ed25519VerificationKey2020',
43
+ capabilityDelegation: 'Ed25519VerificationKey2020',
44
+ keyAgreement: 'X25519KeyAgreementKey2020'
45
+ };
46
+
47
+ function didFromUrl ({ url } = {}) {
48
+ if (!url) {
49
+ throw new TypeError('Cannot convert url to did, missing url.')
50
+ }
51
+ if (url.startsWith('http:')) {
52
+ throw new TypeError('did:web does not support non-HTTPS URLs.')
53
+ }
54
+
55
+ let parsedUrl;
56
+ try {
57
+ parsedUrl = new URL(url);
58
+ } catch (error) {
59
+ throw new TypeError(`Invalid url: "${url}".`)
60
+ }
61
+
62
+ const { host, pathname } = parsedUrl;
63
+
64
+ let pathComponent = '';
65
+ if (pathname && pathname !== '/' && pathname !== '/.well-known/did.json') {
66
+ pathComponent = pathname.split('/').map(encodeURIComponent).join(':');
67
+ }
68
+
69
+ return 'did:web:' + encodeURIComponent(host) + pathComponent
70
+ }
71
+
72
+ function urlFromDid ({ did } = {}) {
73
+ if (!did) {
74
+ throw new TypeError('Cannot convert did to url, missing did.')
75
+ }
76
+ if (!did.startsWith('did:web:')) {
77
+ throw new TypeError(`DID Method not supported: "${did}".`)
78
+ }
79
+
80
+ const [didUrl, hashFragment] = did.split('#');
81
+ // eslint-disable-next-line no-unused-vars
82
+ // const [didResource, query] = didUrl.split('?')
83
+
84
+ // eslint-disable-next-line no-unused-vars
85
+ const [_did, _web, urlNoProtocol] = didUrl.split(':');
86
+
87
+ let parsedUrl;
88
+ try {
89
+ // URI-decode the url (in case it contained a port number,
90
+ // for example, `did:web:localhost%3A8080`
91
+ parsedUrl = new URL('https://' + decodeURIComponent(urlNoProtocol));
92
+ } catch (error) {
93
+ throw new TypeError(`Cannot construct url from did: "${did}".`)
94
+ }
95
+
96
+ if (!parsedUrl.pathname || parsedUrl.pathname === '/') {
97
+ parsedUrl.pathname = '/.well-known/did.json';
98
+ } else {
99
+ const pathFragments = parsedUrl.pathname.split('/');
100
+ parsedUrl.pathname = pathFragments.map(decodeURIComponent).join('/');
101
+ }
102
+
103
+ if (hashFragment) {
104
+ parsedUrl.hash = hashFragment;
105
+ }
106
+ return parsedUrl.toString()
107
+ }
108
+
109
+ /**
110
+ * Initializes the DID Document's keys/proof methods.
111
+ *
112
+ * @example
113
+ * didDocument.id = 'did:ex:123';
114
+ * const {didDocument, keyPairs} = await initKeys({
115
+ * didDocument,
116
+ * cryptoLd,
117
+ * keyMap: {
118
+ * capabilityInvocation: someExistingKey,
119
+ * authentication: 'Ed25519VerificationKey2020',
120
+ * assertionMethod: 'Ed25519VerificationKey2020',
121
+ * keyAgreement: 'X25519KeyAgreementKey2019'
122
+ * }
123
+ * });.
124
+ *
125
+ * @param {object} options - Options hashmap.
126
+ * @param {object} options.didDocument - DID Document.
127
+ * @typedef {object} CryptoLD
128
+ * @param {CryptoLD} [options.cryptoLd] - CryptoLD driver instance,
129
+ * initialized with the key types this DID Document intends to support.
130
+ * @param {object} [options.keyMap] - Map of keys (or key types) by purpose.
131
+ *
132
+ * @returns {Promise<{didDocument: object, keyPairs: Map}>} Resolves with the
133
+ * DID Document initialized with keys, as well as the map of the corresponding
134
+ * key pairs (by key id).
135
+ */
136
+ async function initKeys ({ didDocument, cryptoLd, keyMap } = {}) {
137
+ const doc = { ...didDocument };
138
+ if (!doc.id) {
139
+ throw new TypeError(
140
+ 'DID Document "id" property is required to initialize keys.')
141
+ }
142
+
143
+ const keyPairs = new Map();
144
+
145
+ // Set the defaults for the created keys (if needed)
146
+ const options = { controller: doc.id };
147
+
148
+ for (const purpose in keyMap) {
149
+ if (!VERIFICATION_RELATIONSHIPS.has(purpose)) {
150
+ throw new Error(`Unsupported key purpose: "${purpose}".`)
151
+ }
152
+
153
+ let key;
154
+ if (typeof keyMap[purpose] === 'string') {
155
+ if (!cryptoLd) {
156
+ throw new Error('Please provide an initialized CryptoLD instance.')
157
+ }
158
+ key = await cryptoLd.generate({ type: keyMap[purpose], ...options });
159
+ } else {
160
+ // An existing key has been provided
161
+ key = keyMap[purpose];
162
+ }
163
+
164
+ doc[purpose] = [key.export({ publicKey: true })];
165
+ keyPairs.set(key.id, key);
166
+ }
167
+
168
+ return { didDocument: doc, keyPairs }
169
+ }
170
+
171
+ class DidWebResolver {
172
+ /**
173
+ * @param cryptoLd {CryptoLD}
174
+ * @param keyMap {object}
175
+ * @param [logger] {object} Logger object (with .log, .error, .warn,
176
+ * etc methods).
177
+ */
178
+ constructor ({ cryptoLd, keyMap = DEFAULT_KEY_MAP, logger = console } = {}) {
179
+ this.method = 'web'; // did:web:... (used for didIo resolver harness)
180
+ this.cryptoLd = cryptoLd;
181
+ this.keyMap = keyMap;
182
+ this.logger = logger;
183
+ }
184
+
185
+ /**
186
+ * Generates a new DID Document and initializes various authentication
187
+ * and authorization proof purpose keys.
188
+ *
189
+ * @example
190
+ * const url = 'https://example.com'
191
+ * const { didDocument, didKeys } = await didWeb.generate({url})
192
+ * didDocument.id
193
+ * // -> 'did:web:example.com'
194
+ *
195
+ *
196
+ * Either an `id` or a `url` is required:
197
+ * @param [id] {string} - A did:web DID. If absent, will be converted from url
198
+ * @param [url] {string}
199
+ *
200
+ * @param [keyMap] {object} A hashmap of key types by purpose.
201
+ *
202
+ * @parma [cryptoLd] {object} CryptoLD instance with support for supported
203
+ * crypto suites installed.
204
+ *
205
+ * @returns {Promise<{didDocument: object, keyPairs: Map,
206
+ * methodFor: Function}>} Resolves with the generated DID Document, along
207
+ * with the corresponding key pairs used to generate it (for storage in a
208
+ * KMS).
209
+ */
210
+ async generate ({ id, url, seed, keyMap, cryptoLd = this.cryptoLd } = {}) {
211
+ if (!id && !url) {
212
+ throw new TypeError('A "url" or an "id" parameter is required.')
213
+ }
214
+ if (seed && keyMap) {
215
+ throw new TypeError(
216
+ 'Either a "seed" or a "keyMap" param must be provided, but not both.'
217
+ )
218
+ }
219
+
220
+ const did = id || didFromUrl({ url });
221
+
222
+ if (seed) {
223
+ const keyPair = await _keyPairFromSecretSeed({
224
+ seed, controller: did, cryptoLd
225
+ });
226
+ keyMap = { assertionMethod: keyPair };
227
+ } else {
228
+ keyMap = keyMap || this.keyMap;
229
+ }
230
+
231
+ // Compose the DID Document
232
+ let didDocument = {
233
+ '@context': [
234
+ didContext__default["default"].constants.DID_CONTEXT_URL,
235
+ ed25519Context__default["default"].constants.CONTEXT_URL,
236
+ x25519Context__default["default"].constants.CONTEXT_URL
237
+ ],
238
+ id: did
239
+ };
240
+
241
+ const result = await initKeys({ didDocument, cryptoLd, keyMap });
242
+ const keyPairs = result.keyPairs;
243
+ didDocument = result.didDocument;
244
+
245
+ // Convenience function that returns the public/private key pair instance
246
+ // for a given purpose (authentication, assertionMethod, keyAgreement, etc).
247
+ const methodFor = ({ purpose }) => {
248
+ const { id: methodId } = didIo__namespace.findVerificationMethod({
249
+ doc: didDocument, purpose
250
+ });
251
+ return keyPairs.get(methodId)
252
+ };
253
+
254
+ return { didDocument, keyPairs, methodFor }
255
+ }
256
+
257
+ /**
258
+ * Fetches a DID Document for a given DID.
259
+ *
260
+ * @example
261
+ * // In Node.js tests, use an agent to avoid self-signed certificate errors
262
+ * const agent = new https.agent({rejectUnauthorized: false});
263
+ *
264
+ * @param {string} [did] For example, 'did:web:example.com'
265
+ * @param {string} [url]
266
+ * @param {https.Agent} [agent] Optional agent used to customize network
267
+ * behavior in Node.js (such as `rejectUnauthorized: false`).
268
+ * @param {object} [logger] Logger object (with .log, .error, .warn,
269
+ * etc methods).
270
+ *
271
+ * @throws {Error}
272
+ *
273
+ * @returns {Promise<object>} Plain parsed JSON object of the DID Document.
274
+ */
275
+ async get ({ did, url, agent, logger = this.logger }) {
276
+ const didUrl = url || urlFromDid({ did });
277
+ if (!didUrl) {
278
+ throw new TypeError('A DID or a URL is required.')
279
+ }
280
+
281
+ const [urlAuthority, keyIdFragment] = didUrl.split('#');
282
+
283
+ let didDocument;
284
+ try {
285
+ logger.info(`Fetching "${urlAuthority}" via http client.`);
286
+ const result = await httpClient.httpClient.get(urlAuthority, { agent });
287
+ didDocument = result.data;
288
+ } catch (e) {
289
+ // status is HTTP status code
290
+ // data is JSON error from the server if available
291
+ const { data, status } = e;
292
+ logger.error(`Http ${status} error:`, data);
293
+ throw e
294
+ }
295
+ if (didDocument && keyIdFragment) {
296
+ // resolve an individual key
297
+ // Keys are expected to have format: <did:web:...>#<keyIdFragment>
298
+ const didAuthority = didFromUrl({ url: urlAuthority });
299
+ const methodId = `${didAuthority}#${keyIdFragment}`;
300
+
301
+ const key = didIo__namespace.findVerificationMethod({ doc: didDocument, methodId });
302
+ if (!key) {
303
+ throw new Error(`Key id ${methodId} not found.`)
304
+ }
305
+
306
+ const keyPair = await this.cryptoLd.from(key);
307
+
308
+ return keyPair.export({ publicKey: true, includeContext: true })
309
+ }
310
+
311
+ return didDocument
312
+ }
313
+
314
+ /**
315
+ * Returns the public key (verification method) object for a given DID
316
+ * Document and purpose. Useful in conjunction with a `.get()` call.
317
+ *
318
+ * @example
319
+ * const didDocument = await didKeyDriver.get({did});
320
+ * const authKeyData = didDriver.publicMethodFor({
321
+ * didDocument, purpose: 'authentication'
322
+ * });
323
+ * // You can then create a suite instance object to verify signatures etc.
324
+ * const authPublicKey = await cryptoLd.from(authKeyData);
325
+ * const {verify} = authPublicKey.verifier();
326
+ *
327
+ * @param {object} options - Options hashmap.
328
+ * @param {object} options.didDocument - DID Document (retrieved via a
329
+ * `.get()` or from some other source).
330
+ * @param {string} options.purpose - Verification method purpose, such as
331
+ * 'authentication', 'assertionMethod', 'keyAgreement' and so on.
332
+ *
333
+ * @returns {object} Returns the public key object (obtained from the DID
334
+ * Document), without a `@context`.
335
+ */
336
+ publicMethodFor ({ didDocument, purpose } = {}) {
337
+ if (!didDocument) {
338
+ throw new TypeError('The "didDocument" parameter is required.')
339
+ }
340
+ if (!purpose) {
341
+ throw new TypeError('The "purpose" parameter is required.')
342
+ }
343
+
344
+ const method = didIo__namespace.findVerificationMethod({ doc: didDocument, purpose });
345
+ if (!method) {
346
+ throw new Error(`No verification method found for purpose "${purpose}"`)
347
+ }
348
+ return method
349
+ }
350
+ }
351
+
352
+ /**
353
+ * @param options {object}
354
+ * @param options.seed {string|Uint8Array}
355
+ * @param controller {string}
356
+ * @param cryptoLd {object}
357
+ *
358
+ * @return {Promise<LDKeyPair>}
359
+ */
360
+ async function _keyPairFromSecretSeed ({ seed, controller, cryptoLd } = {}) {
361
+ let seedBytes;
362
+ if (typeof seed === 'string') {
363
+ // Currently only supports base58 multibase / identity multihash encoding.
364
+ if (!seed.startsWith('z1A')) {
365
+ throw new TypeError('"seed" parameter must be a multibase/multihash encoded string, or a Uint8Array.')
366
+ }
367
+ seedBytes = bnid.decodeSecretKeySeed({ secretKeySeed: seed });
368
+ } else {
369
+ seedBytes = new Uint8Array(seed);
370
+ }
371
+ return cryptoLd.generate({
372
+ controller, seed: seedBytes, type: 'Ed25519VerificationKey2020'
373
+ })
374
+ }
375
+
376
+ exports.DidWebResolver = DidWebResolver;
377
+ exports.didFromUrl = didFromUrl;
378
+ exports.initKeys = initKeys;
379
+ 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.1.0",
4
+ "version": "2.2.0",
5
5
  "author": {
6
6
  "name": "Dmitri Zagidulin",
7
7
  "url": "https://github.com/dmitrizagidulin/"
@@ -14,34 +14,57 @@
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
+ "./package.json": "./package.json"
44
+ },
23
45
  "dependencies": {
24
- "@digitalbazaar/did-io": "^1.0.0",
25
- "@digitalbazaar/http-client": "^1.1.0",
26
- "did-context": "^3.0.1",
46
+ "@digitalcredentials/bnid": "^2.1.1",
47
+ "@digitalcredentials/did-io": "^1.0.2",
48
+ "@digitalcredentials/http-client": "^1.2.2",
49
+ "did-context": "^3.1.1",
27
50
  "ed25519-signature-2020-context": "^1.1.0",
28
- "esm": "^3.2.25",
29
51
  "x25519-key-agreement-2020-context": "^1.0.0"
30
52
  },
31
53
  "devDependencies": {
32
- "@babel/core": "^7.13.16",
33
- "@babel/plugin-transform-modules-commonjs": "^7.13.8",
34
- "@babel/plugin-transform-runtime": "^7.13.15",
35
- "@babel/preset-env": "^7.13.15",
36
- "@babel/runtime": "^7.13.17",
37
- "@digitalbazaar/ed25519-verification-key-2020": "^2.1.1",
38
- "@digitalbazaar/x25519-key-agreement-key-2020": "^1.2.0",
39
- "babel-loader": "^8.2.2",
54
+ "@babel/core": "^7.16.7",
55
+ "@babel/plugin-transform-modules-commonjs": "^7.16.7",
56
+ "@babel/plugin-transform-runtime": "^7.16.7",
57
+ "@babel/preset-env": "^7.16.7",
58
+ "@babel/runtime": "^7.16.7",
59
+ "@digitalcredentials/ed25519-verification-key-2020": "^3.2.2",
60
+ "@digitalcredentials/x25519-key-agreement-key-2020": "^2.0.2",
61
+ "babel-loader": "^8.2.3",
40
62
  "chai": "^4.3.4",
41
63
  "cross-env": "^7.0.3",
42
- "crypto-ld": "^5.1.0",
64
+ "crypto-ld": "^6.0.0",
43
65
  "dirty-chai": "^2.0.1",
44
- "karma": "^6.3.2",
66
+ "esm": "^3.2.25",
67
+ "karma": "^6.3.9",
45
68
  "karma-babel-preprocessor": "^8.0.1",
46
69
  "karma-chai": "^0.1.0",
47
70
  "karma-chrome-launcher": "^3.1.0",
@@ -49,13 +72,14 @@
49
72
  "karma-mocha-reporter": "^2.2.5",
50
73
  "karma-sourcemap-loader": "^0.3.8",
51
74
  "karma-webpack": "^5.0.0",
52
- "mocha": "^8.3.2",
75
+ "mocha": "^8.4.0",
53
76
  "nyc": "^15.1.0",
54
- "sinon": "^10.0.0",
55
- "standard": "^16.0.3",
56
- "webpack": "^5.35.1"
77
+ "rimraf": "^3.0.2",
78
+ "rollup": "^2.62.0",
79
+ "sinon": "^12.0.1",
80
+ "standard": "^16.0.4",
81
+ "webpack": "^5.65.0"
57
82
  },
58
- "main": "src/main.js",
59
83
  "nyc": {
60
84
  "reporter": [
61
85
  "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,8 +1,9 @@
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'
6
+ import { decodeSecretKeySeed } from '@digitalcredentials/bnid'
6
7
 
7
8
  const { VERIFICATION_RELATIONSHIPS } = didIo
8
9
 
@@ -49,25 +50,31 @@ export function urlFromDid ({ did } = {}) {
49
50
 
50
51
  const [didUrl, hashFragment] = did.split('#')
51
52
  // eslint-disable-next-line no-unused-vars
52
- const [didResource, query] = didUrl.split('?')
53
+ // const [didResource, query] = didUrl.split('?')
53
54
 
54
55
  // eslint-disable-next-line no-unused-vars
55
- const [_did, _web, host, ...pathFragments] = didResource.split(':')
56
+ const [_did, _web, urlNoProtocol] = didUrl.split(':')
56
57
 
57
- let pathname = ''
58
- if (pathFragments.length === 0) {
59
- pathname = '/.well-known/did.json'
58
+ let parsedUrl
59
+ try {
60
+ // URI-decode the url (in case it contained a port number,
61
+ // for example, `did:web:localhost%3A8080`
62
+ parsedUrl = new URL('https://' + decodeURIComponent(urlNoProtocol))
63
+ } catch (error) {
64
+ throw new TypeError(`Cannot construct url from did: "${did}".`)
65
+ }
66
+
67
+ if (!parsedUrl.pathname || parsedUrl.pathname === '/') {
68
+ parsedUrl.pathname = '/.well-known/did.json'
60
69
  } else {
61
- pathname = '/' + pathFragments.map(decodeURIComponent).join('/')
70
+ const pathFragments = parsedUrl.pathname.split('/')
71
+ parsedUrl.pathname = pathFragments.map(decodeURIComponent).join('/')
62
72
  }
63
73
 
64
- const url = new URL(pathname, 'https://' + decodeURIComponent(host))
65
74
  if (hashFragment) {
66
- url.hash = hashFragment
75
+ parsedUrl.hash = hashFragment
67
76
  }
68
- // TODO: Not passing on the query part currently; reserved for DID URLs?
69
-
70
- return url.toString()
77
+ return parsedUrl.toString()
71
78
  }
72
79
 
73
80
  /**
@@ -97,7 +104,7 @@ export function urlFromDid ({ did } = {}) {
97
104
  * DID Document initialized with keys, as well as the map of the corresponding
98
105
  * key pairs (by key id).
99
106
  */
100
- export async function initKeys ({ didDocument, cryptoLd, keyMap = {} } = {}) {
107
+ export async function initKeys ({ didDocument, cryptoLd, keyMap } = {}) {
101
108
  const doc = { ...didDocument }
102
109
  if (!doc.id) {
103
110
  throw new TypeError(
@@ -161,7 +168,7 @@ export class DidWebResolver {
161
168
  * @param [id] {string} - A did:web DID. If absent, will be converted from url
162
169
  * @param [url] {string}
163
170
  *
164
- * @param [keyMap=DEFAULT_KEY_MAP] {object} A hashmap of key types by purpose.
171
+ * @param [keyMap] {object} A hashmap of key types by purpose.
165
172
  *
166
173
  * @parma [cryptoLd] {object} CryptoLD instance with support for supported
167
174
  * crypto suites installed.
@@ -171,9 +178,27 @@ export class DidWebResolver {
171
178
  * with the corresponding key pairs used to generate it (for storage in a
172
179
  * KMS).
173
180
  */
174
- async generate ({ id, url, keyMap = this.keyMap, cryptoLd = this.cryptoLd } = {}) {
181
+ async generate ({ id, url, seed, keyMap, cryptoLd = this.cryptoLd } = {}) {
182
+ if (!id && !url) {
183
+ throw new TypeError('A "url" or an "id" parameter is required.')
184
+ }
185
+ if (seed && keyMap) {
186
+ throw new TypeError(
187
+ 'Either a "seed" or a "keyMap" param must be provided, but not both.'
188
+ )
189
+ }
190
+
175
191
  const did = id || didFromUrl({ url })
176
192
 
193
+ if (seed) {
194
+ const keyPair = await _keyPairFromSecretSeed({
195
+ seed, controller: did, cryptoLd
196
+ })
197
+ keyMap = { assertionMethod: keyPair }
198
+ } else {
199
+ keyMap = keyMap || this.keyMap
200
+ }
201
+
177
202
  // Compose the DID Document
178
203
  let didDocument = {
179
204
  '@context': [
@@ -224,10 +249,13 @@ export class DidWebResolver {
224
249
  throw new TypeError('A DID or a URL is required.')
225
250
  }
226
251
 
227
- let result
252
+ const [urlAuthority, keyIdFragment] = didUrl.split('#')
253
+
254
+ let didDocument
228
255
  try {
229
- logger.info(`Fetching "${didUrl}" via http client.`)
230
- result = await httpClient.get(didUrl, { agent })
256
+ logger.info(`Fetching "${urlAuthority}" via http client.`)
257
+ const result = await httpClient.get(urlAuthority, { agent })
258
+ didDocument = result.data
231
259
  } catch (e) {
232
260
  // status is HTTP status code
233
261
  // data is JSON error from the server if available
@@ -235,8 +263,23 @@ export class DidWebResolver {
235
263
  logger.error(`Http ${status} error:`, data)
236
264
  throw e
237
265
  }
266
+ if (didDocument && keyIdFragment) {
267
+ // resolve an individual key
268
+ // Keys are expected to have format: <did:web:...>#<keyIdFragment>
269
+ const didAuthority = didFromUrl({ url: urlAuthority })
270
+ const methodId = `${didAuthority}#${keyIdFragment}`
238
271
 
239
- return result.data
272
+ const key = didIo.findVerificationMethod({ doc: didDocument, methodId })
273
+ if (!key) {
274
+ throw new Error(`Key id ${methodId} not found.`)
275
+ }
276
+
277
+ const keyPair = await this.cryptoLd.from(key)
278
+
279
+ return keyPair.export({ publicKey: true, includeContext: true })
280
+ }
281
+
282
+ return didDocument
240
283
  }
241
284
 
242
285
  /**
@@ -276,3 +319,27 @@ export class DidWebResolver {
276
319
  return method
277
320
  }
278
321
  }
322
+
323
+ /**
324
+ * @param options {object}
325
+ * @param options.seed {string|Uint8Array}
326
+ * @param controller {string}
327
+ * @param cryptoLd {object}
328
+ *
329
+ * @return {Promise<LDKeyPair>}
330
+ */
331
+ async function _keyPairFromSecretSeed ({ seed, controller, cryptoLd } = {}) {
332
+ let seedBytes
333
+ if (typeof seed === 'string') {
334
+ // Currently only supports base58 multibase / identity multihash encoding.
335
+ if (!seed.startsWith('z1A')) {
336
+ throw new TypeError('"seed" parameter must be a multibase/multihash encoded string, or a Uint8Array.')
337
+ }
338
+ seedBytes = decodeSecretKeySeed({ secretKeySeed: seed })
339
+ } else {
340
+ seedBytes = new Uint8Array(seed)
341
+ }
342
+ return cryptoLd.generate({
343
+ controller, seed: seedBytes, type: 'Ed25519VerificationKey2020'
344
+ })
345
+ }
@@ -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,29 +0,0 @@
1
- # did-web-driver ChangeLog
2
-
3
- ## 1.1.0 - 2021-04-25
4
-
5
- ### Added
6
- - Add `didWebDriver.publicMethodFor()`.
7
-
8
- ## 1.0.1 - 2021-04-25
9
-
10
- ### Fixed
11
- - Fix handling of hash fragments by `urlFromDid()`.
12
- - Add logger to constructor.
13
-
14
- ## 1.0.0 - 2021-04-24
15
-
16
- ### Changed
17
- - **BREAKING** Update to latest DID Core context
18
- - **BREAKING** Update to use crypto-ld v5 API, latest crypto suites
19
- - Add support for X25519KeyAgreementKey suite
20
-
21
- ## 0.2.0 - 2020-08-01
22
-
23
- ### Changed
24
- - **BREAKING**: Update to use crypto-ld v4 API
25
-
26
- ## 0.0.1
27
-
28
- ### Added
29
- - 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,189 +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('publicMethodFor()', () => {
27
- it('should fetch a public key object for a given purpose', async () => {
28
- const didWeb = new DidWebResolver({ cryptoLd })
29
- const url = 'https://example.com'
30
- const { didDocument } = await didWeb.generate({ url })
31
-
32
- const keyAgreementKey = didWeb.publicMethodFor({
33
- didDocument, purpose: 'keyAgreement'
34
- })
35
-
36
- expect(keyAgreementKey.type).to.equal('X25519KeyAgreementKey2020')
37
- })
38
- })
39
-
40
- describe('generate()', () => {
41
- let didWeb
42
-
43
- beforeEach(async () => {
44
- didWeb = new DidWebResolver({ cryptoLd })
45
- })
46
-
47
- it('should generate using default key map', async () => {
48
- const url = 'https://example.com'
49
- const { didDocument, keyPairs } = await didWeb.generate({ url })
50
-
51
- expect(didDocument).to.have.property('@context')
52
- expect(didDocument.id).to.equal('did:web:example.com')
53
- expect(didDocument.capabilityInvocation[0].type)
54
- .to.equal('Ed25519VerificationKey2020')
55
- expect(didDocument.authentication[0].type)
56
- .to.equal('Ed25519VerificationKey2020')
57
- expect(didDocument.assertionMethod[0].type)
58
- .to.equal('Ed25519VerificationKey2020')
59
- expect(didDocument.capabilityDelegation[0].type)
60
- .to.equal('Ed25519VerificationKey2020')
61
-
62
- expect(keyPairs).to.exist()
63
- })
64
-
65
- it('should return methodFor convenience function', async () => {
66
- const url = 'https://example.com'
67
- const { methodFor } = await didWeb.generate({ url })
68
-
69
- const keyAgreementKey = methodFor({ purpose: 'keyAgreement' })
70
-
71
- expect(keyAgreementKey).to.have.property('type', 'X25519KeyAgreementKey2020')
72
- expect(keyAgreementKey).to.have.property('controller', 'did:web:example.com')
73
- expect(keyAgreementKey).to.have.property('publicKeyMultibase')
74
- expect(keyAgreementKey).to.have.property('privateKeyMultibase')
75
- })
76
- })
77
-
78
- describe('urlFromDid()', () => {
79
- it('should error on missing did', () => {
80
- let error
81
- try {
82
- urlFromDid()
83
- } catch (e) {
84
- error = e
85
- }
86
- expect(error.message).to.equal('Cannot convert did to url, missing did.')
87
- })
88
-
89
- it('should error on non-did:web dids', () => {
90
- let error
91
- try {
92
- urlFromDid({ did: 'did:example:1234' })
93
- } catch (e) {
94
- error = e
95
- }
96
- expect(error.message)
97
- .to.equal('DID Method not supported: "did:example:1234".')
98
- })
99
-
100
- it('should convert first id fragment to pathname plus default path', () => {
101
- expect(urlFromDid({ did: 'did:web:example.com' }))
102
- .to.equal('https://example.com/.well-known/did.json')
103
- })
104
-
105
- it('should url-decode host', () => {
106
- expect(urlFromDid({ did: 'did:web:localhost%3A8080' }))
107
- .to.equal('https://localhost:8080/.well-known/did.json')
108
- })
109
-
110
- it('should url-decode path fragments', () => {
111
- expect(urlFromDid({ did: 'did:web:example.com:path:some%2Bsubpath' }))
112
- .to.equal('https://example.com/path/some+subpath')
113
- })
114
-
115
- it('should preserve hash fragments for dids without paths', () => {
116
- const url = urlFromDid({ did: 'did:web:localhost%3A8080#keyId' })
117
- expect(url).to.equal('https://localhost:8080/.well-known/did.json#keyId')
118
- })
119
-
120
- it('should preserve hash fragments for dids with paths', () => {
121
- const url = urlFromDid({ did: 'did:web:example.com:path:some%2Bsubpath#keyId' })
122
- expect(url).to.equal('https://example.com/path/some+subpath#keyId')
123
- })
124
- })
125
-
126
- describe('didFromUrl', () => {
127
- it('should error on missing url', () => {
128
- let error
129
- try {
130
- didFromUrl()
131
- } catch (e) {
132
- error = e
133
- }
134
- expect(error.message).to.equal('Cannot convert url to did, missing url.')
135
- })
136
-
137
- it('should error on http URLs', () => {
138
- let error
139
- try {
140
- didFromUrl({ url: 'http://example.com' })
141
- } catch (e) {
142
- error = e
143
- }
144
- expect(error.message).to.equal('did:web does not support non-HTTPS URLs.')
145
- })
146
-
147
- it('should error on invalid URLs', () => {
148
- let error
149
- try {
150
- didFromUrl({ url: 'non-url' })
151
- } catch (e) {
152
- error = e
153
- }
154
- expect(error.message).to.equal('Invalid url: "non-url".')
155
- })
156
-
157
- it('should convert host to did identifier', () => {
158
- expect(didFromUrl({ url: 'https://localhost' }))
159
- .to.equal('did:web:localhost')
160
- expect(didFromUrl({ url: 'https://example.com' }))
161
- .to.equal('did:web:example.com')
162
- })
163
-
164
- it('should url-encode host', () => {
165
- expect(didFromUrl({ url: 'https://localhost:8080' }))
166
- .to.equal('did:web:localhost%3A8080')
167
- })
168
-
169
- it('should leave off the default / path', () => {
170
- expect(didFromUrl({ url: 'https://example.com/' }))
171
- .to.equal('did:web:example.com')
172
- })
173
-
174
- it('should encode path / separators as :', () => {
175
- expect(didFromUrl({ url: 'https://example.com/path/subpath/did.json' }))
176
- .to.equal('did:web:example.com:path:subpath:did.json')
177
- })
178
-
179
- it('should drop the default /.well-known/did.json pathname', () => {
180
- expect(didFromUrl({ url: 'https://example.com/.well-known/did.json' }))
181
- .to.equal('did:web:example.com')
182
- })
183
-
184
- it('should url-encode path fragments', () => {
185
- expect(didFromUrl({ url: 'https://example.com/path/some+subpath' }))
186
- .to.equal('did:web:example.com:path:some%2Bsubpath')
187
- })
188
- })
189
- })