From 2aeaf7f90b24079c9d203a9ba07c1f17d967939b Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" <23040076+greenkeeper[bot]@users.noreply.github.com> Date: Mon, 2 Dec 2019 19:57:50 +0000 Subject: [PATCH 01/10] chore(package): update identity-wallet to version 1.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 47c9518b..d1d821f7 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,7 @@ "babel-core": "7.0.0-bridge.0", "babel-loader": "^8.0.6", "express": "^4.17.0", - "identity-wallet": "^1.0.0-beta.2", + "identity-wallet": "^1.0.0", "jest": "^23.6.0", "jsdoc-to-markdown": "^5.0.0", "standard": "^14.3.1", From 424f76bac73d3726fe0444009764a79c18bd1e1d Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" <23040076+greenkeeper[bot]@users.noreply.github.com> Date: Mon, 2 Dec 2019 19:57:57 +0000 Subject: [PATCH 02/10] chore(package): update lockfile package-lock.json --- package-lock.json | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4bc250b1..4c3f3dc8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8361,12 +8361,12 @@ } }, "identity-wallet": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/identity-wallet/-/identity-wallet-1.0.0-beta.2.tgz", - "integrity": "sha512-kThy7ittEQ+JmgLPDfcPskUi7tvG0WzCXZILsAYt8oseL+QDhvKYm+RPYhb47UFwKuUgOj2NqE19ChI+SEjl7Q==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/identity-wallet/-/identity-wallet-1.0.0.tgz", + "integrity": "sha512-Vmf9oGFhVynhPaVJhlbPYIpJjtREoEMn6iln9Totq5+Ee5PtckSEyhDles0XlCQMShwCdoU4Xwq/rypUvVQh3Q==", "dev": true, "requires": { - "3id-blockchain-utils": "^0.2.1", + "3id-blockchain-utils": "^0.3.2", "@babel/runtime": "^7.4.5", "@ethersproject/hdnode": "5.0.0-beta.133", "@ethersproject/wallet": "5.0.0-beta.133", @@ -8381,17 +8381,6 @@ "tweetnacl-util": "^0.15.0" }, "dependencies": { - "3id-blockchain-utils": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/3id-blockchain-utils/-/3id-blockchain-utils-0.2.1.tgz", - "integrity": "sha512-acpRvBIH4KstIem1robPctlrQ9Kp09hFN5RWdXqxvIovncOBtV24BNYe6OcJErbhWxiHh3ykmJ9S5x2LThe9jg==", - "dev": true, - "requires": { - "@ethersproject/contracts": "^5.0.0-beta.140", - "@ethersproject/providers": "^5.0.0-beta.144", - "@ethersproject/wallet": "^5.0.0-beta.133" - } - }, "did-jwt": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/did-jwt/-/did-jwt-0.1.3.tgz", From 5a3bc031a299b3c0e3cd01232807c95b77bfd870 Mon Sep 17 00:00:00 2001 From: Joel Torstensson Date: Tue, 3 Dec 2019 14:08:46 +0100 Subject: [PATCH 03/10] ref(api): move static api functions to separate class --- README.md | 175 +++++++++++++------------- src/3box.js | 147 ++-------------------- src/api.js | 352 ++++++++++++++++++++++++++++++++-------------------- 3 files changed, 316 insertions(+), 358 deletions(-) diff --git a/README.md b/README.md index 25ca230a..5f72f4fd 100644 --- a/README.md +++ b/README.md @@ -284,10 +284,11 @@ idUtils.verifyClaim(claim) ## API Documentation -### Box +### Box ⇐ [BoxApi](#BoxApi) **Kind**: global class +**Extends**: [BoxApi](#BoxApi) -* [Box](#Box) +* [Box](#Box) ⇐ [BoxApi](#BoxApi) * [new Box()](#new_Box_new) * _instance_ * [.public](#Box+public) @@ -308,15 +309,6 @@ idUtils.verifyClaim(claim) * [.verifyClaim](#Box.idUtils.verifyClaim) ⇒ Object * [.isSupportedDID(did)](#Box.idUtils.isSupportedDID) ⇒ \* \| boolean * [.isClaim(claim, opts)](#Box.idUtils.isClaim) ⇒ Promise.<boolean> - * [.getProfile(address, opts)](#Box.getProfile) ⇒ Object - * [.getProfiles(address, opts)](#Box.getProfiles) ⇒ Object - * [.getSpace(address, name, opts)](#Box.getSpace) ⇒ Object - * [.getThread(space, name, firstModerator, members, opts)](#Box.getThread) ⇒ Array.<Object> - * [.getThreadByAddress(address, opts)](#Box.getThreadByAddress) ⇒ Array.<Object> - * [.getConfig(address, opts)](#Box.getConfig) ⇒ Array.<Object> - * [.listSpaces(address, opts)](#Box.listSpaces) ⇒ Object - * [.profileGraphQL(query, opts)](#Box.profileGraphQL) ⇒ Object - * [.getVerifiedAccounts(profile)](#Box.getVerifiedAccounts) ⇒ Object * [.openBox(address, provider, opts)](#Box.openBox) ⇒ [Box](#Box) * [.isLoggedIn(address)](#Box.isLoggedIn) ⇒ Boolean * [.getIPFS()](#Box.getIPFS) ⇒ IPFS @@ -517,45 +509,80 @@ Check whether a string is a valid claim or not | opts | Object | Optional parameters | | opts.audience | string | The DID of the audience of the JWT | - + -#### Box.getProfile(address, opts) ⇒ Object -Get the public profile of a given address +#### Box.openBox(address, provider, opts) ⇒ [Box](#Box) +Opens the 3Box associated with the given address **Kind**: static method of [Box](#Box) -**Returns**: Object - a json object with the profile for the given address +**Returns**: [Box](#Box) - the 3Box instance for the given address | Param | Type | Description | | --- | --- | --- | | address | String | An ethereum address | +| provider | provider | An ethereum or 3ID provider | | opts | Object | Optional parameters | -| opts.blocklist | function | A function that takes an address and returns true if the user has been blocked | -| opts.metadata | String | flag to retrieve metadata | -| opts.addressServer | String | URL of the Address Server | +| opts.consentCallback | function | A function that will be called when the user has consented to opening the box | +| opts.pinningNode | String | A string with an ipfs multi-address to a 3box pinning node | | opts.ipfs | Object | A js-ipfs ipfs object | -| opts.useCacheService | Boolean | Use 3Box API and Cache Service to fetch profile instead of OrbitDB. Default true. | -| opts.profileServer | String | URL of Profile API server | +| opts.addressServer | String | URL of the Address Server | +| opts.contentSignature | String | A signature, provided by a client of 3box using the private keys associated with the given address, of the 3box consent message | - + -#### Box.getProfiles(address, opts) ⇒ Object -Get a list of public profiles for given addresses. This relies on 3Box profile API. +#### Box.isLoggedIn(address) ⇒ Boolean +Check if the given address is logged in **Kind**: static method of [Box](#Box) -**Returns**: Object - a json object with each key an address and value the profile +**Returns**: Boolean - true if the user is logged in | Param | Type | Description | | --- | --- | --- | -| address | Array | An array of ethereum addresses | +| address | String | An ethereum address | + + + +#### Box.getIPFS() ⇒ IPFS +Instanciate ipfs used by 3Box without calling openBox. + +**Kind**: static method of [Box](#Box) +**Returns**: IPFS - the ipfs instance + + +### BoxApi +**Kind**: global class + +* [BoxApi](#BoxApi) + * [.listSpaces(address, opts)](#BoxApi.listSpaces) ⇒ Object + * [.getSpace(address, name, opts)](#BoxApi.getSpace) ⇒ Object + * [.getThread(space, name, firstModerator, members, opts)](#BoxApi.getThread) ⇒ Array.<Object> + * [.getThreadByAddress(address, opts)](#BoxApi.getThreadByAddress) ⇒ Array.<Object> + * [.getConfig(address, opts)](#BoxApi.getConfig) ⇒ Array.<Object> + * [.getProfile(address, opts)](#BoxApi.getProfile) ⇒ Object + * [.getProfiles(address, opts)](#BoxApi.getProfiles) ⇒ Object + * [.profileGraphQL(query, opts)](#BoxApi.profileGraphQL) ⇒ Object + * [.getVerifiedAccounts(profile)](#BoxApi.getVerifiedAccounts) ⇒ Object + + + +#### BoxApi.listSpaces(address, opts) ⇒ Object +Get the names of all spaces a user has + +**Kind**: static method of [BoxApi](#BoxApi) +**Returns**: Object - an array with all spaces as strings + +| Param | Type | Description | +| --- | --- | --- | +| address | String | An ethereum address | | opts | Object | Optional parameters | | opts.profileServer | String | URL of Profile API server | - + -#### Box.getSpace(address, name, opts) ⇒ Object +#### BoxApi.getSpace(address, name, opts) ⇒ Object Get the public data in a space of a given address with the given name -**Kind**: static method of [Box](#Box) +**Kind**: static method of [BoxApi](#BoxApi) **Returns**: Object - a json object with the public space data | Param | Type | Description | @@ -567,12 +594,12 @@ Get the public data in a space of a given address with the given name | opts.metadata | String | flag to retrieve metadata | | opts.profileServer | String | URL of Profile API server | - + -#### Box.getThread(space, name, firstModerator, members, opts) ⇒ Array.<Object> +#### BoxApi.getThread(space, name, firstModerator, members, opts) ⇒ Array.<Object> Get all posts that are made to a thread. -**Kind**: static method of [Box](#Box) +**Kind**: static method of [BoxApi](#BoxApi) **Returns**: Array.<Object> - An array of posts | Param | Type | Description | @@ -584,12 +611,12 @@ Get all posts that are made to a thread. | opts | Object | Optional parameters | | opts.profileServer | String | URL of Profile API server | - + -#### Box.getThreadByAddress(address, opts) ⇒ Array.<Object> +#### BoxApi.getThreadByAddress(address, opts) ⇒ Array.<Object> Get all posts that are made to a thread. -**Kind**: static method of [Box](#Box) +**Kind**: static method of [BoxApi](#BoxApi) **Returns**: Array.<Object> - An array of posts | Param | Type | Description | @@ -598,12 +625,12 @@ Get all posts that are made to a thread. | opts | Object | Optional parameters | | opts.profileServer | String | URL of Profile API server | - + -#### Box.getConfig(address, opts) ⇒ Array.<Object> +#### BoxApi.getConfig(address, opts) ⇒ Array.<Object> Get the configuration of a users 3Box -**Kind**: static method of [Box](#Box) +**Kind**: static method of [BoxApi](#BoxApi) **Returns**: Array.<Object> - An array of posts | Param | Type | Description | @@ -612,84 +639,62 @@ Get the configuration of a users 3Box | opts | Object | Optional parameters | | opts.profileServer | String | URL of Profile API server | - + -#### Box.listSpaces(address, opts) ⇒ Object -Get the names of all spaces a user has +#### BoxApi.getProfile(address, opts) ⇒ Object +Get the public profile of a given address -**Kind**: static method of [Box](#Box) -**Returns**: Object - an array with all spaces as strings +**Kind**: static method of [BoxApi](#BoxApi) +**Returns**: Object - a json object with the profile for the given address | Param | Type | Description | | --- | --- | --- | | address | String | An ethereum address | | opts | Object | Optional parameters | +| opts.blocklist | function | A function that takes an address and returns true if the user has been blocked | +| opts.metadata | String | flag to retrieve metadata | | opts.profileServer | String | URL of Profile API server | - + -#### Box.profileGraphQL(query, opts) ⇒ Object -GraphQL for 3Box profile API +#### BoxApi.getProfiles(address, opts) ⇒ Object +Get a list of public profiles for given addresses. This relies on 3Box profile API. -**Kind**: static method of [Box](#Box) +**Kind**: static method of [BoxApi](#BoxApi) **Returns**: Object - a json object with each key an address and value the profile | Param | Type | Description | | --- | --- | --- | -| query | Object | A graphQL query object. | +| address | Array | An array of ethereum addresses | | opts | Object | Optional parameters | -| opts.graphqlServer | String | URL of graphQL 3Box profile service | - - - -#### Box.getVerifiedAccounts(profile) ⇒ Object -Verifies the proofs of social accounts that is present in the profile. - -**Kind**: static method of [Box](#Box) -**Returns**: Object - An object containing the accounts that have been verified - -| Param | Type | Description | -| --- | --- | --- | -| profile | Object | A user profile object, received from the `getProfile` function | +| opts.profileServer | String | URL of Profile API server | - + -#### Box.openBox(address, provider, opts) ⇒ [Box](#Box) -Opens the 3Box associated with the given address +#### BoxApi.profileGraphQL(query, opts) ⇒ Object +GraphQL for 3Box profile API -**Kind**: static method of [Box](#Box) -**Returns**: [Box](#Box) - the 3Box instance for the given address +**Kind**: static method of [BoxApi](#BoxApi) +**Returns**: Object - a json object with each key an address and value the profile | Param | Type | Description | | --- | --- | --- | -| address | String | An ethereum address | -| provider | provider | An ethereum or 3ID provider | +| query | Object | A graphQL query object. | | opts | Object | Optional parameters | -| opts.consentCallback | function | A function that will be called when the user has consented to opening the box | -| opts.pinningNode | String | A string with an ipfs multi-address to a 3box pinning node | -| opts.ipfs | Object | A js-ipfs ipfs object | -| opts.addressServer | String | URL of the Address Server | -| opts.contentSignature | String | A signature, provided by a client of 3box using the private keys associated with the given address, of the 3box consent message | +| opts.graphqlServer | String | URL of graphQL 3Box profile service | - + -#### Box.isLoggedIn(address) ⇒ Boolean -Check if the given address is logged in +#### BoxApi.getVerifiedAccounts(profile) ⇒ Object +Verifies the proofs of social accounts that is present in the profile. -**Kind**: static method of [Box](#Box) -**Returns**: Boolean - true if the user is logged in +**Kind**: static method of [BoxApi](#BoxApi) +**Returns**: Object - An object containing the accounts that have been verified | Param | Type | Description | | --- | --- | --- | -| address | String | An ethereum address | - - - -#### Box.getIPFS() ⇒ IPFS -Instanciate ipfs used by 3Box without calling openBox. +| profile | Object | A user profile object, received from the `getProfile` function | -**Kind**: static method of [Box](#Box) -**Returns**: IPFS - the ipfs instance ### KeyValueStore diff --git a/src/3box.js b/src/3box.js index b607dd17..79b477b5 100644 --- a/src/3box.js +++ b/src/3box.js @@ -12,7 +12,7 @@ const Space = require('./space') const utils = require('./utils/index') const idUtils = require('./utils/id') const config = require('./config.js') -const API = require('./api') +const BoxApi = require('./api') const IPFSRepo = require('ipfs-repo') const LevelStore = require('datastore-level') @@ -39,11 +39,16 @@ if (typeof window !== 'undefined' && typeof document !== 'undefined') { cacheProxy = OrbitDBCacheProxy({ postMessage }) } */ -class Box { +/** + * @extends BoxApi + */ +class Box extends BoxApi { /** * Please use the **openBox** method to instantiate a 3Box + * @constructor */ constructor (threeId, provider, ipfs, opts = {}) { + super() this._3id = threeId this._web3provider = provider this._ipfs = ipfs @@ -107,144 +112,6 @@ class Box { await this.private._load() } - /** - * Get the public profile of a given address - * - * @param {String} address An ethereum address - * @param {Object} opts Optional parameters - * @param {Function} opts.blocklist A function that takes an address and returns true if the user has been blocked - * @param {String} opts.metadata flag to retrieve metadata - * @param {String} opts.addressServer URL of the Address Server - * @param {Object} opts.ipfs A js-ipfs ipfs object - * @param {Boolean} opts.useCacheService Use 3Box API and Cache Service to fetch profile instead of OrbitDB. Default true. - * @param {String} opts.profileServer URL of Profile API server - * @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, { 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) - } - return profile - } - - /** - * Get a list of public profiles for given addresses. This relies on 3Box profile API. - * - * @param {Array} address An array of ethereum addresses - * @param {Object} opts Optional parameters - * @param {String} opts.profileServer URL of Profile API server - * @return {Object} a json object with each key an address and value the profile - */ - static async getProfiles (addressArray, opts = {}) { - return API.getProfiles(addressArray, opts) - } - - /** - * Get the public data in a space of a given address with the given name - * - * @param {String} address An ethereum address - * @param {String} name A space name - * @param {Object} opts Optional parameters - * @param {Function} opts.blocklist A function that takes an address and returns true if the user has been blocked - * @param {String} opts.metadata flag to retrieve metadata - * @param {String} opts.profileServer URL of Profile API server - * @return {Object} a json object with the public space data - */ - static async getSpace (address, name, opts = {}) { - return API.getSpace(address, name, opts.profileServer, opts) - } - - /** - * Get all posts that are made to a thread. - * - * @param {String} space The name of the space the thread is in - * @param {String} name The name of the thread - * @param {String} firstModerator The DID (or ethereum address) of the first moderator - * @param {Boolean} members True if only members are allowed to post - * @param {Object} opts Optional parameters - * @param {String} opts.profileServer URL of Profile API server - * @return {Array} An array of posts - */ - static async getThread (space, name, firstModerator, members, opts = {}) { - return API.getThread(space, name, firstModerator, members, opts) - } - - /** - * Get all posts that are made to a thread. - * - * @param {String} address The orbitdb-address of the thread - * @param {Object} opts Optional parameters - * @param {String} opts.profileServer URL of Profile API server - * @return {Array} An array of posts - */ - static async getThreadByAddress (address, opts = {}) { - return API.getThreadByAddress(address, opts) - } - - /** - * Get the configuration of a users 3Box - * - * @param {String} address The ethereum address - * @param {Object} opts Optional parameters - * @param {String} opts.profileServer URL of Profile API server - * @return {Array} An array of posts - */ - static async getConfig (address, opts = {}) { - return API.getConfig(address, opts) - } - - /** - * Get the names of all spaces a user has - * - * @param {String} address An ethereum address - * @param {Object} opts Optional parameters - * @param {String} opts.profileServer URL of Profile API server - * @return {Object} an array with all spaces as strings - */ - static async listSpaces (address, opts = {}) { - return API.listSpaces(address, opts.profileServer) - } - - static async _getProfileOrbit (address, opts = {}) { - // Removed this code since it's completely outdated. - // TODO - implement using the replicator module - throw new Error('Not implemented yet') - } - - /** - * GraphQL for 3Box profile API - * - * @param {Object} query A graphQL query object. - * @param {Object} opts Optional parameters - * @param {String} opts.graphqlServer URL of graphQL 3Box profile service - * @return {Object} a json object with each key an address and value the profile - */ - - static async profileGraphQL (query, opts = {}) { - return API.profileGraphQL(query, opts.graphqlServer) - } - - /** - * Verifies the proofs of social accounts that is present in the profile. - * - * @param {Object} profile A user profile object, received from the `getProfile` function - * @return {Object} An object containing the accounts that have been verified - */ - static async getVerifiedAccounts (profile) { - return API.getVerifiedAccounts(profile) - } - /** * Opens the 3Box associated with the given address * diff --git a/src/api.js b/src/api.js index 812293a0..8fda704b 100644 --- a/src/api.js +++ b/src/api.js @@ -8,171 +8,257 @@ const GRAPHQL_SERVER_URL = config.graphql_server_url const PROFILE_SERVER_URL = config.profile_server_url const ADDRESS_SERVER_URL = config.address_server_url -async function getRootStoreAddress (identifier, serverUrl = ADDRESS_SERVER_URL) { - // read orbitdb root store address from the 3box-address-server - const res = await utils.fetchJson(serverUrl + '/odbAddress/' + identifier) - return res.data.rootStoreAddress -} +/** + * @class + */ +class BoxApi { + static async getRootStoreAddress (identifier, serverUrl = ADDRESS_SERVER_URL) { + // read orbitdb root store address from the 3box-address-server + const res = await utils.fetchJson(serverUrl + '/odbAddress/' + identifier) + return res.data.rootStoreAddress + } -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 (isSupportedDID(address)) { - return await utils.fetchJson(serverUrl + '/list-spaces?did=' + address) - } else { - return await utils.fetchJson(serverUrl + '/list-spaces?address=' + encodeURIComponent(address)) + /** + * Get the names of all spaces a user has + * + * @param {String} address An ethereum address + * @param {Object} opts Optional parameters + * @param {String} opts.profileServer URL of Profile API server + * @return {Object} an array with all spaces as strings + */ + static async listSpaces (address, { profileServer } = {}) { + const serverUrl = profileServer || PROFILE_SERVER_URL + try { + // we await explicitly here to make sure the error is catch'd in the correct scope + if (isSupportedDID(address)) { + return await utils.fetchJson(serverUrl + '/list-spaces?did=' + address) + } else { + return await utils.fetchJson(serverUrl + '/list-spaces?address=' + encodeURIComponent(address)) + } + } catch (err) { + return [] } - } catch (err) { - return [] } -} -async function getSpace (address, name, serverUrl = PROFILE_SERVER_URL, { metadata, blocklist } = {}) { - if (blocklist && blocklist(address)) throw new Error(`user with ${address} is blocked`) - let url = `${serverUrl}/space` + /** + * Get the public data in a space of a given address with the given name + * + * @param {String} address An ethereum address + * @param {String} name A space name + * @param {Object} opts Optional parameters + * @param {Function} opts.blocklist A function that takes an address and returns true if the user has been blocked + * @param {String} opts.metadata flag to retrieve metadata + * @param {String} opts.profileServer URL of Profile API server + * @return {Object} a json object with the public space data + */ + static async getSpace (address, name, { profileServer, metadata, blocklist } = {}) { + if (blocklist && blocklist(address)) throw new Error(`user with ${address} is blocked`) + const serverUrl = profileServer || PROFILE_SERVER_URL + let url = `${serverUrl}/space` - try { - // Add first parameter: address or did - if (isSupportedDID(address)) { - url = `${url}?did=${address}` - } else { - url = `${url}?address=${encodeURIComponent(address.toLowerCase())}` - } + try { + // Add first parameter: address or did + if (isSupportedDID(address)) { + url = `${url}?did=${address}` + } else { + url = `${url}?address=${encodeURIComponent(address.toLowerCase())}` + } - // Add name: - url = `${url}&name=${encodeURIComponent(name)}` + // Add name: + url = `${url}&name=${encodeURIComponent(name)}` - // Add metadata: - if (metadata) { - url = `${url}&metadata=${encodeURIComponent(metadata)}` - } + // 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 {} + // Query: + // we await explicitly to make sure the error is catch'd in the correct scope + return await utils.fetchJson(url) + } catch (err) { + return {} + } } -} - -// TODO consumes address now, could also give root DID to get space DID -async function getSpaceDID (address, space, opts = {}) { - const conf = await getConfig(address, opts) - if (!conf.spaces[space] || !conf.spaces[space].DID) throw new Error(`Could not find appropriate DID for address ${address}`) - return conf.spaces[space].DID -} -async function getThread (space, name, firstModerator, members, opts = {}) { - const serverUrl = opts.profileServer || PROFILE_SERVER_URL - if (firstModerator.startsWith('0x')) { - firstModerator = await getSpaceDID(firstModerator, space, opts) + // TODO consumes address now, could also give root DID to get space DID + static async getSpaceDID (address, space, opts = {}) { + const conf = await BoxApi.getConfig(address, opts) + if (!conf.spaces[space] || !conf.spaces[space].DID) throw new Error(`Could not find appropriate DID for address ${address}`) + return conf.spaces[space].DID } - try { - let url = `${serverUrl}/thread?space=${encodeURIComponent(space)}&name=${encodeURIComponent(name)}` - url += `&mod=${encodeURIComponent(firstModerator)}&members=${encodeURIComponent(members)}` - return await utils.fetchJson(url) - } catch (err) { - throw new Error(err) + + /** + * Get all posts that are made to a thread. + * + * @param {String} space The name of the space the thread is in + * @param {String} name The name of the thread + * @param {String} firstModerator The DID (or ethereum address) of the first moderator + * @param {Boolean} members True if only members are allowed to post + * @param {Object} opts Optional parameters + * @param {String} opts.profileServer URL of Profile API server + * @return {Array} An array of posts + */ + static async getThread (space, name, firstModerator, members, opts = {}) { + const serverUrl = opts.profileServer || PROFILE_SERVER_URL + if (firstModerator.startsWith('0x')) { + firstModerator = await BoxApi.getSpaceDID(firstModerator, space, opts) + } + try { + let url = `${serverUrl}/thread?space=${encodeURIComponent(space)}&name=${encodeURIComponent(name)}` + url += `&mod=${encodeURIComponent(firstModerator)}&members=${encodeURIComponent(members)}` + return await utils.fetchJson(url) + } catch (err) { + throw new Error(err) + } } -} -async function getThreadByAddress (address, opts = {}) { - const serverUrl = opts.profileServer || PROFILE_SERVER_URL - try { - return await utils.fetchJson(`${serverUrl}/thread?address=${encodeURIComponent(address)}`) - } catch (err) { - throw new Error(err) + /** + * Get all posts that are made to a thread. + * + * @param {String} address The orbitdb-address of the thread + * @param {Object} opts Optional parameters + * @param {String} opts.profileServer URL of Profile API server + * @return {Array} An array of posts + */ + static async getThreadByAddress (address, opts = {}) { + const serverUrl = opts.profileServer || PROFILE_SERVER_URL + try { + return await utils.fetchJson(`${serverUrl}/thread?address=${encodeURIComponent(address)}`) + } catch (err) { + throw new Error(err) + } } -} -async function getConfig (address, opts = {}) { - const serverUrl = opts.profileServer || PROFILE_SERVER_URL - try { - return await utils.fetchJson(`${serverUrl}/config?address=${encodeURIComponent(address)}`) - } catch (err) { - throw new Error(err) + /** + * Get the configuration of a users 3Box + * + * @param {String} address The ethereum address + * @param {Object} opts Optional parameters + * @param {String} opts.profileServer URL of Profile API server + * @return {Array} An array of posts + */ + static async getConfig (address, opts = {}) { + const serverUrl = opts.profileServer || PROFILE_SERVER_URL + try { + return await utils.fetchJson(`${serverUrl}/config?address=${encodeURIComponent(address)}`) + } catch (err) { + throw new Error(err) + } } -} -async function getProfile (address, serverUrl = PROFILE_SERVER_URL, { metadata, blocklist } = {}) { - if (blocklist && blocklist(address)) throw new Error(`user with ${address} is blocked`) - let url = `${serverUrl}/profile` + /** + * Get the public profile of a given address + * + * @param {String} address An ethereum address + * @param {Object} opts Optional parameters + * @param {Function} opts.blocklist A function that takes an address and returns true if the user has been blocked + * @param {String} opts.metadata flag to retrieve metadata + * @param {String} opts.profileServer URL of Profile API server + * @return {Object} a json object with the profile for the given address + */ + static async getProfile (address, { profileServer, metadata, blocklist } = {}) { + if (blocklist && blocklist(address)) throw new Error(`user with ${address} is blocked`) + const serverUrl = profileServer || PROFILE_SERVER_URL + let url = `${serverUrl}/profile` - try { - // Add first parameter: address or did - if (isSupportedDID(address)) { - url = `${url}?did=${address}` - } else { - url = `${url}?address=${encodeURIComponent(address.toLowerCase())}` - } + try { + // Add first parameter: address or did + if (isSupportedDID(address)) { + url = `${url}?did=${address}` + } else { + url = `${url}?address=${encodeURIComponent(address.toLowerCase())}` + } - // Add metadata: - if (metadata) { - url = `${url}&metadata=${encodeURIComponent(metadata)}` - } + // 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 + // 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 + } } -} -async function getProfiles (addressArray, opts = {}) { - opts = Object.assign({ profileServer: PROFILE_SERVER_URL }, opts) - const req = { addressList: [], didList: [] } + /** + * Get a list of public profiles for given addresses. This relies on 3Box profile API. + * + * @param {Array} address An array of ethereum addresses + * @param {Object} opts Optional parameters + * @param {String} opts.profileServer URL of Profile API server + * @return {Object} a json object with each key an address and value the profile + */ + static async getProfiles (addressArray, opts = {}) { + opts = Object.assign({ profileServer: PROFILE_SERVER_URL }, opts) + const req = { addressList: [], didList: [] } - // Split addresses on ethereum / dids - addressArray.forEach(address => { - if (isSupportedDID(address)) { - req.didList.push(address) - } else { - req.addressList.push(address) - } - }) + // Split addresses on ethereum / dids + addressArray.forEach(address => { + if (isSupportedDID(address)) { + req.didList.push(address) + } else { + req.addressList.push(address) + } + }) - const url = `${opts.profileServer}/profileList` - return utils.fetchJson(url, req) -} + const url = `${opts.profileServer}/profileList` + return utils.fetchJson(url, req) + } -async function profileGraphQL (query, opts = {}) { - opts = Object.assign({ graphqlServer: GRAPHQL_SERVER_URL }, opts) - return graphQLRequest(opts.graphqlServer, query) -} + /** + * GraphQL for 3Box profile API + * + * @param {Object} query A graphQL query object. + * @param {Object} opts Optional parameters + * @param {String} opts.graphqlServer URL of graphQL 3Box profile service + * @return {Object} a json object with each key an address and value the profile + */ + static async profileGraphQL (query, opts = {}) { + opts = Object.assign({ graphqlServer: GRAPHQL_SERVER_URL }, opts) + return graphQLRequest(opts.graphqlServer, query) + } -async function getVerifiedAccounts (profile) { - const verifs = {} - try { - const did = await verifier.verifyDID(profile.proof_did) + /** + * Verifies the proofs of social accounts that is present in the profile. + * + * @param {Object} profile A user profile object, received from the `getProfile` function + * @return {Object} An object containing the accounts that have been verified + */ + static async getVerifiedAccounts (profile) { + const verifs = {} + try { + const did = await verifier.verifyDID(profile.proof_did) - verifs.did = did + verifs.did = did - if (profile.proof_github) { - try { - verifs.github = await verifier.verifyGithub(did, profile.proof_github) - } catch (err) { - // Invalid github verification + if (profile.proof_github) { + try { + verifs.github = await verifier.verifyGithub(did, profile.proof_github) + } catch (err) { + // Invalid github verification + } } - } - if (profile.proof_twitter) { - try { - verifs.twitter = await verifier.verifyTwitter(did, profile.proof_twitter) - } catch (err) { - // Invalid twitter verification + if (profile.proof_twitter) { + try { + verifs.twitter = await verifier.verifyTwitter(did, profile.proof_twitter) + } catch (err) { + // Invalid twitter verification + } } - } - if (profile.ethereum_proof) { - try { - verifs.ethereum = await verifier.verifyEthereum(profile.ethereum_proof, did) - } catch (err) { - // Invalid eth verification + if (profile.ethereum_proof) { + try { + verifs.ethereum = await verifier.verifyEthereum(profile.ethereum_proof, did) + } catch (err) { + // Invalid eth verification + } } + } catch (err) { + // Invalid proof for DID return an empty profile } - } catch (err) { - // Invalid proof for DID return an empty profile + return verifs } - return verifs } -module.exports = { profileGraphQL, getProfile, getSpace, listSpaces, getThread, getThreadByAddress, getConfig, getRootStoreAddress, getProfiles, getVerifiedAccounts, getSpaceDID } +module.exports = BoxApi From 9c5d0ff6463db5dbd5d2a2118500d4eefe945b67 Mon Sep 17 00:00:00 2001 From: Joel Torstensson Date: Thu, 5 Dec 2019 10:22:16 +0100 Subject: [PATCH 04/10] fix(ghost): add postId to ghost posts --- package-lock.json | 34 +++++++++++++++++++++------------- package.json | 2 +- src/__tests__/ghost.test.js | 18 ++++++++++++------ src/ghost.js | 12 ++++++++---- 4 files changed, 42 insertions(+), 24 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4c3f3dc8..d537005c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "3box", - "version": "1.13.2", + "version": "1.14.1-beta.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -4326,6 +4326,16 @@ "idb-readable-stream": "0.0.4", "ltgt": "^2.1.2", "xtend": "^4.0.1" + }, + "dependencies": { + "idb-readable-stream": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/idb-readable-stream/-/idb-readable-stream-0.0.4.tgz", + "integrity": "sha1-MoPaZkW/ayINxhumHfYr7l2uSs8=", + "requires": { + "xtend": "^4.0.1" + } + } } } } @@ -8352,14 +8362,6 @@ "safer-buffer": ">= 2.1.2 < 3" } }, - "idb-readable-stream": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/idb-readable-stream/-/idb-readable-stream-0.0.4.tgz", - "integrity": "sha1-MoPaZkW/ayINxhumHfYr7l2uSs8=", - "requires": { - "xtend": "^4.0.1" - } - }, "identity-wallet": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/identity-wallet/-/identity-wallet-1.0.0.tgz", @@ -9147,6 +9149,10 @@ "murmurhash3js": "^3.0.1", "nodeify": "^1.0.1" } + }, + "webcrypto-shim": { + "version": "github:dignifiedquire/webcrypto-shim#190bc9ec341375df6025b17ae12ddb2428ea49c8", + "from": "github:dignifiedquire/webcrypto-shim#master" } } }, @@ -9239,6 +9245,12 @@ "rsa-pem-to-jwk": "^1.1.3", "tweetnacl": "^1.0.0", "webcrypto-shim": "github:dignifiedquire/webcrypto-shim#master" + }, + "dependencies": { + "webcrypto-shim": { + "version": "github:dignifiedquire/webcrypto-shim#190bc9ec341375df6025b17ae12ddb2428ea49c8", + "from": "github:dignifiedquire/webcrypto-shim#master" + } } }, "multiaddr": { @@ -18301,10 +18313,6 @@ "neo-async": "^2.5.0" } }, - "webcrypto-shim": { - "version": "github:dignifiedquire/webcrypto-shim#190bc9ec341375df6025b17ae12ddb2428ea49c8", - "from": "github:dignifiedquire/webcrypto-shim#master" - }, "webidl-conversions": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", diff --git a/package.json b/package.json index d1d821f7..e70fcde8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "3box", - "version": "1.14.0", + "version": "1.14.1-beta.1", "description": "Interact with user data", "main": "lib/3box.js", "directories": { diff --git a/src/__tests__/ghost.test.js b/src/__tests__/ghost.test.js index 2f1a23a7..d0991e7d 100644 --- a/src/__tests__/ghost.test.js +++ b/src/__tests__/ghost.test.js @@ -87,8 +87,9 @@ describe('Ghost Chat', () => { expect(message).toEqual('wide') const posts = await chat2.getPosts() const post = posts.pop() - delete post.timestamp // since we have no way to get it from onUpdate - expect(post).toEqual({ type: 'chat', author: DID1, message: 'wide' }) + expect(post).toMatchObject({ type: 'chat', author: DID1, message: 'wide' }) + expect(post).toHaveProperty('postId') + expect(post).toHaveProperty('timestamp') done() } }) @@ -102,8 +103,9 @@ describe('Ghost Chat', () => { expect(message).toEqual('direct peer') const posts = await chat2.getPosts() const post = posts.pop() - delete post.timestamp // since we have no way to get it from onUpdate - expect(post).toEqual({ type: 'chat', author: DID1, message: 'direct peer' }) + expect(post).toMatchObject({ type: 'chat', author: DID1, message: 'direct peer' }) + expect(post).toHaveProperty('postId') + expect(post).toHaveProperty('timestamp') done() } }) @@ -117,8 +119,9 @@ describe('Ghost Chat', () => { expect(message).toEqual('direct 3id') const posts = await chat2.getPosts() const post = posts.pop() - delete post.timestamp // since we have no way to get it from onUpdate - expect(post).toEqual({ type: 'chat', author: DID1, message: 'direct 3id' }) + expect(post).toMatchObject({ type: 'chat', author: DID1, message: 'direct 3id' }) + expect(post).toHaveProperty('postId') + expect(post).toHaveProperty('timestamp') done() } }) @@ -138,6 +141,7 @@ describe('Ghost Chat', () => { }) afterAll(async () => { + await chat2.close() await utils.stopIPFS(ipfs2, 12) }) }) @@ -188,11 +192,13 @@ describe('Ghost Chat', () => { }) afterAll(async () => { + await chat3.close() await utils.stopIPFS(ipfs3, 12) }) }) afterAll(async () => { + await chat1.close() await utils.stopIPFS(ipfs, 11) }) }) diff --git a/src/ghost.js b/src/ghost.js index 00a72191..e3d8afda 100644 --- a/src/ghost.js +++ b/src/ghost.js @@ -11,6 +11,7 @@ class GhostThread extends EventEmitter { this._spaceName = name.split('.')[2] this._3id = threeId this._room = Room(ipfs, name) // instance of ipfs pubsub room + this._ipfs = ipfs this._peerId = ipfs._peerInfo.id.toB58String() this._members = {} @@ -223,9 +224,9 @@ class GhostThread extends EventEmitter { * @param {Object} payload The payload of the message */ async _messageReceived (payload) { - const { type, message, iss: author, iat: timestamp } = payload - this._backlog.add(JSON.stringify({ type, author, message, timestamp })) - this.emit('message', { type, author, message, timestamp }) + const { type, message, iss: author, iat: timestamp, postId } = payload + this._backlog.add(JSON.stringify({ type, author, message, timestamp, postId })) + this.emit('message', { type, author, message, timestamp, postId }) } /** @@ -236,8 +237,11 @@ class GhostThread extends EventEmitter { */ async _verifyData (data) { const jwt = data.toString() + const cidPromise = this._ipfs.dag.put(jwt) try { - return await verifyJWT(jwt) + const verified = await verifyJWT(jwt) + verified.payload.postId = (await cidPromise).toString() + return verified } catch (e) { console.log(e) } From b1499c2180d779811194a8fbd3c4b3706343556f Mon Sep 17 00:00:00 2001 From: Joel Torstensson Date: Fri, 6 Dec 2019 12:26:07 +0100 Subject: [PATCH 05/10] feat(3box): implement new init api --- src/3box.js | 104 ++++++++++++--- src/__mocks__/3box.js | 1 + src/__mocks__/keyValueStore.js | 2 +- src/__tests__/3box.test.js | 10 +- .../__snapshots__/replicator.test.js.snap | 15 ++- .../__snapshots__/space.test.js.snap | 2 + src/__tests__/ghost.test.js | 12 +- src/__tests__/keyValueStore.test.js | 8 ++ src/__tests__/replicator.test.js | 10 +- src/__tests__/space.test.js | 40 ++++-- src/__tests__/thread.test.js | 125 +++++++++++------- src/ghost.js | 15 ++- src/keyValueStore.js | 3 +- src/replicator.js | 3 +- src/space.js | 46 +++++-- src/thread.js | 27 ++-- src/verified.js | 11 +- 17 files changed, 317 insertions(+), 117 deletions(-) diff --git a/src/3box.js b/src/3box.js index 79b477b5..b833e03a 100644 --- a/src/3box.js +++ b/src/3box.js @@ -47,10 +47,9 @@ class Box extends BoxApi { * Please use the **openBox** method to instantiate a 3Box * @constructor */ - constructor (threeId, provider, ipfs, opts = {}) { + constructor (provider, ipfs, opts = {}) { super() - this._3id = threeId - this._web3provider = provider + this._provider = provider this._ipfs = ipfs registerResolver(this._ipfs, { pin: true }) this._serverUrl = opts.addressServer || ADDRESS_SERVER_URL @@ -78,18 +77,20 @@ class Box extends BoxApi { this.hasPublishedLink = {} } - async _load (opts = {}) { + async _init (opts) { this.replicator = await Replicator.create(this._ipfs, opts) + } + async _load (opts = {}) { const address = await this._3id.getAddress() const rootstoreAddress = address ? await this._getRootstore(address) : null if (rootstoreAddress) { await this.replicator.start(rootstoreAddress, { profile: true }) await this.replicator.rootstoreSyncDone const authData = await this.replicator.getAuthData() - await this._3id.authenticate(null, { authData }) + await this._3id.authenticate(opts.spaces, { authData }) } else { - await this._3id.authenticate() + await this._3id.authenticate(opts.spaces) const rootstoreName = this._3id.muportFingerprint + '.root' const key = (await this._3id.getPublicKeys(null, true)).signingKey await this.replicator.new(rootstoreName, key, this._3id.DID, this._3id.muportDID) @@ -112,6 +113,46 @@ class Box extends BoxApi { await this.private._load() } + /** + * Creates an instance of 3Box + * + * @param {provider} provider A 3ID provider, or ethereum provider + * @param {Object} opts Optional parameters + * @param {String} opts.pinningNode A string with an ipfs multi-address to a 3box pinning node + * @param {Object} opts.ipfs A js-ipfs ipfs object + * @param {String} opts.addressServer URL of the Address Server + * @return {Box} the 3Box instance for the given address + */ + static async create (provider, opts = {}) { + const ipfs = await Box.getIPFS(opts) + const box = new Box(provider, ipfs, opts) + await box._init(opts) + return box + } + + /** + * Authenticate the user + * + * @param {Array} spaces A list of spaces to authenticate (optional) + * @param {Object} opts Optional parameters + * @param {String} opts.address An ethereum address + * @param {Function} opts.consentCallback A function that will be called when the user has consented to opening the box + */ + async auth (spaces = [], opts = {}) { + if (!this._3id) { + if (!this._provider.is3idProvider && !opts.address) throw new Error('auth: address needed when 3ID provider is not used') + this._3id = await ThreeId.getIdFromEthAddress(opts.address, this._provider, this._ipfs, opts) + await this._load(Object.assign(opts, { spaces })) + } else { + // box already loaded, just authenticate spaces + await this._3id.authenticate(spaces) + } + // make sure we are authenticated to threads + spaces.forEach(space => { + this.spaces[space]._authThreads(this._3id) + }) + } + /** * Opens the 3Box associated with the given address * @@ -126,14 +167,9 @@ class Box extends BoxApi { * @return {Box} the 3Box instance for the given address */ static async openBox (address, provider, opts = {}) { - const ipfs = await Box.getIPFS(opts) - if (typeof address === 'object' && address !== null) { - // legacy support for IdentityWallet being passed in first param - provider = address.get3idProvider() - } - const threeId = await ThreeId.getIdFromEthAddress(address, provider, ipfs, opts) - const box = new Box(threeId, provider, ipfs, opts) - await box._load(opts) + opts = Object.assign(opts, { address }) + const box = await Box.create(provider, opts) + await box.auth([], opts) return box } @@ -147,10 +183,13 @@ class Box extends BoxApi { * @return {Space} the Space instance for the given space name */ async openSpace (name, opts = {}) { + if (!this._3id) throw new Error('openSpace: auth required') if (!this.spaces[name]) { - this.spaces[name] = new Space(name, this.replicator, this._3id) + this.spaces[name] = new Space(name, this.replicator) + } + if (!this.spaces[name].isOpen) { try { - await this.spaces[name].open(opts) + await this.spaces[name].open(this._3id, opts) if (!await this.isAddressLinked()) this.linkAddress() } catch (e) { delete this.spaces[name] @@ -167,6 +206,28 @@ class Box extends BoxApi { return this.spaces[name] } + /** + * Join a thread. Use this to start receiving updates + * + * @param {String} space The name of the space for this thread + * @param {String} name The name of the thread + * @param {Object} opts Optional parameters + * @param {String} opts.firstModerator DID of first moderator of a thread, by default, user is first moderator + * @param {Boolean} opts.members join a members only thread, which only members can post in, defaults to open thread + * @param {Boolean} opts.noAutoSub Disable auto subscription to the thread when posting to it (default false) + * @param {Boolean} opts.ghost Enable ephemeral messaging via Ghost Thread + * @param {Number} opts.ghostBacklogLimit The number of posts to maintain in the ghost backlog + * @param {Array} opts.ghostFilters Array of functions for filtering messages + * + * @return {Thread} An instance of the thread class for the joined thread + */ + async joinThread(space, name, opts) { + if (!this.spaces[space]) { + this.spaces[space] = new Space(space, this.replicator) + } + return this.spaces[space].joinThread(name, opts) + } + /** * Sets the callback function that will be called once when the box is fully synced. * @@ -221,6 +282,7 @@ class Box extends BoxApi { * @property {String} DID the DID of the user */ get DID () { + if (!this._3id) throw new Error('DID: auth required') // TODO - update once verification service supports 3ID return this._3id.muportDID } @@ -232,6 +294,7 @@ class Box extends BoxApi { * @param {Object} [link.proof] Proof object, should follow [spec](https://github.com/3box/3box/blob/master/3IPs/3ip-5.md) */ async linkAddress (link = {}) { + if (!this._3id) throw new Error('linkAddress: auth required') if (link.proof) { await this._writeAddressLink(link.proof) } else { @@ -245,6 +308,7 @@ class Box extends BoxApi { * @param {String} address address that is linked */ async removeAddressLink (address) { + if (!this._3id) throw new Error('removeAddressLink: auth required') address = address.toLowerCase() const linkExist = await this.isAddressLinked({ address }) if (!linkExist) throw new Error('removeAddressLink: link for given address does not exist') @@ -280,6 +344,7 @@ class Box extends BoxApi { * @param {String} [query.address] Is the given adressed linked */ async isAddressLinked (query = {}) { + if (!this._3id) throw new Error('isAddressLinked: auth required') if (query.address) query.address = query.address.toLowerCase() const links = await this._readAddressLinks() const linksQuery = links.find(link => { @@ -295,6 +360,7 @@ class Box extends BoxApi { * @return {Array} An array of link objects */ async listAddressLinks () { + if (!this._3id) throw new Error('listAddressLinks: auth required') const entries = await this._readAddressLinks() return entries.reduce((list, entry) => { const item = Object.assign({}, entry) @@ -320,9 +386,9 @@ class Box extends BoxApi { let proof = await this._readAddressLink(address) if (!proof) { - if (!this._web3provider.is3idProvider) { + if (!this._provider.is3idProvider) { try { - proof = await createLink(this._3id.DID, address, this._web3provider) + proof = await createLink(this._3id.DID, address, this._provider) } catch (e) { throw new Error('Link consent message must be signed before adding data, to link address to store', e) } @@ -398,6 +464,7 @@ class Box extends BoxApi { } async close () { + if (!this._3id) throw new Error('close: auth required') await this.replicator.stop() } @@ -407,6 +474,7 @@ class Box extends BoxApi { * you call openBox. */ async logout () { + if (!this._3id) throw new Error('logout: auth required') await this.close() this._3id.logout() const address = await this._3id.getAddress() diff --git a/src/__mocks__/3box.js b/src/__mocks__/3box.js index 3f934051..9b861f3e 100644 --- a/src/__mocks__/3box.js +++ b/src/__mocks__/3box.js @@ -13,6 +13,7 @@ class Box { set: (k, v) => this._privateStore[k] = v, get: (k) => this._privateStore[k] } + this.DID = muportDID } static async openBox (address, ethereumProvider, opts = {}) { diff --git a/src/__mocks__/keyValueStore.js b/src/__mocks__/keyValueStore.js index 3bc330d2..11e25c17 100644 --- a/src/__mocks__/keyValueStore.js +++ b/src/__mocks__/keyValueStore.js @@ -49,7 +49,7 @@ class KeyValueStore { return '/orbitdb/myodbaddr' } - _load () { + _load (threeId) { this._db = { all: () => { let allObj = {} diff --git a/src/__tests__/3box.test.js b/src/__tests__/3box.test.js index d34e0659..dbb965e2 100644 --- a/src/__tests__/3box.test.js +++ b/src/__tests__/3box.test.js @@ -77,9 +77,13 @@ jest.mock('../privateStore', () => { }) jest.mock('../space', () => { return jest.fn(name => { + let isOpen = false return { _name: name, - open: jest.fn() + get isOpen () { + return isOpen + }, + open: jest.fn(() => isOpen = true) } }) }) @@ -362,7 +366,7 @@ describe('3Box', () => { expect(mockedUtils.fetchJson).toHaveBeenCalledTimes(1) expect(mockedUtils.fetchJson.mock.calls[0][0]).toEqual('address-server/odbAddress/0x12345') expect(box._3id.authenticate).toHaveBeenCalledTimes(1) - expect(box._3id.authenticate).toHaveBeenCalledWith(null, { authData: [] }) + expect(box._3id.authenticate).toHaveBeenCalledWith([], { authData: [] }) expect(box.replicator.start).toHaveBeenCalledTimes(1) expect(box.replicator.start).toHaveBeenCalledWith('/orbitdb/asdf/rootstore-address', { profile: true }) expect(box.replicator.new).toHaveBeenCalledTimes(0) @@ -382,7 +386,7 @@ describe('3Box', () => { global.console.error = jest.fn() let space1 = await box.openSpace('name1', {}) expect(space1._name).toEqual('name1') - expect(space1.open).toHaveBeenCalledWith(expect.any(Object)) + expect(space1.open).toHaveBeenCalledWith(expect.any(Object), {}) let opts = { onSyncDone: jest.fn() } let space2 = await box.openSpace('name1', opts) expect(space1).toEqual(space2) diff --git a/src/__tests__/__snapshots__/replicator.test.js.snap b/src/__tests__/__snapshots__/replicator.test.js.snap index 266868a1..e9a44119 100644 --- a/src/__tests__/__snapshots__/replicator.test.js.snap +++ b/src/__tests__/__snapshots__/replicator.test.js.snap @@ -39,12 +39,6 @@ Array [ ] `; -exports[`Replicator creates replicator correctly 1`] = ` -Array [ - "3box-pinning", -] -`; - exports[`Replicator ensureConnected works as expected for store 1`] = ` Object { "odbAddress": "/orbitdb/QmVL3Dtc7GxYdjXQW6raRoCnA1t8PsPkE832jcZXr2QDP5/rsName.root", @@ -87,9 +81,18 @@ exports[`Replicator new rootstore created 1`] = `"/orbitdb/QmVL3Dtc7GxYdjXQW6raR exports[`Replicator new rootstore created 2`] = `"/orbitdb/QmVL3Dtc7GxYdjXQW6raRoCnA1t8PsPkE832jcZXr2QDP5/rsName.root"`; +exports[`Replicator new rootstore created 3`] = ` +Array [ + "3box-pinning", + "/orbitdb/QmVL3Dtc7GxYdjXQW6raRoCnA1t8PsPkE832jcZXr2QDP5/rsName.root", +] +`; + exports[`Replicator replicates 3box on start, with profile 1`] = ` Object { "emoji": ";P", "name": "asdfasdf", } `; + +exports[`Replicator replicates 3box on start, without stores 1`] = `"3box-pinning"`; diff --git a/src/__tests__/__snapshots__/space.test.js.snap b/src/__tests__/__snapshots__/space.test.js.snap index ff8f324c..4a401844 100644 --- a/src/__tests__/__snapshots__/space.test.js.snap +++ b/src/__tests__/__snapshots__/space.test.js.snap @@ -2,6 +2,8 @@ exports[`Space Threads does not subscribe or return invalid thread address (ignore experimental) 1`] = `"subscribeThread: must subscribe to valid thread/orbitdb address"`; +exports[`Space Threads should trow if space not open and no firstModerator 1`] = `[Error: firstModerator required if not authenticated]`; + exports[`Space public store reducer all should return all values from public store 1`] = ` Object { "k1": "v1", diff --git a/src/__tests__/ghost.test.js b/src/__tests__/ghost.test.js index d0991e7d..b77369a0 100644 --- a/src/__tests__/ghost.test.js +++ b/src/__tests__/ghost.test.js @@ -35,11 +35,13 @@ describe('Ghost Chat', () => { }) it('creates chat correctly', async () => { - chat = new GhostThread(CHAT_NAME, { ipfs }, THREEID1_MOCK); + chat = new GhostThread(CHAT_NAME, { ipfs }) expect(chat._name).toEqual(CHAT_NAME) - expect(chat._3id).toEqual(THREEID1_MOCK) expect(chat.listMembers).toBeDefined() expect(chat.getPosts()).toBeDefined() + expect(chat.isGhost).toBeTruthy() + chat._set3id(THREEID1_MOCK) + expect(chat._3id).toEqual(THREEID1_MOCK) }) it('should catch messages', async (done) => { @@ -63,7 +65,8 @@ describe('Ghost Chat', () => { }) it('creates second chat correctly', async (done) => { - chat2 = new GhostThread(CHAT_NAME, { ipfs: ipfs2 }, THREEID2_MOCK); + chat2 = new GhostThread(CHAT_NAME, { ipfs: ipfs2 }); + chat2._set3id(THREEID2_MOCK) expect(chat2._name).toEqual(CHAT_NAME) expect(chat2._3id).toEqual(THREEID2_MOCK) expect(chat2.listMembers()).toBeDefined() @@ -158,7 +161,8 @@ describe('Ghost Chat', () => { it('creates third chat correctly', async (done) => { chat.removeAllListeners() - chat3 = new GhostThread(CHAT_NAME, { ipfs: ipfs3 }, THREEID3_MOCK, { ghostFilters: [filter] }); + chat3 = new GhostThread(CHAT_NAME, { ipfs: ipfs3 }, { ghostFilters: [filter] }); + chat3._set3id(THREEID3_MOCK) expect(chat3._name).toEqual(CHAT_NAME) expect(chat3._3id).toEqual(THREEID3_MOCK) expect(chat3.listMembers()).toBeDefined() diff --git a/src/__tests__/keyValueStore.test.js b/src/__tests__/keyValueStore.test.js index c70b95a5..25f771dc 100644 --- a/src/__tests__/keyValueStore.test.js +++ b/src/__tests__/keyValueStore.test.js @@ -72,6 +72,14 @@ describe('KeyValueStore', () => { replicatorMock.addKVStore.mockClear() }) + it('should correctly take 3id at _load', async () => { + const kvs = new KeyValueStore('tmp', replicatorMock) + expect(kvs._3id).toBeUndefined() + await kvs._load(THREEID_MOCK) + expect(kvs._3id).toEqual(THREEID_MOCK) + kvs.close() + }) + it('should throw if not synced', async () => { expect(keyValueStore.set('key', 'value')).rejects.toThrow(/_load must/) expect(keyValueStore.setMultiple(['keys'], ['values'])).rejects.toThrow(/_load must/) diff --git a/src/__tests__/replicator.test.js b/src/__tests__/replicator.test.js index 13fcaa8f..e520951f 100644 --- a/src/__tests__/replicator.test.js +++ b/src/__tests__/replicator.test.js @@ -37,9 +37,6 @@ describe('Replicator', () => { orbitPath: './tmp/orbitdb13', } replicator1 = await Replicator.create(ipfs1, opts) - - expect(await replicator1.ipfs.pubsub.ls()).toMatchSnapshot() - expect((await replicator1.ipfs.swarm.peers()).map(p => p.peer._idB58String)).toContain(ipfs2MultiAddr.split('/').pop()) registerMethod('3', didResolverMock) }) @@ -60,6 +57,9 @@ describe('Replicator', () => { expect(replicator1.rootstore.address.toString()).toMatchSnapshot() await publishPromise await pubsub2.unsubscribe(PINNING_ROOM) + + expect(await replicator1.ipfs.pubsub.ls()).toMatchSnapshot() + expect((await replicator1.ipfs.swarm.peers()).map(p => p.peer._idB58String)).toContain(ipfs2MultiAddr.split('/').pop()) }) it('adds profile KVStore correctly', async () => { @@ -98,6 +98,10 @@ describe('Replicator', () => { replicator1._pubsub.publish(PINNING_ROOM, { type: 'HAS_ENTRIES', odbAddress: rootstoreAddress, numEntries: rootstoreNumEntries }) await replicator2.rootstoreSyncDone await replicator2.syncDone + + expect((await replicator2.ipfs.pubsub.ls())[0]).toMatchSnapshot() + expect((await replicator2.ipfs.swarm.peers()).map(p => p.peer._idB58String)).toContain(ipfs1MultiAddr.split('/').pop()) + expect(replicator2.listStoreAddresses()).toEqual(replicator1.listStoreAddresses()) expect(replicator2.rootstore.iterator({ limit: -1 }).collect()).toEqual(replicator1.rootstore.iterator({ limit: -1 }).collect()) expect(replicator2._stores).toEqual({}) diff --git a/src/__tests__/space.test.js b/src/__tests__/space.test.js index 06e7d479..73da6593 100644 --- a/src/__tests__/space.test.js +++ b/src/__tests__/space.test.js @@ -14,6 +14,7 @@ const threeIdMock = { signJWT: (payload, { space }) => { return `a fake jwt for ${space}` }, + getOdbId: async () => 'odbid', getSubDID: (space) => `subdid-${space}` } const replicatorMock = 'replicator' @@ -40,11 +41,11 @@ describe('Space', () => { }) it('should be correctly constructed', async () => { - space = new Space(NAME1, replicatorMock, threeIdMock) + space = new Space(NAME1, replicatorMock) expect(space._name).toEqual(NAME1) - expect(space._3id).toEqual(threeIdMock) expect(space._store._replicator).toEqual(replicatorMock) expect(space._store._name).toEqual('3box.space.' + NAME1 + '.keyvalue') + expect(space.isOpen).toBeFalsy() }) it('should open a new space correctly', async () => { @@ -54,7 +55,8 @@ describe('Space', () => { const syncDonePromise = new Promise((resolve, reject) => { opts.onSyncDone = resolve }) - await space.open(opts) + await space.open(threeIdMock, opts) + expect(space.isOpen).toBeTruthy() expect(opts.consentCallback).toHaveBeenCalledWith(true, NAME1) //expect(rootstoreMock.add).toHaveBeenCalledWith({ type: 'space', DID: threeIdMock.getSubDID(NAME1), odbAddress:'/orbitdb/myodbaddr' }) expect(threeIdMock.isAuthenticated).toHaveBeenCalledWith([NAME1]) @@ -66,7 +68,7 @@ describe('Space', () => { let opts = { consentCallback: jest.fn(), } - await space.open(opts) + await space.open(threeIdMock, opts) expect(opts.consentCallback).toHaveBeenCalledTimes(0) }) @@ -207,10 +209,11 @@ describe('Space', () => { expect(Thread).toHaveBeenCalledTimes(1) expect(Thread.mock.calls[0][0]).toEqual(`3box.thread.${NAME1}.t2`) expect(Thread.mock.calls[0][1]).toEqual(replicatorMock) - expect(Thread.mock.calls[0][2]).toEqual(threeIdMock) expect(t1._load).toHaveBeenCalledTimes(1) + expect(t1._setIdentity).toHaveBeenCalledTimes(1) + expect(t1._setIdentity).toHaveBeenCalledWith('odbid') // function for autosubscribing works as intended - await Thread.mock.calls[0][5](threadAddress) + await Thread.mock.calls[0][4](threadAddress) expect(await space.subscribedThreads()).toEqual([{address: threadAddress}]) }) @@ -224,11 +227,32 @@ describe('Space', () => { expect(Thread).toHaveBeenCalledTimes(1) expect(Thread.mock.calls[0][0]).toEqual(`3box.thread.${NAME1}.t3`) expect(Thread.mock.calls[0][1]).toEqual(replicatorMock) - expect(Thread.mock.calls[0][2]).toEqual(threeIdMock) expect(t1._load).toHaveBeenCalledTimes(1) + expect(t1._setIdentity).toHaveBeenCalledTimes(1) + expect(t1._setIdentity).toHaveBeenCalledWith('odbid') // function for autosubscribing works as intended - await Thread.mock.calls[0][5](threadAddress2) + await Thread.mock.calls[0][4](threadAddress2) expect(await space.subscribedThreads()).toEqual([{address: threadAddress}]) }) + + it('should trow if space not open and no firstModerator', async () => { + const sp = new Space(NAME2, replicatorMock) + const t1 = await + expect(sp.joinThread('t1')).rejects.toMatchSnapshot() + }) + + it('joins thread correctly with space that is not open', async () => { + const sp = new Space(NAME2, replicatorMock) + const firstModerator = 'did:3:bafyasdf' + const t1 = await sp.joinThread('t1', { firstModerator }) + expect(Thread).toHaveBeenCalledTimes(1) + expect(Thread.mock.calls[0][0]).toEqual(`3box.thread.${NAME2}.t1`) + expect(Thread.mock.calls[0][1]).toEqual(replicatorMock) + expect(Thread.mock.calls[0][3]).toEqual(firstModerator) + expect(t1._load).toHaveBeenCalledTimes(1) + expect(t1._setIdentity).toHaveBeenCalledTimes(0) + // function for autosubscribing works as intended + await Thread.mock.calls[0][4](threadAddress) + }) }) }) diff --git a/src/__tests__/thread.test.js b/src/__tests__/thread.test.js index 4d177807..d212bc98 100644 --- a/src/__tests__/thread.test.js +++ b/src/__tests__/thread.test.js @@ -64,7 +64,7 @@ describe('Thread', () => { }) it('creates thread correctly', async () => { - thread = new Thread(THREAD1_NAME, replicatorMock, THREEID1_MOCK, false, DID1, subscribeMock) + thread = new Thread(THREAD1_NAME, replicatorMock, false, DID1, subscribeMock) }) it('should throw if not loaded', async () => { @@ -73,14 +73,23 @@ describe('Thread', () => { await expect(thread.onUpdate(() => {})).rejects.toThrow(/_load must/) }) - it('should start with an empty db on load', async () => { + it('should throw if not authed', async () => { storeAddr = await thread._load() - expect(storeAddr.split('/')[3]).toEqual(THREAD1_NAME) - expect(await thread.getPosts()).toEqual([]) + await expect(thread.post('key')).rejects.toThrow(/You must/) + await expect(thread.addModerator('key')).rejects.toThrow(/You must/) + await expect(thread.addMember('key')).rejects.toThrow(/You must/) + await expect(thread.deletePost('key')).rejects.toThrow(/You must/) + // load should call this: expect(replicatorMock.ensureConnected).toHaveBeenCalledTimes(1) expect(replicatorMock.ensureConnected).toHaveBeenCalledWith(storeAddr, true) }) + it('should start with an empty db on load', async () => { + thread._setIdentity(await THREEID1_MOCK.getOdbId()) + expect(storeAddr.split('/')[3]).toEqual(THREAD1_NAME) + expect(await thread.getPosts()).toEqual([]) + }) + it('adding posts works as expected', async () => { await thread.post(MSG1) let posts = await thread.getPosts() @@ -108,26 +117,19 @@ describe('Thread', () => { expect(replicatorMock.ensureConnected).toHaveBeenNthCalledWith(3, storeAddr, true) }) - it('defaults to user as root moderator and no members ', async () => { - const threadDefault = new Thread(THREAD3_NAME, replicatorMock, THREEID1_MOCK, undefined, undefined, subscribeMock) - expect(threadDefault._firstModerator).toEqual(THREEID1_MOCK.DID) - expect(threadDefault._members).toEqual(false) - await threadDefault._load() - const moderators = await threadDefault.listModerators() - expect(moderators).toEqual([THREEID1_MOCK.DID]) - }) - it('root moderator can add another moderator', async () => { - const threadMods = new Thread(THREAD3_NAME, replicatorMock, THREEID1_MOCK, false, DID1, subscribeMock) + const threadMods = new Thread(THREAD3_NAME, replicatorMock, false, DID1, subscribeMock) await threadMods._load() + threadMods._setIdentity(await THREEID1_MOCK.getOdbId()) expect(await threadMods.listModerators()).toEqual([DID1]) await threadMods.addModerator(DID2) expect(await threadMods.listModerators()).toEqual([DID1, DID2]) }) it('user who is not moderator can NOT add a moderator', async () => { - const threadMods = new Thread(THREAD3_NAME, replicatorMock, THREEID1_MOCK, false, DID2, subscribeMock) + const threadMods = new Thread(THREAD3_NAME, replicatorMock, false, DID2, subscribeMock) await threadMods._load() + threadMods._setIdentity(await THREEID1_MOCK.getOdbId()) expect(await threadMods.listModerators()).toEqual([DID2]) await expect(threadMods.addModerator(DID3)).rejects.toThrow(/can not be granted/) expect(await threadMods.listModerators()).toEqual([DID2]) @@ -135,21 +137,24 @@ describe('Thread', () => { it('a moderator can add another moderator', async () => { const threadName = randomThreadName() - const threadMod1 = new Thread(threadName, replicatorMock, THREEID1_MOCK, false, DID1, subscribeMock) + const threadMod1 = new Thread(threadName, replicatorMock, false, DID1, subscribeMock) await threadMod1._load() + threadMod1._setIdentity(await THREEID1_MOCK.getOdbId()) expect(await threadMod1.listModerators()).toEqual([DID1]) await threadMod1.addModerator(DID2) expect(await threadMod1.listModerators()).toEqual([DID1, DID2]) - const threadMod2 = new Thread(threadName, replicatorMock, THREEID2_MOCK, false, DID1, subscribeMock) + const threadMod2 = new Thread(threadName, replicatorMock, false, DID1, subscribeMock) await threadMod2._load() + threadMod2._setIdentity(await THREEID2_MOCK.getOdbId()) await threadMod2.addModerator(DID3) expect(await threadMod2.listModerators()).toEqual([DID1, DID2, DID3]) }) it('moderator can add a Member', async () => { const threadName = randomThreadName() - const threadMembers = new Thread(threadName, replicatorMock, THREEID1_MOCK, true, DID1, subscribeMock) + const threadMembers = new Thread(threadName, replicatorMock, true, DID1, subscribeMock) await threadMembers._load() + threadMembers._setIdentity(await THREEID1_MOCK.getOdbId()) expect(await threadMembers.listMembers()).toEqual([]) await threadMembers.addMember(DID2) expect(await threadMembers.listMembers()).toEqual([DID2]) @@ -157,48 +162,55 @@ describe('Thread', () => { it('user who is not a moderator can NOT add a Member', async () => { const threadName = randomThreadName() - const threadMembers = new Thread(threadName, replicatorMock, THREEID1_MOCK, true, DID2, subscribeMock) + const threadMembers = new Thread(threadName, replicatorMock, true, DID2, subscribeMock) await threadMembers._load() + threadMembers._setIdentity(await THREEID1_MOCK.getOdbId()) await expect(threadMembers.addMember(DID3)).rejects.toThrow(/can not be granted/) expect(await threadMembers.listMembers()).toEqual([]) }) it('throws if using member operations on a non member thread', async () => { const threadName = randomThreadName() - const threadMembers = new Thread(threadName, replicatorMock, THREEID1_MOCK, false, DID1, subscribeMock) + const threadMembers = new Thread(threadName, replicatorMock, false, DID1, subscribeMock) await threadMembers._load() + threadMembers._setIdentity(await THREEID1_MOCK.getOdbId()) await expect(threadMembers.addMember(DID2)).rejects.toThrow(/Not a members only thread/) }) it('a moderator can delete other users posts', async () => { const threadName = randomThreadName() - const thread1 = new Thread(threadName, replicatorMock, THREEID1_MOCK, false, DID2, subscribeMock) + const thread1 = new Thread(threadName, replicatorMock, false, DID2, subscribeMock) await thread1._load() + thread1._setIdentity(await THREEID1_MOCK.getOdbId()) await thread1.post(MSG1) const posts = await thread1.getPosts() const entryId = posts[0].postId - const thread2 = new Thread(threadName, replicatorMock, THREEID2_MOCK, false, DID2, subscribeMock) + const thread2 = new Thread(threadName, replicatorMock, false, DID2, subscribeMock) await thread2._load() + thread2._setIdentity(await THREEID2_MOCK.getOdbId()) await thread2.deletePost(entryId) expect(await thread2.getPosts()).toEqual([]) }) it('user without being a moderator can NOT delete others posts', async () => { const threadName = randomThreadName() - const thread1 = new Thread(threadName, replicatorMock, THREEID1_MOCK, false, DID3, subscribeMock) + const thread1 = new Thread(threadName, replicatorMock, false, DID3, subscribeMock) await thread1._load() + thread1._setIdentity(await THREEID1_MOCK.getOdbId()) await thread1.post(MSG1) const posts = await thread1.getPosts() const entryId = posts[0].postId - const thread2 = new Thread(threadName, replicatorMock, THREEID2_MOCK, false, DID3, subscribeMock) + const thread2 = new Thread(threadName, replicatorMock, false, DID3, subscribeMock) await thread2._load() + thread2._setIdentity(await THREEID2_MOCK.getOdbId()) await expect(thread2.deletePost(entryId)).rejects.toThrow(/not append entry/) }) it('user without being a moderator can delete their own posts', async () => { const threadName = randomThreadName() - const thread = new Thread(threadName, replicatorMock, THREEID1_MOCK, false, DID3, subscribeMock) + const thread = new Thread(threadName, replicatorMock, false, DID3, subscribeMock) await thread._load() + thread._setIdentity(await THREEID1_MOCK.getOdbId()) await thread.post(MSG1) const posts = await thread.getPosts() const entryId = posts[0].postId @@ -208,19 +220,22 @@ describe('Thread', () => { it('non member can NOT post to a members thread', async () => { const threadName = randomThreadName() - const thread1 = new Thread(threadName, replicatorMock, THREEID1_MOCK, true, DID3, subscribeMock) + const thread1 = new Thread(threadName, replicatorMock, true, DID3, subscribeMock) await thread1._load() + thread1._setIdentity(await THREEID1_MOCK.getOdbId()) await expect(thread1.post(MSG1)).rejects.toThrow(/not append entry/) }) it('a member can post in a members only thread', async () => { const threadName = randomThreadName() - const thread1 = new Thread(threadName, replicatorMock, THREEID1_MOCK, true, DID1, subscribeMock) + const thread1 = new Thread(threadName, replicatorMock, true, DID1, subscribeMock) await thread1._load() + thread1._setIdentity(await THREEID1_MOCK.getOdbId()) await thread1.addMember(DID2) expect(await thread1.listMembers()).toEqual([DID2]) - const thread2 = new Thread(threadName, replicatorMock, THREEID2_MOCK, true, DID1, subscribeMock) + const thread2 = new Thread(threadName, replicatorMock, true, DID1, subscribeMock) await thread2._load() + thread2._setIdentity(await THREEID2_MOCK.getOdbId()) await thread2.post(MSG1) const posts = await thread.getPosts() await expect(posts[0].message).toEqual(MSG1) @@ -228,36 +243,42 @@ describe('Thread', () => { it('a member can NOT add a member', async () => { const threadName = randomThreadName() - const thread1 = new Thread(threadName, replicatorMock, THREEID1_MOCK, true, DID1, subscribeMock) + const thread1 = new Thread(threadName, replicatorMock, true, DID1, subscribeMock) await thread1._load() + thread1._setIdentity(await THREEID1_MOCK.getOdbId()) await thread1.addMember(DID2) expect(await thread1.listMembers()).toEqual([DID2]) - const thread2 = new Thread(threadName, replicatorMock, THREEID2_MOCK, true, DID1, subscribeMock) + const thread2 = new Thread(threadName, replicatorMock, true, DID1, subscribeMock) await thread2._load() + thread2._setIdentity(await THREEID2_MOCK.getOdbId()) await expect(thread2.addMember(DID3)).rejects.toThrow(/can not be granted/) expect(await thread2.listMembers()).toEqual([DID2]) }) it('a member can NOT add a moderator', async () => { const threadName = randomThreadName() - const thread1 = new Thread(threadName, replicatorMock, THREEID1_MOCK, true, DID1, subscribeMock) + const thread1 = new Thread(threadName, replicatorMock, true, DID1, subscribeMock) await thread1._load() + thread1._setIdentity(await THREEID1_MOCK.getOdbId()) await thread1.addMember(DID2) expect(await thread1.listMembers()).toEqual([DID2]) - const thread2 = new Thread(threadName, replicatorMock, THREEID2_MOCK, true, DID1, subscribeMock) + const thread2 = new Thread(threadName, replicatorMock, true, DID1, subscribeMock) await thread2._load() + thread2._setIdentity(await THREEID2_MOCK.getOdbId()) await expect(thread2.addModerator(DID3)).rejects.toThrow(/can not be granted/) expect(await thread2.listMembers()).toEqual([DID2]) }) it('a moderator can post in a members only thread', async () => { const threadName = randomThreadName() - const thread1 = new Thread(threadName, replicatorMock, THREEID1_MOCK, true, DID1, subscribeMock) + const thread1 = new Thread(threadName, replicatorMock, true, DID1, subscribeMock) await thread1._load() + thread1._setIdentity(await THREEID1_MOCK.getOdbId()) await thread1.addModerator(DID2) expect(await thread1.listModerators()).toEqual([DID1, DID2]) - const thread2 = new Thread(threadName, replicatorMock, THREEID2_MOCK, true, DID1, subscribeMock) + const thread2 = new Thread(threadName, replicatorMock, true, DID1, subscribeMock) await thread2._load() + thread2._setIdentity(await THREEID2_MOCK.getOdbId()) await thread2.post(MSG1) const posts = await thread.getPosts() await expect(posts[0].message).toEqual(MSG1) @@ -265,38 +286,44 @@ describe('Thread', () => { it('a member upgraded to a moderator can add other mods', async () => { const threadName = randomThreadName() - const thread1 = new Thread(threadName, replicatorMock, THREEID1_MOCK, true, DID1, subscribeMock) + const thread1 = new Thread(threadName, replicatorMock, true, DID1, subscribeMock) await thread1._load() + thread1._setIdentity(await THREEID1_MOCK.getOdbId()) await thread1.addMember(DID2) await thread1.addModerator(DID2) - const thread2 = new Thread(threadName, replicatorMock, THREEID2_MOCK, true, DID1, subscribeMock) + const thread2 = new Thread(threadName, replicatorMock, true, DID1, subscribeMock) await thread2._load() + thread2._setIdentity(await THREEID2_MOCK.getOdbId()) await thread2.addModerator(DID3) expect(await thread2.listModerators()).toEqual([DID1, DID2, DID3]) }) it('a member upgraded to a moderator can add other members', async () => { const threadName = randomThreadName() - const thread1 = new Thread(threadName, replicatorMock, THREEID1_MOCK, true, DID1, subscribeMock) + const thread1 = new Thread(threadName, replicatorMock, true, DID1, subscribeMock) await thread1._load() + thread1._setIdentity(await THREEID1_MOCK.getOdbId()) await thread1.addMember(DID2) await thread1.addModerator(DID2) - const thread2 = new Thread(threadName, replicatorMock, THREEID2_MOCK, true, DID1, subscribeMock) + const thread2 = new Thread(threadName, replicatorMock, true, DID1, subscribeMock) await thread2._load() + thread2._setIdentity(await THREEID2_MOCK.getOdbId()) await thread2.addMember(DID3) expect(await thread2.listMembers()).toEqual([DID2, DID3]) }) it('a thread can be loaded by its address only', async () => { const threadName = randomThreadName() - const thread1 = new Thread(threadName, replicatorMock, THREEID1_MOCK, false, DID1, subscribeMock) + const thread1 = new Thread(threadName, replicatorMock, false, DID1, subscribeMock) await thread1._load() + thread1._setIdentity(await THREEID1_MOCK.getOdbId()) const thread1Address = thread1.address await thread1.post(MSG1) const posts = await thread1.getPosts() const entryId = posts[0].postId - const thread2 = new Thread(threadName, replicatorMock, THREEID2_MOCK, undefined, undefined, subscribeMock) + const thread2 = new Thread(threadName, replicatorMock, undefined, DID1, subscribeMock) await thread2._load(thread1Address) + thread2._setIdentity(await THREEID2_MOCK.getOdbId()) const thread2Address = thread2.address expect(thread1Address).toEqual(thread2Address) await expect(posts[0].message).toEqual(MSG1) @@ -320,10 +347,12 @@ describe('Thread', () => { }) it('syncs thread between users', async () => { - threadUser1 = new Thread(THREAD2_NAME, replicatorMock, THREEID1_MOCK,false, DID1, subscribeMock) + threadUser1 = new Thread(THREAD2_NAME, replicatorMock, false, DID1, subscribeMock) await threadUser1._load() - threadUser2 = new Thread(THREAD2_NAME, replicatorMock2, THREEID2_MOCK, false, DID1,subscribeMock) + threadUser1._setIdentity(await THREEID1_MOCK.getOdbId()) + threadUser2 = new Thread(THREAD2_NAME, replicatorMock2, false, DID1,subscribeMock) await threadUser2._load() + threadUser2._setIdentity(await THREEID2_MOCK.getOdbId()) // user1 posts and user2 receives // done needed to not catch the write event let done = false @@ -368,10 +397,12 @@ describe('Thread', () => { it('moderator capabilities are shared/synced between users', async () => { const threadName = randomThreadName() - threadUser1 = new Thread(threadName, replicatorMock, THREEID1_MOCK,false, DID1, subscribeMock) + threadUser1 = new Thread(threadName, replicatorMock, false, DID1, subscribeMock) await threadUser1._load() - threadUser2 = new Thread(threadName, replicatorMock2, THREEID2_MOCK, false, DID1,subscribeMock) + threadUser1._setIdentity(await THREEID1_MOCK.getOdbId()) + threadUser2 = new Thread(threadName, replicatorMock2, false, DID1,subscribeMock) await threadUser2._load() + threadUser2._setIdentity(await THREEID2_MOCK.getOdbId()) // user two tries to add a moderator and fails await expect(threadUser2.addModerator(DID3)).rejects.toThrow(/can not be granted/) @@ -396,10 +427,12 @@ describe('Thread', () => { it('member capabilities are shared/synced between users', async () => { const threadName = randomThreadName() - threadUser1 = new Thread(threadName, replicatorMock, THREEID1_MOCK, true, DID1, subscribeMock) + threadUser1 = new Thread(threadName, replicatorMock, true, DID1, subscribeMock) await threadUser1._load() - threadUser2 = new Thread(threadName, replicatorMock2, THREEID2_MOCK, true, DID1, subscribeMock) + threadUser1._setIdentity(await THREEID1_MOCK.getOdbId()) + threadUser2 = new Thread(threadName, replicatorMock2, true, DID1, subscribeMock) await threadUser2._load() + threadUser2._setIdentity(await THREEID2_MOCK.getOdbId()) // user two tries to add a post and fails await expect(threadUser2.post(MSG1)).rejects.toThrow(/not append entry/) diff --git a/src/ghost.js b/src/ghost.js index e3d8afda..53e1d7ba 100644 --- a/src/ghost.js +++ b/src/ghost.js @@ -5,11 +5,10 @@ const Room = require('ipfs-pubsub-room') const DEFAULT_BACKLOG_LIMIT = 100 class GhostThread extends EventEmitter { - constructor (name, { ipfs }, threeId, opts = {}) { + constructor (name, { ipfs }, opts = {}) { super() this._name = name this._spaceName = name.split('.')[2] - this._3id = threeId this._room = Room(ipfs, name) // instance of ipfs pubsub room this._ipfs = ipfs this._peerId = ipfs._peerInfo.id.toB58String() @@ -54,6 +53,14 @@ class GhostThread extends EventEmitter { this._room.on('peer left', (peer) => this._userLeft(peer)) } + get isGhost () { + return true + } + + _set3id (threeId) { + this._3id = threeId + } + /** * Get a list of users online * @@ -148,6 +155,7 @@ class GhostThread extends EventEmitter { * @param {Object} message The message */ async _broadcast (message) { + if (!this._3id) throw new Error('Can not send message if not authenticated') const jwt = await this._3id.signJWT(message, { use3ID: true }) this._room.broadcast(jwt) } @@ -159,6 +167,7 @@ class GhostThread extends EventEmitter { * @param {String} to The PeerID or 3ID of the receiver */ async _sendDirect (message, to) { + if (!this._3id) throw new Error('Can not send message if not authenticated') const jwt = await this._3id.signJWT(message, { use3ID: true }) to.startsWith('Qm') ? this._room.sendTo(to, jwt) : this._room.sendTo(this._threeIdToPeerId(to), jwt) @@ -198,7 +207,7 @@ class GhostThread extends EventEmitter { */ async _userJoined (did, peerID) { const members = await this.listMembers() - if (!members.includes(did) && this._3id.DID !== did) { + if (!members.includes(did) && (this._3id && this._3id.DID !== did)) { this._members[did] = peerID this._members[peerID] = did this.emit('user-joined', 'joined', did, peerID) diff --git a/src/keyValueStore.js b/src/keyValueStore.js index 7f3fceee..5f5ac252 100644 --- a/src/keyValueStore.js +++ b/src/keyValueStore.js @@ -140,7 +140,8 @@ class KeyValueStore { return this._db.address.toString() } - async _load () { + async _load (threeId) { + this._3id = threeId ? threeId : this._3id const odbAddress = this._replicator.listStoreAddresses().find(odbAddress => odbAddress.includes(this._name)) if (odbAddress) { this._db = await this._replicator.getStore(odbAddress) diff --git a/src/replicator.js b/src/replicator.js index 275b32bf..290ff1df 100644 --- a/src/replicator.js +++ b/src/replicator.js @@ -85,7 +85,6 @@ class Replicator { async _init (opts) { this._pubsub = new Pubsub(this.ipfs, (await this.ipfs.id()).id) this._orbitdb = await OrbitDB.createInstance(this.ipfs, { directory: opts.orbitPath }) - await this._joinPinningRoom(true) } async _joinPinningRoom (firstJoin) { @@ -106,6 +105,7 @@ class Replicator { } async start (rootstoreAddress, opts = {}) { + await this._joinPinningRoom(true) this._publishDB({ odbAddress: rootstoreAddress }) this.rootstore = await this._orbitdb.feed(rootstoreAddress, ODB_STORE_OPTS) @@ -126,6 +126,7 @@ class Replicator { async new (rootstoreName, pubkey, did, muportDID) { if (this.rootstore) throw new Error('This method can only be called once before the replicator has started') + await this._joinPinningRoom(true) const opts = { ...ODB_STORE_OPTS, format: 'dag-pb' diff --git a/src/space.js b/src/space.js index c0ff1369..4bdc6343 100644 --- a/src/space.js +++ b/src/space.js @@ -12,11 +12,10 @@ class Space { /** * Please use **box.openSpace** to get the instance of this class */ - constructor (name, replicator, threeId) { + constructor (name, replicator) { this._name = name - this._3id = threeId this._replicator = replicator - this._store = new KeyValueStore(nameToSpaceName(this._name), this._replicator, this._3id) + this._store = new KeyValueStore(nameToSpaceName(this._name), this._replicator) this._activeThreads = {} /** * @property {KeyValueStore} public access the profile store of the space @@ -39,15 +38,20 @@ class Space { return this._3id.getSubDID(this._name) } - async open (opts = {}) { - if (!this._store._db) { + get isOpen () { + return Boolean(this._store._db) + } + + async open (threeId, opts = {}) { + if (!this.isOpen) { // store is not loaded opened yet + this._3id = threeId const authenticated = await this._3id.isAuthenticated([this._name]) if (!authenticated) { await this._3id.authenticate([this._name]) } if (opts.consentCallback) opts.consentCallback(!authenticated, this._name) - await this._store._load() + await this._store._load(this._3id) const syncSpace = async () => { await this._store._sync() @@ -56,9 +60,22 @@ class Space { this.syncDone = syncSpace() this.public = publicStoreReducer(this._store) this.private = privateStoreReducer(this._store, this._3id, this._name) + // make sure we're authenticated to all threads + await this._authThreads(this._3id) } } + async _authThreads (threeId) { + const odbIdentity = await threeId.getOdbId(this._name) + Object.values(this._activeThreads).forEach(thread => { + if (thread.isGhost) { + thread._set3id(this._3id) + } else { + thread._setIdentity(odbIdentity) + } + }) + } + /** * Join a thread. Use this to start receiving updates from, and to post in threads * @@ -79,14 +96,23 @@ class Space { if (!this._activeThreads[ghostAddress]) { this._activeThreads[ghostAddress] = new GhostThread(ghostAddress, this._replicator, this._3id, opts) } + if (this._3id) { + thread._set3id(this._3id) + } return this._activeThreads[ghostAddress] } else { const subscribeFn = opts.noAutoSub ? () => {} : this.subscribeThread.bind(this) - if (!opts.firstModerator) opts.firstModerator = this._3id.getSubDID(this._name) - const thread = new Thread(namesTothreadName(this._name, name), this._replicator, this._3id, opts.members, opts.firstModerator, subscribeFn) + if (!opts.firstModerator) { + if (!this._3id) throw new Error('firstModerator required if not authenticated') + opts.firstModerator = this._3id.getSubDID(this._name) + } + const thread = new Thread(namesTothreadName(this._name, name), this._replicator, opts.members, opts.firstModerator, subscribeFn) const address = await thread._getThreadAddress() if (this._activeThreads[address]) return this._activeThreads[address] await thread._load() + if (this._3id) { + thread._setIdentity(await this._3id.getOdbId(this._name)) + } this._activeThreads[address] = thread return thread } @@ -103,6 +129,7 @@ class Space { */ async joinThreadByAddress (address, opts = {}) { if (!OrbitDBAddress.isValid(address)) throw new Error('joinThreadByAddress: valid orbitdb address required') + if (!this.isOpen) throw new Error('joinThreadByAddress requires space to be open') const threadSpace = address.split('.')[2] const threadName = address.split('.')[3] if (threadSpace !== this._name) throw new Error('joinThreadByAddress: attempting to open thread from different space, must open within same space') @@ -125,6 +152,7 @@ class Space { */ async subscribeThread (address, config = {}) { if (!OrbitDBAddress.isValid(address)) throw new Error('subscribeThread: must subscribe to valid thread/orbitdb address') + if (!this.isOpen) return // we can't subscribe if space isn't open const threadKey = `thread-${address}` await this.syncDone if (!(await this.public.get(threadKey))) { @@ -138,6 +166,7 @@ class Space { * @param {String} address The address of the thread */ async unsubscribeThread (address) { + if (!this.isOpen) throw new Error('unsubscribeThread requires space to be open') const threadKey = `thread-${address}` if (await this.public.get(threadKey)) { await this.public.remove(threadKey) @@ -150,6 +179,7 @@ class Space { * @return {Array} A list of thread objects as { address, firstModerator, members, name} */ async subscribedThreads () { + if (!this.isOpen) throw new Error('subscribedThreads requires space to be open') const allEntries = await this.public.all() return Object.keys(allEntries).reduce((threads, key) => { if (key.startsWith('thread')) { diff --git a/src/thread.js b/src/thread.js index 86a901a4..87f7985b 100644 --- a/src/thread.js +++ b/src/thread.js @@ -16,15 +16,14 @@ class Thread { /** * Please use **space.joinThread** to get the instance of this class */ - constructor (name, replicator, threeId, members, firstModerator, subscribe) { + constructor (name, replicator, members, firstModerator, subscribe) { this._name = name this._replicator = replicator this._spaceName = name.split('.')[2] - this._3id = threeId this._subscribe = subscribe this._queuedNewPosts = [] this._members = Boolean(members) - this._firstModerator = firstModerator || this._3id.getSubDID(this._spaceName) + this._firstModerator = firstModerator } /** @@ -35,6 +34,7 @@ class Thread { */ async post (message) { this._requireLoad() + this._requireAuth() this._subscribe(this._address, { firstModerator: this._firstModerator, members: this._members, name: this._name }) this._replicator.ensureConnected(this._address, true) const timestamp = Math.floor(new Date().getTime() / 1000) // seconds @@ -64,6 +64,7 @@ class Thread { */ async addModerator (id) { this._requireLoad() + this._requireAuth() if (id.startsWith('0x')) { id = await API.getSpaceDID(id, this._spaceName) } @@ -88,6 +89,7 @@ class Thread { */ async addMember (id) { this._requireLoad() + this._requireAuth() this._throwIfNotMembers() if (id.startsWith('0x')) { id = await API.getSpaceDID(id, this._spaceName) @@ -119,6 +121,7 @@ class Thread { */ async deletePost (hash) { this._requireLoad() + this._requireAuth() return this._db.remove(hash) } @@ -181,10 +184,8 @@ class Thread { async _load (odbAddress) { await this._initConfigs() - const identity = this._identity this._db = await this._replicator._orbitdb.feed(odbAddress || this._name, { ...ORBITDB_OPTS, - identity, accessController: this._accessController }) await this._db.load() @@ -197,14 +198,23 @@ class Thread { if (!this._db) throw new Error('_load must be called before interacting with the store') } + _requireAuth () { + if (!this._authenticated) throw new Error('You must authenticate before performing this action') + } + async close () { this._requireLoad() await this._db.close() } + _setIdentity (odbId) { + this._db.setIdentity(odbId) + this._db.access._db.setIdentity(odbId) + this._authenticated = true + } + async _initConfigs () { - if (this._identity) return - this._identity = await this._3id.getOdbId(this._spaceName) + if (this._accessController) return if (this._firstModerator.startsWith('0x')) { this._firstModerator = await API.getSpaceDID(this._firstModerator, this._spaceName) } @@ -212,8 +222,7 @@ class Thread { type: 'thread-access', threadName: this._name, members: this._members, - firstModerator: this._firstModerator, - identity: this._identity + firstModerator: this._firstModerator } } } diff --git a/src/verified.js b/src/verified.js index 0e551109..848dde29 100644 --- a/src/verified.js +++ b/src/verified.js @@ -6,29 +6,28 @@ class Verified { */ constructor (box) { this._box = box - this._did = box._3id.muportDID } async _addVerifiedPublicAccount (key, proof, verificationFunction) { - const account = await verificationFunction(this._did, proof) + const account = await verificationFunction(this._box.DID, proof) await this._box.public.set('proof_' + key, proof) return account } async _getVerifiedPublicAccount (key, verificationFunction) { const proof = await this._box.public.get('proof_' + key) - return verificationFunction(this._did, proof) + return verificationFunction(this._box.DID, proof) } async _addVerifiedPrivateAccount (key, proof, verificationFunction) { - const account = await verificationFunction(this._did, proof) + const account = await verificationFunction(this._box.DID, proof) await this._box.private.set('proof_' + key, proof) return account } async _getVerifiedPrivateAccount (key, verificationFunction) { const proof = await this._box.private.get('proof_' + key) - return verificationFunction(this._did, proof) + return verificationFunction(this._box.DID, proof) } /** @@ -37,7 +36,7 @@ class Verified { * @return {String} The DID of the user */ async DID () { - return this._did + return this._box.DID } /** From f57ad49fe3bf06e046a91fcc7e68de93963b54a9 Mon Sep 17 00:00:00 2001 From: Joel Torstensson Date: Fri, 6 Dec 2019 15:31:35 +0100 Subject: [PATCH 06/10] feat(3box): test new init api --- src/3box.js | 6 ++- src/__tests__/3box.test.js | 77 +++++++++++++++++++-------- src/__tests__/idWallet.integration.js | 25 +++++---- src/keyValueStore.js | 2 +- src/space.js | 2 +- 5 files changed, 76 insertions(+), 36 deletions(-) diff --git a/src/3box.js b/src/3box.js index b833e03a..5ce06aed 100644 --- a/src/3box.js +++ b/src/3box.js @@ -149,7 +149,9 @@ class Box extends BoxApi { } // make sure we are authenticated to threads spaces.forEach(space => { - this.spaces[space]._authThreads(this._3id) + if (this.spaces[space]) { + this.spaces[space]._authThreads(this._3id) + } }) } @@ -221,7 +223,7 @@ class Box extends BoxApi { * * @return {Thread} An instance of the thread class for the joined thread */ - async joinThread(space, name, opts) { + async joinThread (space, name, opts) { if (!this.spaces[space]) { this.spaces[space] = new Space(space, this.replicator) } diff --git a/src/__tests__/3box.test.js b/src/__tests__/3box.test.js index dbb965e2..0710ba8e 100644 --- a/src/__tests__/3box.test.js +++ b/src/__tests__/3box.test.js @@ -1,6 +1,5 @@ const testUtils = require('./testUtils') const OrbitDB = require('orbit-db') -const Pubsub = require('orbit-db-pubsub') const jsdom = require('jsdom') const didJWT = require('did-jwt') const Box = require('../3box') @@ -83,7 +82,9 @@ jest.mock('../space', () => { get isOpen () { return isOpen }, - open: jest.fn(() => isOpen = true) + open: jest.fn(() => isOpen = true), + joinThread: jest.fn(), + _authThreads: jest.fn() } }) }) @@ -192,7 +193,7 @@ const MOCK_HASH_SERVER = 'address-server' const MOCK_PROFILE_SERVER = 'profile-server' describe('3Box', () => { - let ipfs, pubsub, boxOpts, ipfsBox, box, boxWithLinks + let ipfs, boxOpts, ipfsBox jest.setTimeout(30000) const clearMocks = () => { @@ -206,7 +207,6 @@ describe('3Box', () => { beforeAll(async () => { if (!ipfs) ipfs = await testUtils.initIPFS(0) const ipfsMultiAddr = (await ipfs.id()).addresses[0] - if (!pubsub) pubsub = new Pubsub(ipfs, (await ipfs.id()).id) const IPFS_OPTIONS = { EXPERIMENTAL: { @@ -235,12 +235,61 @@ describe('3Box', () => { }) afterAll(async () => { - await pubsub.disconnect() - await box.close() await testUtils.stopIPFS(ipfs, 0) await testUtils.stopIPFS(ipfsBox, 1) }) + it('Create instance of 3box works as intended', async () => { + const prov = 'web3prov' + const box = await Box.create(prov, boxOpts) + expect(box.replicator).toBeDefined() + expect(box._provider).toEqual(prov) + expect(box._ipfs).toEqual(boxOpts.ipfs) + }) + + it('joinThread without being authenticated', async () => { + const space = 's1' + const name = 't1' + const prov = 'web3prov' + const box = await Box.create(prov, boxOpts) + await box.joinThread(space, name, {}) + expect(box.spaces[space]).toBeDefined() + expect(box.spaces[space].joinThread).toHaveBeenCalledWith(name, {}) + }) + + it('authenticating works as expected', async () => { + const space = 's1' + const name = 't1' + const prov = 'web3prov' + const opts = { address: '0x12345' } + const box = await Box.create(prov, boxOpts) + await box.joinThread(space, name, {}) + + await box.auth([space], opts) + expect(mocked3id.getIdFromEthAddress).toHaveBeenCalledTimes(1) + expect(mocked3id.getIdFromEthAddress).toHaveBeenCalledWith(opts.address, prov, boxOpts.ipfs, opts) + expect(box._3id.getAddress).toHaveBeenCalledTimes(1) + expect(mockedUtils.fetchJson.mock.calls[0][0]).toEqual('address-server/odbAddress/0x12345') + expect(box._3id.authenticate).toHaveBeenCalledTimes(1) + expect(box.replicator.start).toHaveBeenCalledTimes(0) + expect(box.replicator.new).toHaveBeenCalledTimes(1) + expect(box.replicator.rootstore.setIdentity).toHaveBeenCalledTimes(1) + expect(box.public._load).toHaveBeenCalledTimes(1) + expect(box.public._load).toHaveBeenCalledWith() + expect(box.private._load).toHaveBeenCalledTimes(1) + expect(box.private._load).toHaveBeenCalledWith() + expect(box.spaces[space]._authThreads).toHaveBeenCalledTimes(1) + await box.syncDone + expect(mockedUtils.fetchJson).toHaveBeenCalledTimes(2) + expect(mockedUtils.fetchJson.mock.calls[1][0]).toEqual('address-server/odbAddress') + expect(didJWT.decodeJWT(mockedUtils.fetchJson.mock.calls[1][1].address_token).payload.rootStoreAddress).toEqual('/orbitdb/asdf/rootstore-address') + + // second call to auth should only auth new spaces + await box.auth(['s2']) + expect(box._3id.authenticate).toHaveBeenCalledTimes(2) + expect(box._3id.authenticate).toHaveBeenCalledWith(['s2']) + }) + it('should openBox correctly with normal auth flow, for new accounts', async () => { const addr = '0x12345' const prov = 'web3prov' @@ -401,22 +450,6 @@ describe('3Box', () => { await box.close() }) - //it.skip('should getProfile correctly (when profile API is not used)', async () => { - //// Disabled this for now. I don't think the way we get profiles - //// though orbitdb right now makes sense anyway. In the future - //// we propbably want to have a stateful api for getting and following - //// other users. - //await box._rootStore.drop() - //// awaitbox2._ruotStore.drop() - //const profile = await Box.getProfile('0x12345', Object.assign(boxOpts, {useCacheService: false})) - //expect(profile).toEqual({ - //name: 'oed', - //image: 'an awesome selfie' - //}) - //expect(mockedUtils.fetchJson).toHaveBeenCalledTimes(1) - //expect(mockedUtils.fetchJson).toHaveBeenCalledWith('address-server/odbAddress/0x12345') - //}) - it('should get profile (when API is used)', async () => { delete boxOpts.useCacheService const profile = await Box.getProfile('0x12345', boxOpts) diff --git a/src/__tests__/idWallet.integration.js b/src/__tests__/idWallet.integration.js index c428599b..9b204a89 100644 --- a/src/__tests__/idWallet.integration.js +++ b/src/__tests__/idWallet.integration.js @@ -81,13 +81,15 @@ describe('Integration Test: IdentityWallet', () => { }) afterAll(async () => { - //await testutils.stopipfs(ipfs1, 9) - //await testUtils.stopIPFS(ipfs2, 10) + await pubsub.disconnect() + await testUtils.stopIPFS(ipfs1, 9) + await testUtils.stopIPFS(ipfs2, 10) }) - it('should openBox correctly when idWallet is passed', async () => { + it('should create and auth correctly when idWallet is passed', async () => { const provider = idWallet.get3idProvider() - const box = await Box.openBox(null, provider, opts) + const box = await Box.create(provider, opts) + await box.auth([], opts) await box.syncDone await box.public.set('a', 1) await box.public.set('b', 2) @@ -99,10 +101,11 @@ describe('Integration Test: IdentityWallet', () => { await box.close() }) - it('should get same state on second openBox', async () => { + it('should get same state on second open and auth', async () => { const provider = idWallet.get3idProvider() publishHasEntries() - const box = await Box.openBox(null, provider, opts) + const box = await Box.create(provider, opts) + await box.auth([], opts) await box.syncDone expect(await box.public.all()).toEqual(pubState) @@ -114,11 +117,12 @@ describe('Integration Test: IdentityWallet', () => { await box.close() }) - it('should get same state on openBox with IdentityWallet opened using first authSecret', async () => { + it('should get same state on create and auth with IdentityWallet opened using first authSecret', async () => { idWallet = new IdentityWallet(getConsent, { authSecret: AUTH_1 }) const provider = idWallet.get3idProvider() publishHasEntries() - const box = await Box.openBox(null, provider, opts) + const box = await Box.create(provider, opts) + await box.auth([], opts) await box.syncDone expect(await box.public.all()).toEqual(pubState) @@ -130,11 +134,12 @@ describe('Integration Test: IdentityWallet', () => { await box.close() }) - it('should get same state on openBox with IdentityWallet opened using second authSecret', async () => { + it('should get same state on create and auth with IdentityWallet opened using second authSecret', async () => { idWallet = new IdentityWallet(getConsent, { authSecret: AUTH_2 }) const provider = idWallet.get3idProvider() publishHasEntries() - const box = await Box.openBox(null, provider, opts) + const box = await Box.create(provider, opts) + await box.auth([], opts) await box.syncDone expect(await box.public.all()).toEqual(pubState) await box.close() diff --git a/src/keyValueStore.js b/src/keyValueStore.js index 5f5ac252..6954880c 100644 --- a/src/keyValueStore.js +++ b/src/keyValueStore.js @@ -141,7 +141,7 @@ class KeyValueStore { } async _load (threeId) { - this._3id = threeId ? threeId : this._3id + this._3id = threeId || this._3id const odbAddress = this._replicator.listStoreAddresses().find(odbAddress => odbAddress.includes(this._name)) if (odbAddress) { this._db = await this._replicator.getStore(odbAddress) diff --git a/src/space.js b/src/space.js index 4bdc6343..21bae17f 100644 --- a/src/space.js +++ b/src/space.js @@ -97,7 +97,7 @@ class Space { this._activeThreads[ghostAddress] = new GhostThread(ghostAddress, this._replicator, this._3id, opts) } if (this._3id) { - thread._set3id(this._3id) + this._activeThreads[ghostAddress]._set3id(this._3id) } return this._activeThreads[ghostAddress] } else { From 3cc451c9f4007b8d87859edd64bcc02656b8cb0e Mon Sep 17 00:00:00 2001 From: Joel Torstensson Date: Mon, 9 Dec 2019 10:31:48 +0100 Subject: [PATCH 07/10] docs: update readme with new init api --- README.md | 81 ++++++++++++++++++++++++++++++++++++++++++---- readme-template.md | 28 ++++++++++++---- src/3box.js | 2 +- 3 files changed, 98 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 5f72f4fd..63cbadcd 100644 --- a/README.md +++ b/README.md @@ -51,16 +51,24 @@ console.log(profile) ``` ### Update (get, set, remove) public and private profile data -3Box allows applications to create, read, update, and delete public and private data stored in a user's 3Box. To enable this functionality, applications must first open the user's 3Box by calling the openBox method. This method prompts the user to authenticate (sign-in) to your dapp and returns a promise with a threeBox instance. You can only update (set, get, remove) data for users that have authenticated to and are currently interacting with your dapp. Below `ethereumProvider` refers to the object that you would get from `web3.currentProvider`, or `window.ethereum`. +3Box allows applications to create, read, update, and delete public and private data stored in a user's 3Box. To enable this functionality, applications must first authenticate the user's 3Box by calling the `auth` method. This method prompts the user to authenticate (sign-in) to your dapp and returns a promise with a threeBox instance. You can only update (set, get, remove) data for users that have authenticated to and are currently interacting with your dapp. Below `ethereumProvider` refers to the object that you would get from `web3.currentProvider`, or `window.ethereum`. -#### 1. Authenticate users to begin new 3Box session -Calling the openBox method will open a new 3Box session. If the user's ethereum address already has a 3Box account, your application will gain access to it. If the user does not have an existing 3Box account, this method will automatically create one for them in the background. +#### 1. Create a 3Box instance +To create a 3Box session you call the `create` method. This creates an instance of the Box class which can be used to joinThreads and authenticate the user in any order. In order to create a 3Box session a `provider` needs to be passed. This can be an `ethereum provider` (from `web3.currentProvider`, or `window.ethereum`) or a `3ID Provider` (from [IdentityWallet](https://github.com/3box/identity-wallet-js)). ```js -const box = await Box.openBox('0x12345abcde', ethereumProvider) +const box = await Box.create(provider) +``` + +#### 2. Authenticate user +Calling the `auth` method will authenticate the user. If you want to authenticate the user to one or multiple spaces you can specify this here. If when you created the 3Box session you used an ethereum provider you need to pass an ethereum address to the `auth` method. If the user does not have an existing 3Box account, this method will automatically create one for them in the background. +```js +const address = '0x12345abcde' +const spaces = ['myDapp'] +await box.auth(spaces, { address }) ``` -#### 2. Sync user's available 3Box data from the network -When you first open the box in your dapp all data might not be synced from the network yet. You should therefore wait for the data to be fully synced. To do this you can simply await the `box.syncDone` promise: +#### 3. Sync user's available 3Box data from the network +When you first authenticate the box in your dapp all data might not be synced from the network yet. You should therefore wait for the data to be fully synced. To do this you can simply await the `box.syncDone` promise: ```js await box.syncDone ``` @@ -104,6 +112,14 @@ const privateValues = ['xxx', 'yyy'] await box.private.setMultiple(privateFields, privateValues) ``` +##### Join a thread +Once you have created a 3Box session you can join a thread to view data in it. This can be done before you authenticate the user to be able to post in the thread. +When joining a thread the moderation options need to be given. You can pass `firstModerator`, a 3ID (or ethereum address) of the first moderator, and a `members` boolean which indicates if it is a members thread or not. +```js +const thread = await box.joinThread('myDapp', 'myThread', { firstModerator: 'did:3:bafy...', members: true }) +``` + +