diff --git a/README.md b/README.md index cb58880b..68b130f8 100644 --- a/README.md +++ b/README.md @@ -336,6 +336,7 @@ Get the public data in a space of a given address with the given name | name | String | A space name | | opts | Object | Optional parameters | | opts.profileServer | String | URL of Profile API server | +| opts.metadata | String | flag to retrieve metadata | @@ -431,6 +432,7 @@ Check if the given address is logged in * [new KeyValueStore()](#new_KeyValueStore_new) * [.log](#KeyValueStore+log) ⇒ Array.<Object> * [.get(key)](#KeyValueStore+get) ⇒ String + * [.getMetadata(key)](#KeyValueStore+getMetadata) ⇒ Metadata * [.set(key, value)](#KeyValueStore+set) ⇒ Boolean * [.remove(key)](#KeyValueStore+remove) ⇒ Boolean @@ -460,7 +462,19 @@ const log = store.log Get the value of the given key **Kind**: instance method of [KeyValueStore](#KeyValueStore) -**Returns**: String - the value associated with the key +**Returns**: String - the value associated with the key, undefined if there's no such key + +| Param | Type | Description | +| --- | --- | --- | +| key | String | the key | + + + +#### keyValueStore.getMetadata(key) ⇒ Metadata +Get metadata for for a given key + +**Kind**: instance method of [KeyValueStore](#KeyValueStore) +**Returns**: Metadata - Metadata for the key, undefined if there's no such key | Param | Type | Description | | --- | --- | --- | diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 0f5b83e1..7362005f 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -1,5 +1,10 @@ # Release Notes +## v1.7.0 - 2019-04-12 +* Feature: Add ability to get metadata for entries +* Feature: Add idUtils helper functions +* Feature: Send along DID when opening db with pinning node + ## v1.6.2 - 2019-04-09 * Fix: Use correct key when subscribing to thread in a space. diff --git a/package.json b/package.json index e7972717..83546b1a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "3box", - "version": "1.6.2", + "version": "1.7.0", "description": "Interact with user data", "main": "lib/3box.js", "directories": { @@ -8,7 +8,7 @@ }, "scripts": { "lint": "./node_modules/.bin/standard --verbose src/**", - "test": "rm -rf ./tmp ; jest --forceExit --detectOpenHandles --coverage --runInBand --testURL=\"http://localhost\"", + "test": "rm -rf ./tmp ; jest --forceExit --coverage --runInBand --testURL=\"http://localhost\"", "build:es5": "rm -rf ./lib; ./node_modules/.bin/babel src --out-dir lib --ignore=src/__tests__/,src/__mocks__/", "build:dist": "./node_modules/.bin/webpack --config webpack.config.js --mode=development", "build:dist:dev": "./node_modules/.bin/webpack --config webpack.dev.config.js --mode=development", diff --git a/src/3box.js b/src/3box.js index 6f89871f..b92b5e75 100644 --- a/src/3box.js +++ b/src/3box.js @@ -11,6 +11,7 @@ const PrivateStore = require('./privateStore') const Verified = require('./verified') const Space = require('./space') const utils = require('./utils/index') +const idUtils = require('./utils/id') const config = require('./config.js') const API = require('./api') @@ -92,7 +93,7 @@ class Box { const onNewPeer = async (topic, peer) => { if (peer === this.pinningNode.split('/').pop()) { - this._pubsub.publish(PINNING_ROOM, { type: 'PIN_DB', odbAddress: rootStoreAddress }) + this._pubsub.publish(PINNING_ROOM, { type: 'PIN_DB', odbAddress: rootStoreAddress, did: this._3id.getDid() }) } } @@ -168,11 +169,17 @@ class Box { * @return {Object} a json object with the profile for the given address */ static async getProfile (address, opts = {}) { + const metadata = opts.metadata opts = Object.assign({ useCacheService: true }, opts) + let profile if (opts.useCacheService) { - profile = await API.getProfile(address, opts.profileServer) + profile = await API.getProfile(address, opts.profileServer, { metadata }) } else { + if (metadata) { + throw new Error('getting metadata is not yet supported outside of the API') + } + const normalizedAddress = address.toLowerCase() profile = await this._getProfileOrbit(normalizedAddress, opts) } @@ -198,10 +205,11 @@ class Box { * @param {String} name A space name * @param {Object} opts Optional parameters * @param {String} opts.profileServer URL of Profile API server + * @param {String} opts.metadata flag to retrieve metadata * @return {Object} a json object with the public space data */ static async getSpace (address, name, opts = {}) { - return API.getSpace(address, name, opts.profileServer) + return API.getSpace(address, name, opts.profileServer, opts) } /** @@ -230,7 +238,7 @@ class Box { } static async _getProfileOrbit (address, opts = {}) { - if (utils.isMuportDID(address)) { + if (idUtils.isMuportDID(address)) { throw new Error('DID are supported in the cached version only') } @@ -489,4 +497,6 @@ async function initIPFS (ipfs, iframeStore, ipfsOptions) { } } +Box.idUtils = idUtils + module.exports = Box diff --git a/src/__tests__/3box.test.js b/src/__tests__/3box.test.js index 486f1bfd..1fa282e5 100644 --- a/src/__tests__/3box.test.js +++ b/src/__tests__/3box.test.js @@ -82,7 +82,6 @@ jest.mock('../utils/index', () => { let linkmap = {} let linkNum = 0 return { - isMuportDID: actualUtils.isMuportDID, getMessageConsent: actualUtils.getMessageConsent, openBoxConsent: jest.fn(async () => '0x8726348762348723487238476238746827364872634876234876234'), diff --git a/src/__tests__/idUtils.test.js b/src/__tests__/idUtils.test.js new file mode 100644 index 00000000..f4ecb115 --- /dev/null +++ b/src/__tests__/idUtils.test.js @@ -0,0 +1,26 @@ +const { isMuportDID, isClaim, verifyClaim } = require('../utils/id') + +const CLAIM_1 = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJpYXQiOjE1NTQ5NzI2MDksImV4cCI6MTk1NzQ2MzQyMSwibmFtZSI6InVQb3J0IERldmVsb3BlciIsImlzcyI6ImRpZDp1cG9ydDoyb3NuZko0V3k3TEJBbTJuUEJYaXJlMVdmUW43NVJyVjZUcyJ9.e9H1ngK7Kto_Am3N9NAJWm8kj7NetGPbOoQtKw8y-C21ytj1zjDr99w63AtlFCytYkLRcHnTHSl0eByaZww5dg' +const INVALID_CLAIM_FORMAT = '%eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJpYXQiOjE1NTQ5NzI2MDksImV4cCI6MTk1NzQ2MzQyMSwibmFtZSI6InVQb3J0IERldmVsb3BlciIsImlzcyI6ImRpZDp1cG9ydDoyb3NuZko0V3k3TEJBbTJuUEJYaXJlMVdmUW43NVJyVjZUcyJ9.e9H1ngK7Kto_Am3N9NAJWm8kj7NetGPbOoQtKw8y-C21ytj1zjDr99w63AtlFCytYkLRcHnTHSl0eByaZww5dg' +const EXPIRED_CLAIM = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJpYXQiOjE1NTQ5NzM5NDgsImV4cCI6MTk1LCJuYW1lIjoidVBvcnQgRGV2ZWxvcGVyIiwiaXNzIjoiZGlkOnVwb3J0OjJvc25mSjRXeTdMQkFtMm5QQlhpcmUxV2ZRbjc1UnJWNlRzIn0.M_HupDVb7N4TFOUg4B_PU6XQm9TTx7S0klhMLT1U3zfpThA4DAT2L8HGeBDTMuGS3-nXVo8oDYORASEX_ecGsQ' + +describe('basic utils tests', () => { + test('is muport did', () => { + expect(isMuportDID('abc')).toEqual(false) + expect(isMuportDID('did:example')).toEqual(false) + expect(isMuportDID('did:muport')).toEqual(false) + expect(isMuportDID('did:muport:Qmb9E8wLqjfAqfKhideoApU5g26Yz2Q2bSp6MSZmc5WrNr')).toEqual(true) + }) + + test('isClaim', async () => { + expect(await isClaim(CLAIM_1)).toEqual(true) + expect(await isClaim(INVALID_CLAIM_FORMAT)).toEqual(false) + expect(await isClaim(EXPIRED_CLAIM)).toEqual(true) // invalid claim will throw during verify + }) + + test('verifyClaim', async () => { + expect(verifyClaim(CLAIM_1)).resolves.toBeTruthy() + expect(verifyClaim(INVALID_CLAIM_FORMAT)).rejects.toBeTruthy() + expect(verifyClaim(EXPIRED_CLAIM)).rejects.toBeTruthy() + }, 100000) +}) \ No newline at end of file diff --git a/src/__tests__/keyValueStore.test.js b/src/__tests__/keyValueStore.test.js index 35cc2bd1..8574b08f 100644 --- a/src/__tests__/keyValueStore.test.js +++ b/src/__tests__/keyValueStore.test.js @@ -84,6 +84,24 @@ describe('KeyValueStore', () => { // await ipfs2.stop() }) + describe('metdata', () => { + it('should contain the metadata method', async () => { + await keyValueStore.set('some-key', 'some-value') + + const v = await keyValueStore.get('some-key') + const m = await keyValueStore.getMetadata('some-key') + + expect(v).toEqual('some-value') + expect(m).toBeDefined() + expect(m.timestamp).toBeDefined() + }) + + it('should return an undefined value for unknown key', async () => { + const m = await keyValueStore.getMetadata('a key so complex no one would set it') + expect(m).toBeUndefined() + }) + }) + describe('log', () => { let storeNum = 0 diff --git a/src/api.js b/src/api.js index 780a4829..ca3db779 100644 --- a/src/api.js +++ b/src/api.js @@ -1,6 +1,7 @@ const graphQLRequest = require('graphql-request').request const utils = require('./utils/index') const verifier = require('./utils/verifier') +const { isMuportDID } = require('./utils/id') const config = require('./config.js') const GRAPHQL_SERVER_URL = config.graphql_server_url @@ -16,7 +17,7 @@ async function getRootStoreAddress (identifier, serverUrl = ADDRESS_SERVER_URL) async function listSpaces (address, serverUrl = PROFILE_SERVER_URL) { try { // we await explicitly here to make sure the error is catch'd in the correct scope - if (utils.isMuportDID(address)) { + if (isMuportDID(address)) { return await utils.fetchJson(serverUrl + '/list-spaces?did=' + encodeURIComponent(address)) } else { return await utils.fetchJson(serverUrl + '/list-spaces?address=' + encodeURIComponent(address)) @@ -26,14 +27,28 @@ async function listSpaces (address, serverUrl = PROFILE_SERVER_URL) { } } -async function getSpace (address, name, serverUrl = PROFILE_SERVER_URL) { +async function getSpace (address, name, serverUrl = PROFILE_SERVER_URL, { metadata }) { + let url = `${serverUrl}/space` + try { - // we await explicitly here to make sure the error is catch'd in the correct scope - if (utils.isMuportDID(address)) { - return await utils.fetchJson(serverUrl + `/space?did=${encodeURIComponent(address)}&name=${encodeURIComponent(name)}`) + // Add first parameter: address or did + if (isMuportDID(address)) { + url = `${url}?did=${encodeURIComponent(address)}` } else { - return await utils.fetchJson(serverUrl + `/space?address=${encodeURIComponent(address)}&name=${encodeURIComponent(name)}`) + url = `${url}?address=${encodeURIComponent(address.toLowerCase())}` + } + + // Add name: + url = `${url}&name=${encodeURIComponent(name)}` + + // Add metadata: + if (metadata) { + url = `${url}&metadata=${encodeURIComponent(metadata)}` } + + // Query: + // we await explicitly to make sure the error is catch'd in the correct scope + return await utils.fetchJson(url) } catch (err) { return {} } @@ -50,16 +65,25 @@ async function getThread (space, name, serverUrl = PROFILE_SERVER_URL) { }) } -async function getProfile (address, serverUrl = PROFILE_SERVER_URL) { +async function getProfile (address, serverUrl = PROFILE_SERVER_URL, { metadata }) { + let url = `${serverUrl}/profile` + try { - // Note: we await explicitly to make sure the error is catch'd in the correct scope - if (utils.isMuportDID(address)) { - const normalized = encodeURIComponent(address) // uppercase is significant in did:muport - return await utils.fetchJson(serverUrl + '/profile?did=' + normalized) + // Add first parameter: address or did + if (isMuportDID(address)) { + url = `${url}?did=${encodeURIComponent(address)}` } else { - const normalized = encodeURIComponent(address.toLowerCase()) - return await utils.fetchJson(serverUrl + '/profile?address=' + normalized) + url = `${url}?address=${encodeURIComponent(address.toLowerCase())}` } + + // Add metadata: + if (metadata) { + url = `${url}&metadata=${encodeURIComponent(metadata)}` + } + + // Query: + // we await explicitly to make sure the error is catch'd in the correct scope + return await utils.fetchJson(url) } catch (err) { return {} // empty profile } @@ -71,7 +95,7 @@ async function getProfiles (addressArray, opts = {}) { // Split addresses on ethereum / dids addressArray.forEach(address => { - if (utils.isMuportDID(address)) { + if (isMuportDID(address)) { req.didList.push(address) } else { req.addressList.push(address) diff --git a/src/keyValueStore.js b/src/keyValueStore.js index 4720a3f1..2acf5034 100644 --- a/src/keyValueStore.js +++ b/src/keyValueStore.js @@ -13,12 +13,22 @@ class KeyValueStore { * Get the value of the given key * * @param {String} key the key - * @return {String} the value associated with the key + * @return {String} the value associated with the key, undefined if there's no such key */ async get (key) { - this._requireLoad() - const dbGetRes = await this._db.get(key) - return dbGetRes ? dbGetRes.value : dbGetRes + const x = await this._get(key) + return x ? x.value : x + } + + /** + * Get metadata for for a given key + * + * @param {String} key the key + * @return {Metadata} Metadata for the key, undefined if there's no such key + */ + async getMetadata (key) { + const x = await this._get(key) + return x ? { timestamp: x.timeStamp } : x } /** @@ -49,6 +59,18 @@ class KeyValueStore { return true } + /** + * Get the raw value of the given key + * @private + * + * @param {String} key the key + * @return {String} the value associated with the key + */ + async _get (key) { + this._requireLoad() + return this._db.get(key) + } + async _sync (numRemoteEntries) { this._requireLoad() // let toid = null diff --git a/src/privateStore.js b/src/privateStore.js index ad346525..f4d72fb9 100644 --- a/src/privateStore.js +++ b/src/privateStore.js @@ -15,6 +15,11 @@ class PrivateStore extends KeyValueStore { return encryptedEntry ? this._decryptEntry(encryptedEntry) : null } + async getMetadata (key) { + // Note: assumes metadata is not encrypted. + return super.getMetadata(this._genDbKey(key)) + } + async set (key, value) { value = this._encryptEntry(value) key = this._genDbKey(key) diff --git a/src/space.js b/src/space.js index 1a97ff97..43bde22d 100644 --- a/src/space.js +++ b/src/space.js @@ -117,6 +117,7 @@ const publicStoreReducer = (store) => { const PREFIX = 'pub_' return { get: async key => store.get(PREFIX + key), + getMetadata: async key => store.getMetadata(PREFIX + key), set: async (key, value) => store.set(PREFIX + key, value), remove: async key => store.remove(PREFIX + key), get log () { @@ -158,6 +159,7 @@ const privateStoreReducer = (store, keyring) => { const entry = await store.get(dbKey(key)) return entry ? decryptEntry(entry).value : null }, + getMetadata: async key => store.getMetadata(dbKey(key)), set: async (key, value) => store.set(dbKey(key), encryptEntry({ key, value })), remove: async key => store.remove(dbKey(key)), get log () { diff --git a/src/utils/id.js b/src/utils/id.js new file mode 100644 index 00000000..d1684385 --- /dev/null +++ b/src/utils/id.js @@ -0,0 +1,15 @@ +const didJWT = require('did-jwt') +const DID_MUPORT_PREFIX = 'did:muport:' + +module.exports = { + isMuportDID: (address) => address.startsWith(DID_MUPORT_PREFIX), + isClaim: async (claim, opts = {}) => { + try { + await didJWT.decodeJWT(claim, opts) + return true + } catch (e) { + return false + } + }, + verifyClaim: didJWT.verifyJWT +} \ No newline at end of file diff --git a/src/utils/index.js b/src/utils/index.js index 8faccbb2..21b977b8 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -2,8 +2,6 @@ const fetch = typeof window !== 'undefined' ? window.fetch : require('node-fetch const Multihash = require('multihashes') const sha256 = require('js-sha256').sha256 -const DID_MUPORT_PREFIX = 'did:muport:' - const HTTPError = (status, message) => { const e = new Error(message) e.statusCode = status @@ -16,7 +14,6 @@ const getMessageConsent = (did) => ( module.exports = { getMessageConsent, - isMuportDID: (address) => address.startsWith(DID_MUPORT_PREFIX), openBoxConsent: (fromAddress, ethereum) => { const text = 'This app wants to view and update your 3Box profile.'