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.'