From 0c6a785d8a3b30974bd3b2e9950612a98d3939fa Mon Sep 17 00:00:00 2001 From: tahpot Date: Wed, 15 Feb 2023 18:11:01 +1030 Subject: [PATCH] Feature/56 redesign replication (#57) --- CHANGELOG.md | 5 + package.json | 11 +- sample.env | 7 +- src/components/authManager.js | 29 +-- src/components/dbManager.js | 11 +- src/components/replicationManager.js | 316 +++++++++++++------------- src/controllers/user.js | 101 +++++++- src/middleware/garbageCollection.js | 18 ++ src/routes/private.js | 6 + src/server-app.js | 7 +- src/services/didStorage/controller.js | 1 - test/replication.js | 155 +++++++------ test/utils.js | 16 ++ yarn.lock | 101 +++++++- 14 files changed, 504 insertions(+), 280 deletions(-) create mode 100644 src/middleware/garbageCollection.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 228d7896..e861e116 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +2023-02-15 (2.1.0) +-------------------- + +- Redesign how replication works (initiated when a user connects and remains active for 20 minutes, instead of always replicate everything) + 2023-01-13 (2.0.0) -------------------- diff --git a/package.json b/package.json index 7c0317b7..29eb723f 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,9 @@ "dev": "nodemon --exec babel-node src/server.js", "build": "yarn clean && yarn babel src -d dist --extensions .js", "serve": "node --trace-warnings dist/server.js", - "build-docker-multiplatform": "yarn build && docker buildx build --platform linux/amd64,linux/arm64 --push -t verida/storage-node:latest .", - "build-docker-amd64": "yarn build && docker buildx build --platform linux/amd64 --push -t verida/storage-node:latest ." + "build-docker-multiplatform-dev": "yarn build && docker buildx build --platform linux/amd64,linux/arm64 --push -t verida/storage-node:dev .", + "build-docker-multiplatform-prod": "yarn build && docker buildx build --platform linux/amd64,linux/arm64 --push -t verida/storage-node:latest .", + "build-docker-amd64-prod": "yarn build && docker buildx build --platform linux/amd64 --push -t verida/storage-node:latest ." }, "files": [ "**/*.ts", @@ -43,9 +44,9 @@ "homepage": "https://github.com/verida/storage-node/README.md", "dependencies": { "@babel/runtime": "^7.16.7", - "@verida/did-client": "^2.0.0-rc4", - "@verida/did-document": "^2.0.0-rc4", - "@verida/encryption-utils": "^2.0.0-rc5", + "@verida/did-client": "^2.0.5", + "@verida/did-document": "^2.0.5", + "@verida/encryption-utils": "^2.0.4", "aws-serverless-express": "^3.4.0", "axios": "^1.2.1", "cors": "^2.8.5", diff --git a/sample.env b/sample.env index 76a708fa..ec3fb48b 100644 --- a/sample.env +++ b/sample.env @@ -27,8 +27,8 @@ ENDPOINT_URI="http://localhost:5000" DB_REJECT_UNAUTHORIZED_SSL=true ACCESS_JWT_SIGN_PK=insert-random-access-symmetric-key -# 5 Minutes -ACCESS_TOKEN_EXPIRY=300 +# 10 Minutes +ACCESS_TOKEN_EXPIRY=600 REFRESH_JWT_SIGN_PK=insert-random-refresh-symmetric-key # 30 Days REFRESH_TOKEN_EXPIRY=2592000 @@ -41,6 +41,9 @@ VDA_PRIVATE_KEY= DEFAULT_USER_CONTEXT_LIMIT_MB=10 # Maximum number of users supported by this node MAX_USERS=10000 +# How many minutes before the replication expires on an open database +# Should be 2x ACCESS_TOKEN_EXPIRY +REPLICATION_EXPIRY_MINUTES=20 # Alpha numeric only DB_PUBLIC_USER=784c2n780c9cn0789 diff --git a/src/components/authManager.js b/src/components/authManager.js index 8677a9dc..54179509 100644 --- a/src/components/authManager.js +++ b/src/components/authManager.js @@ -193,8 +193,6 @@ class AuthManager { await tokenDb.insert(tokenRow); - this.gc() - return token } @@ -378,7 +376,7 @@ class AuthManager { }) } catch (err) { // Handle invalid JWT by rejecting verification - if (err.name == "JsonWebTokenError") { + if (err.name == "JsonWebTokenError" || err.name == "TokenExpiredError") { return false } @@ -446,6 +444,14 @@ class AuthManager { } } + const expiryIndex = { + index: { fields: ['expiry'] }, + name: 'expiry' + }; + + const replicatorDb = couch.db.use('_replicator'); + await replicatorDb.createIndex(expiryIndex); + const tokenDb = couch.db.use(process.env.DB_REFRESH_TOKENS); const deviceIndex = { @@ -453,11 +459,6 @@ class AuthManager { name: 'deviceHash' }; - const expiryIndex = { - index: { fields: ['expiry'] }, - name: 'expiry' - }; - await tokenDb.createIndex(deviceIndex); await tokenDb.createIndex(expiryIndex); } @@ -533,15 +534,7 @@ class AuthManager { } // Garbage collection of refresh tokens - async gc() { - const GC_PERCENT = process.env.GC_PERCENT - const random = Math.random() - - if (random >= GC_PERCENT) { - // Skip running GC - return - } - + async clearExpired() { // Delete all expired refresh tokens const now = parseInt((new Date()).getTime() / 1000.0) const query = { @@ -563,7 +556,7 @@ class AuthManager { await tokenDb.destroy(doc._id, doc._rev) } catch (err) { if (err.error != 'not_found' && err.error != 'conflict') { - console.log(`Unknown error in garbage collection: ${err.message}`) + console.error(`Unknown error in garbage collection: ${err.message}`) } } } diff --git a/src/components/dbManager.js b/src/components/dbManager.js index b12c5c20..179fd253 100644 --- a/src/components/dbManager.js +++ b/src/components/dbManager.js @@ -97,13 +97,18 @@ class DbManager { } } - async getUserDatabase(did, contextName, databaseName) { + async getUserDatabase(did, contextName, databaseName, isHash=false) { const couch = Db.getCouch() const didContextHash = Utils.generateDidContextHash(did, contextName) const didContextDbName = `c${didContextHash}` const db = couch.db.use(didContextDbName) - const id = Utils.generateDatabaseName(did, contextName, databaseName) + let id + if (isHash) { + id = databaseName + } else { + id = Utils.generateDatabaseName(did, contextName, databaseName) + } try { const doc = await db.get(id) @@ -220,7 +225,6 @@ class DbManager { async configurePermissions(did, db, username, contextName, permissions) { try { - console.log(`configurePermissions() START`) permissions = permissions ? permissions : {}; let owner = username; @@ -310,7 +314,6 @@ class DbManager { } } - console.log(`configurePermissions() END`) return true; } catch (err) { console.log(err) diff --git a/src/components/replicationManager.js b/src/components/replicationManager.js index f76ae5ed..c60d7a14 100644 --- a/src/components/replicationManager.js +++ b/src/components/replicationManager.js @@ -1,7 +1,6 @@ import Db from './db.js' import Utils from './utils.js' import DbManager from './dbManager.js'; -import UserManager from './userManager.js'; import AuthManager from './authManager.js'; import Axios from 'axios' import EncryptionUtils from '@verida/encryption-utils'; @@ -9,138 +8,62 @@ import EncryptionUtils from '@verida/encryption-utils'; import dotenv from 'dotenv'; dotenv.config(); -class ReplicationManager { - - /** - * Confirm replication is correctly configured for a given DID and application context. - * - * If a storage node is being added or removed to the application context, it must be the - * last node to have checkReplication called. This ensures the node has a list of all the - * active databases and can ensure it is replicating correctly to the other nodes. - * - * The client SDK should call checkReplication() when opening a context to ensure the replication is working as expected. - * - * This is called very often, so needs to be efficient - * - * @param {*} did - * @param {*} contextName - * @param {*} databaseName (optional) If not specified, checks all databases - */ - async checkReplication(did, contextName, databaseName) { - console.log(`${Utils.serverUri()}: checkReplication(${did}, ${contextName}, ${databaseName})`) - // Lookup DID document and get list of endpoints for this context - let didDocument = await AuthManager.getDidDocument(did) - let didService = didDocument.locateServiceEndpoint(contextName, 'database') - - if (!didService) { - // Service not found, try to fetch the DID document without caching (as it may have been udpated) - didDocument = await AuthManager.getDidDocument(did, true) - didService = didDocument.locateServiceEndpoint(contextName, 'database') - } - - // create a copy of the endpoints as this is cached and we will modify later - // ensure it's hostname only - let endpoints = [] - const serverHostname = (new URL(Utils.serverUri())).hostname - let endpointIndex = -1 - for (let e in didService.serviceEndpoint) { - const url = new URL(didService.serviceEndpoint[e]) - endpoints.push(url) - if (url.hostname == serverHostname) { - endpointIndex = e - } - } - - // Confirm this endpoint is in the list of endpoints - if (endpointIndex === -1) { - //console.log(`${Utils.serverUri()}: Error: Server not a valid endpoint for this DID and context:`) - //console.log(endpoints, endpointIndex) - throw new Error(`Server not a valid endpoint (${serverHostname}) for this DID and context`) - } - - // Remove this endpoint from the list of endpoints to check - endpoints.splice(endpointIndex, 1) - - // Build a list of databases to chck - const userDatabases = await DbManager.getUserDatabases(did, contextName) - - let databases = {} - if (databaseName) { - for (let i in userDatabases) { - const item = userDatabases[i] - if (item.databaseName == databaseName) { - databases[item.databaseName] = item - } - } - - // Only check a single database - if (!Object.keys(databases).length === 0) { - return - } - } else { - // Fetch all databases for this context - for (let i in userDatabases) { - const item = userDatabases[i] - databases[item.databaseName] = item - } - - // Ensure the user database list database is included in the list of databases - const didContextHash = Utils.generateDidContextHash(did, contextName) - const didContextDbName = `c${didContextHash}` - - databases[didContextDbName] = { - did, - contextName, - databaseName: didContextDbName, - databaseHash: didContextDbName - } - } - - // Ensure there is a replication entry for each - const couch = Db.getCouch('internal') - const replicationDb = couch.db.use('_replicator') +function now() { + return Math.floor(Date.now() / 1000) +} - const localAuthBuffer = Buffer.from(`${process.env.DB_REPLICATION_USER}:${process.env.DB_REPLICATION_PASS}`); - const localAuthBase64 = localAuthBuffer.toString('base64') +class ReplicationManager { - console.log(`${Utils.serverUri()}: Checking ${endpoints.length} endpoints and ${Object.keys(databases).length} databases`) + async touchDatabases(did, contextName, databaseHashes) { + //console.log(`${Utils.serverUri()}: touchDatabases(${did}, ${contextName}, ${databaseHashes.length})`) + + // Determine the endpoints this node needs to replicate to + const endpoints = await this.getReplicationEndpoints(did, contextName) + // Touch all the databases for every endpoint for (let e in endpoints) { // create a fake endpoint to have a valid URL // generateReplicatorHash() will strip back to hostname const endpointUri = endpoints[e].origin const replicatorId = Utils.generateReplicatorHash(endpointUri, did, contextName) - const replicatorUsername = Utils.generateReplicaterUsername(endpointUri) + + const touchReplicationEntries = [] // Find all entries that have an issue const brokenReplicationEntries = [] - const missingReplicationEntries = {} let authError = false - for (let d in databases) { - const dbHash = databases[d].databaseHash + for (let d in databaseHashes) { + const dbHash = databaseHashes[d] - // Find any replication errors and handle them nicely + // Find existing replication records try { const replicationStatus = await Db.getReplicationStatus(`${replicatorId}-${dbHash}`) + // Handle replication errors if (!replicationStatus) { - console.error(`${Utils.serverUri()}: ${databases[d].databaseName} missing from ${endpointUri}`) + //console.error(`${Utils.serverUri()}: ${dbHash} missing replication to ${endpointUri}`) // Replication entry not found... Will need to create it - missingReplicationEntries[dbHash] = databases[d] + touchReplicationEntries.push(dbHash) } else if (replicationStatus.state == 'failed' || replicationStatus.state == 'crashing' || replicationStatus.state == 'error') { - console.error(`${Utils.serverUri()}: ${databases[d].databaseName} have invalid state ${replicationStatus.state} from ${endpointUri}`) + console.error(`${Utils.serverUri()}: ${dbHash} has invalid state (${replicationStatus.state}) replicating to ${endpointUri}`) brokenReplicationEntries.push(replicationStatus) - missingReplicationEntries[dbHash] = databases[d] + touchReplicationEntries.push(dbHash) if (replicationStatus.state == 'crashing' && replicationStatus.info.error.match(/replication_auth_error/)) { authError = true } + } else { + // Replication is good, but need to update the touched timestamp + touchReplicationEntries.push(dbHash) } } catch (err) { - console.error(`${Utils.serverUri()}: Unknown error checking replication status of database ${databases[d].databaseName} / ${dbHash}: ${err.message}`) + console.error(`${Utils.serverUri()}: Unknown error checking replication status of database ${dbHash}: ${err.message}`) } } - // Delete broken replication entries and add to missing + // Delete broken replication entries + const couch = Db.getCouch('internal') + const replicationDb = couch.db.use('_replicator') + // @todo: No need as they will be garbage collected? for (let b in brokenReplicationEntries) { const replicationEntry = brokenReplicationEntries[b] console.log(`${Utils.serverUri()}: Replication has issues, deleting entry: ${replicationEntry.doc_id} (${replicationEntry.state})`) @@ -150,62 +73,109 @@ class ReplicationManager { await replicationDb.destroy(replicationRecord._id, replicationRecord._rev) } catch (err) { console.error(`${Utils.serverUri()}: Unable to find and delete replication record (${replicationEntry.doc_id}): ${err.message}`) - delete missingReplicationEntries[replicationEntry.databaseHash] } } - if (Object.keys(missingReplicationEntries).length > 0) { - //console.log(`${Utils.serverUri()}: We had some failed or missing replication entries for endpoint ${endpointUri}, so fetch credentials`) - // force create of new credentials if we have an auth error - const { username, password, couchUri } = await this.fetchReplicaterCredentials(endpointUri, did, contextName, authError) - - // re-add all missing replication entries - for (let m in missingReplicationEntries) { - const replicationEntry = missingReplicationEntries[m] - const dbHash = replicationEntry.databaseHash - - console.log(`${Utils.serverUri()}: Replication record for ${endpointUri} / ${replicationEntry.databaseName} / ${dbHash} is missing... creating.`) - const remoteAuthBuffer = Buffer.from(`${username}:${password}`); - const remoteAuthBase64 = remoteAuthBuffer.toString('base64') - - const replicationRecord = { - _id: `${replicatorId}-${dbHash}`, - user_ctx: { - name: process.env.DB_REPLICATION_USER - }, - source: { - url: `http://localhost:${process.env.DB_PORT_INTERNAL}/${dbHash}`, - headers: { - Authorization: `Basic ${localAuthBase64}` - } - }, - target: { - url: `${couchUri}/${dbHash}`, - headers: { - Authorization: `Basic ${remoteAuthBase64}` - } - }, - create_target: false, - continuous: true, - owner: 'admin' - } + // Create or update all replication entries for the list of database hashes + if (Object.keys(touchReplicationEntries).length > 0) { + await this.createUpdateReplicationEntries(did, contextName, endpointUri, touchReplicationEntries, authError) + } + } + } + + /** + * Create new or update existing replication entry + */ + async createUpdateReplicationEntries(did, contextName, endpointUri, dbHashes, forceCreds = false) { + const { username, password, couchUri } = await this.fetchReplicaterCredentials(endpointUri, did, contextName, forceCreds) + const replicatorId = Utils.generateReplicatorHash(endpointUri, did, contextName) - try { - const result = await DbManager._insertOrUpdate(replicationDb, replicationRecord, replicationRecord._id) - replicationRecord._rev = result.rev - console.log(`${Utils.serverUri()}: Saved replication entry for ${endpointUri} (${replicatorId})`) - } catch (err) { - console.log(`${Utils.serverUri()}: Error saving replication entry for ${endpointUri} (${replicatorId}): ${err.message}`) - throw new Error(`Unable to create replication entry: ${err.message}`) + const remoteAuthBuffer = Buffer.from(`${username}:${password}`); + const remoteAuthBase64 = remoteAuthBuffer.toString('base64') + + const localAuthBuffer = Buffer.from(`${process.env.DB_REPLICATION_USER}:${process.env.DB_REPLICATION_PASS}`); + const localAuthBase64 = localAuthBuffer.toString('base64') + + const couch = Db.getCouch('internal') + const replicationDb = couch.db.use('_replicator') + + for (let d in dbHashes) { + const dbHash = dbHashes[d] + //console.log(`${Utils.serverUri()}: Create / update replication record for ${endpointUri} / ${dbHash}`) + + const replicationRecord = { + _id: `${replicatorId}-${dbHash}`, + user_ctx: { + name: process.env.DB_REPLICATION_USER + }, + source: { + url: `http://localhost:${process.env.DB_PORT_INTERNAL}/${dbHash}`, + headers: { + Authorization: `Basic ${localAuthBase64}` } - } + }, + target: { + url: `${couchUri}/${dbHash}`, + headers: { + Authorization: `Basic ${remoteAuthBase64}` + } + }, + create_target: false, + continuous: true, + owner: 'admin', + expiry: (now() + process.env.REPLICATION_EXPIRY_MINUTES*60) + } + + try { + const result = await DbManager._insertOrUpdate(replicationDb, replicationRecord, replicationRecord._id) + replicationRecord._rev = result.rev + //console.log(`${Utils.serverUri()}: Saved replication entry for ${endpointUri} (${replicatorId})`) + } catch (err) { + console.log(`${Utils.serverUri()}: Error saving replication entry for ${endpointUri} (${replicatorId}): ${err.message}`) + throw new Error(`Unable to create replication entry: ${err.message}`) } } + } + + async getReplicationEndpoints(did, contextName) { + // Lookup DID document and get list of endpoints for this context + let didDocument = await AuthManager.getDidDocument(did) + let didService = didDocument.locateServiceEndpoint(contextName, 'database') - // @todo: Remove any replication entries for deleted databases + if (!didService) { + // Service not found, try to fetch the DID document without caching (as it may have been updated) + didDocument = await AuthManager.getDidDocument(did, true) + didService = didDocument.locateServiceEndpoint(contextName, 'database') + } - // Check user databases are configured correctly - await UserManager.checkDatabases(userDatabases) + if (!didService) { + throw new Error(`Unable to locate service endpoint for this DID and context`) + } + + // create a copy of the endpoints as this is cached and we will modify later + // ensure it's hostname only + let endpoints = [] + const serverHostname = (new URL(Utils.serverUri())).hostname + let endpointIndex = -1 + for (let e in didService.serviceEndpoint) { + const url = new URL(didService.serviceEndpoint[e]) + endpoints.push(url) + if (url.hostname == serverHostname) { + endpointIndex = e + } + } + + // Confirm this endpoint is in the list of endpoints + if (endpointIndex === -1) { + //console.log(`${Utils.serverUri()}: Error: Server not a valid endpoint for this DID and context:`) + //console.log(endpoints, endpointIndex) + throw new Error(`Server not a valid endpoint (${serverHostname}) for this DID and context`) + } + + // Remove this endpoint from the list of endpoints to check + endpoints.splice(endpointIndex, 1) + + return endpoints } /** @@ -225,13 +195,13 @@ class ReplicationManager { const thisReplicaterUsername = Utils.generateReplicaterUsername(Utils.serverUri()) const remoteReplicaterUsername = Utils.generateReplicaterUsername(remoteEndpointUri) - console.log(`${Utils.serverUri()}: Fetching credentials from ${remoteEndpointUri} / ${remoteReplicaterUsername} for this replicator username (${thisEndointUri} / ${thisReplicaterUsername})`) + //console.log(`${Utils.serverUri()}: Fetching credentials from ${remoteEndpointUri} / ${remoteReplicaterUsername} for this replicator username (${thisEndointUri} / ${thisReplicaterUsername}) force = ${force}`) let creds, password try { creds = await replicaterCredsDb.get(remoteReplicaterUsername) password = creds.password - console.log(`${Utils.serverUri()}: Credentials for ${remoteEndpointUri} already existed`) + //console.log(`${Utils.serverUri()}: Credentials for ${remoteEndpointUri} already existed`) } catch (err) { // If credentials aren't found, that's okay we will create them below if (err.error != 'not_found') { @@ -275,7 +245,7 @@ class ReplicationManager { // 5 second timeout timeout: 5000 }) - console.log(`${Utils.serverUri()}: Credentials verified for ${remoteEndpointUri}`) + //console.log(`${Utils.serverUri()}: Credentials verified for ${remoteEndpointUri}`) credsUpdated = updatePassword ? updatePassword : result.result == 'updated' } catch (err) { const message = err.response ? err.response.data.message : err.message @@ -316,7 +286,7 @@ class ReplicationManager { try { const result = await DbManager._insertOrUpdate(replicaterCredsDb, creds, creds._id) - console.log(`${Utils.serverUri()}: Credentials saved for ${remoteEndpointUri} ${result.id}`) + //console.log(`${Utils.serverUri()}: Credentials saved for ${remoteEndpointUri} ${result.id}`) } catch (err) { throw new Error(`Unable to save replicater password : ${err.message} (${remoteEndpointUri})`) } @@ -337,7 +307,7 @@ class ReplicationManager { } async updateReplicationCredentials(username, password, couchUri) { - console.log(`${Utils.serverUri()}: Credentials were updated, so updating all existing replication records for this endpoint ${couchUri} to use the new credentials`) + //console.log(`${Utils.serverUri()}: Credentials were updated, so updating all existing replication records for this endpoint ${couchUri} to use the new credentials`) const remoteAuthBuffer = Buffer.from(`${username}:${password}`); const remoteAuthBase64 = remoteAuthBuffer.toString('base64') @@ -356,7 +326,7 @@ class ReplicationManager { const couch = Db.getCouch('internal') const replicationDb = couch.db.use('_replicator') const replicationEntries = await replicationDb.find(query) - console.log(`${Utils.serverUri()}: Found ${replicationEntries.docs.length}`) + //console.log(`${Utils.serverUri()}: Found ${replicationEntries.docs.length}`) for (let r in replicationEntries.docs) { const replicationEntry = replicationEntries.docs[r] @@ -364,8 +334,36 @@ class ReplicationManager { try { await DbManager._insertOrUpdate(replicationDb, replicationEntry, replicationEntry._id) } catch (err) { - console.log(`${Utils.serverUri()}: Error updating replication credentials for ${couchUri} (${r}): ${err.message}`) + console.error(`${Utils.serverUri()}: Error updating replication credentials for ${couchUri} (${r}): ${err.message}`) + } + } + } + + async clearExpired() { + const couch = Db.getCouch('internal') + const replicationDb = couch.db.use('_replicator') + + // Find all expired replication entries + const query = { + selector: { + expiry: { + '$lt': now() + } + }, + limit: 10 + } + + const expiredReplications = await replicationDb.find(query) + if (expiredReplications.docs.length == 0) { + return + } + + for (let e in expiredReplications.docs) { + const replicationEntry = expiredReplications.docs[e] + if (replicationEntry._id.match(/_design/)) { + continue } + const destroyResult = await replicationDb.destroy(replicationEntry._id, replicationEntry._rev) } } diff --git a/src/controllers/user.js b/src/controllers/user.js index 24b99f6d..1db93987 100644 --- a/src/controllers/user.js +++ b/src/controllers/user.js @@ -69,7 +69,8 @@ class UserController { await DbManager.saveUserDatabase(did, contextName, databaseName, databaseHash, options.permissions) return Utils.signedResponse({ - status: "success" + status: "success", + databaseHash }, res); } } catch (err) { @@ -113,6 +114,61 @@ class UserController { } } + /** + * Ensure replication is running on databases owned by this user + * OR + * A database that is public write + * + * @param {*} req + * @param {*} res + * @returns + */ + async pingDatabases(req, res) { + try { + let did = req.tokenData.did + let contextName = req.tokenData.contextName + const databaseHashes = req.body.databaseHashes + const isWritePublic = req.body.isWritePublic + + if (isWritePublic && databaseHashes.length > 1) { + // If we are expecting to be pinging a public write database + // Ensure we only touch one database to prevent any security issues + // of users spoofing the replication of databases they don't have + // access + databaseHashes = [databaseHashes[0]] + } + + if (isWritePublic) { + // If we have a public write database, then the current user + // isn't the owner. + // As such, need to use the supplied owner `did` and `contextName` + did = req.body.did + contextName = req.body.contextName + + const databaseEntry = await DbManager.getUserDatabase(did, contextName, databaseHashes[0], true) + if (databaseEntry.permissions.write != 'public') { + return res.status(500).send({ + status: "fail", + message: `Invalid permissions to initiate replication for ${databaseHashes[0]}` + }); + } + } + + await ReplicationManager.touchDatabases(did, contextName, databaseHashes) + + return Utils.signedResponse({ + status: "success", + databaseHashes + }, res); + } catch (err) { + console.error(err.message) + return res.status(500).send({ + status: "fail", + message: err.message + }); + } + } + async deleteDatabases(req, res) { const did = req.tokenData.did const contextName = req.tokenData.contextName @@ -282,24 +338,45 @@ class UserController { } } + /** + * This is now deprecated + * + * @todo: Remove + * + * @param {*} req + * @param {*} res + * @returns + */ async checkReplication(req, res) { const did = req.tokenData.did const contextName = req.tokenData.contextName const databaseName = req.body.databaseName - try { - const result = await ReplicationManager.checkReplication(did, contextName, databaseName) + let userDatabases = await DbManager.getUserDatabases(did, contextName) - return Utils.signedResponse({ - status: "success", - result - }, res); - } catch (err) { - return res.status(500).send({ - status: "fail", - message: err.message - }); + if (databaseName) { + let dbIsValidForUser = false + for (let d in userDatabases) { + if (userDatabases[d].databaseName == databaseName) { + dbIsValidForUser = true + userDatabases = [userDatabases[d]] + break + } + } + if (!dbIsValidForUser) { + return res.status(401).send({ + status: "fail", + message: "Invalid database name" + }); + } } + + await UserManager.checkDatabases(userDatabases) + + return Utils.signedResponse({ + status: "success", + result: {} + }, res); } } diff --git a/src/middleware/garbageCollection.js b/src/middleware/garbageCollection.js new file mode 100644 index 00000000..0b8f2055 --- /dev/null +++ b/src/middleware/garbageCollection.js @@ -0,0 +1,18 @@ +import AuthManager from '../components/authManager'; +import ReplicationManager from '../components/replicationManager' + +function getRandomInt(min, max) { + min = Math.ceil(min); + max = Math.floor(max); + return Math.floor(Math.random() * (max - min) + min); //The maximum is exclusive and the minimum is inclusive +} + +export default async function garbageCollection(req, res, next) { + next() + + const random = getRandomInt(0, 100) + if (random <= (process.env.GC_PERCENT*100)) { + await ReplicationManager.clearExpired() + await AuthManager.clearExpired() + } +} \ No newline at end of file diff --git a/src/routes/private.js b/src/routes/private.js index cb196fda..c2749f24 100644 --- a/src/routes/private.js +++ b/src/routes/private.js @@ -10,6 +10,12 @@ router.post('/user/deleteDatabases', UserController.deleteDatabases); router.post('/user/databases', UserController.databases); router.post('/user/databaseInfo', UserController.databaseInfo); router.post('/user/usage', UserController.usage); + +// ensure databases are replicating +router.post('/user/pingDatabases', UserController.pingDatabases); +router.post('/user/pingDatabase', UserController.pingDatabases); + +// @todo: remove router.post('/user/checkReplication', UserController.checkReplication); export default router; \ No newline at end of file diff --git a/src/server-app.js b/src/server-app.js index ea5229aa..82121e2a 100644 --- a/src/server-app.js +++ b/src/server-app.js @@ -1,16 +1,16 @@ import express from 'express'; import cors from 'cors'; import dotenv from 'dotenv'; -import basicAuth from 'express-basic-auth'; import privateRoutes from './routes/private.js'; import publicRoutes from './routes/public.js'; -import didStorageRoutes from './services/didStorage/routes' +import didStorageRoutes from './services/didStorage/routes.js'; import requestValidator from './middleware/requestValidator.js'; +import garbageCollection from './middleware/garbageCollection.js'; import userManager from './components/userManager.js'; import AuthManager from './components/authManager.js'; -import didUtils from './services/didStorage/utils' +import didUtils from './services/didStorage/utils.js'; dotenv.config(); @@ -26,6 +26,7 @@ let corsConfig = { app.use(cors(corsConfig)); app.use(express.urlencoded({ extended: false })); app.use(express.json()); +app.use(garbageCollection) app.use(didStorageRoutes); app.use(publicRoutes); app.use(requestValidator); diff --git a/src/services/didStorage/controller.js b/src/services/didStorage/controller.js index 6547cbc6..413de7a5 100644 --- a/src/services/didStorage/controller.js +++ b/src/services/didStorage/controller.js @@ -124,7 +124,6 @@ class DidStorage { const signature = req.headers.signature const versionResponse = await Utils.getDidDocument(did, true, false) - if (!versionResponse || !versionResponse.versions || versionResponse.versions.length === 0) { return Utils.error(res, `DID Document not found`) } diff --git a/test/replication.js b/test/replication.js index 8bfe6dc9..899ad528 100644 --- a/test/replication.js +++ b/test/replication.js @@ -15,14 +15,14 @@ import Utils from './utils' import CONFIG from './config' // Enable verbose logging of what the tests are doing -const LOGGING_ENABLED = true +const LOGGING_ENABLED = false // Use a pre-built mnemonic where the first private key is a Verida DID private key // mnemonic with a Verida DID that points to 2x local endpoints //const MNEMONIC = 'pave online install gift glimpse purpose truth loan arm wing west option' -const MNEMONIC = false +//const MNEMONIC = false // 3x devnet endpoints -//const MNEMONIC = 'corn annual wealth busy pigeon kind vacuum fitness awful uncover pony dad' +const MNEMONIC = 'oval nasty choice palm aim phrase destroy slice twelve recall witness night' // Context name to use for the tests const CONTEXT_NAME = 'Verida Test: Storage Node Replication' @@ -38,8 +38,9 @@ const CONTEXT_NAME = 'Verida Test: Storage Node Replication' 'https://acacia-dev3.tn.verida.tech:443': 'https://admin:ZVOyBzwLxlmTTOQx25mA@acacia-dev3.tn.verida.tech:443', }*/ const ENDPOINT_DSN = { - 'http://192.168.68.124:5000': 'http://admin:admin@192.168.68.124:5984', -// 'http://192.168.68.115:5000': 'http://admin:admin@192.168.68.115:5984', + 'http://192.168.68.135:5000': 'http://admin:admin@192.168.68.135:5984', + 'http://192.168.68.127:5000': 'http://admin:admin@192.168.68.127:5984', + 'http://192.168.68.113:5000': 'http://admin:admin@192.168.68.113:5984', } const ENDPOINTS = Object.keys(ENDPOINT_DSN) const ENDPOINTS_DID = ENDPOINTS.map(item => `${item}/did/`) @@ -68,6 +69,8 @@ function log(output) { } } +const databaseHashes = {} + /** * WARNING: ONLY RUN THIS TEST ON LOCALHOST * @@ -102,12 +105,11 @@ describe("Replication tests", function() { DID_PUBLIC_KEY = wallet.publicKey DID_PRIVATE_KEY = wallet.privateKey keyring = new Keyring(wallet.mnemonic.phrase) - console.log(ENDPOINTS_DID) await didClient.authenticate(DID_PRIVATE_KEY, 'web3', CONFIG.DID_CLIENT_CONFIG.web3Config, ENDPOINTS_DID) TEST_DATABASE_HASH = TEST_DATABASES.map(item => ComponentUtils.generateDatabaseName(DID, CONTEXT_NAME, item)) - log(DID_ADDRESS, DID, DID_PRIVATE_KEY, DID_PUBLIC_KEY, wallet.mnemonic.phrase) + console.log(DID_ADDRESS, DID, DID_PRIVATE_KEY, DID_PUBLIC_KEY, wallet.mnemonic.phrase) // Create a new VDA account using our test endpoints account = new AutoAccount({ @@ -148,7 +150,7 @@ describe("Replication tests", function() { try { log ('Saving DID document') - const endpointResponses = await didClient.save(doc) + await didClient.save(doc) } catch (err) { log(err) log(didClient.getLastEndpointErrors()) @@ -188,76 +190,72 @@ describe("Replication tests", function() { const dbName = TEST_DATABASES[i] const response = await Utils.createDatabase(dbName, DID, CONTEXT_NAME, AUTH_TOKENS[endpoint], endpoint) assert.equal(response.data.status, 'success', 'database created') + databaseHashes[dbName] = response.data.databaseHash } } }) // Call `checkReplication(db1)` on all the endpoints (first database only) - it('can initialise replication for one database via checkReplication()', async () => { + it('can initialise replication for one database via pingDatabases()', async () => { // @todo: fix code so endpoint doesn't create replication entries to itself - try { - for (let i in ENDPOINTS) { - const endpoint = ENDPOINTS[i] - log(`${endpoint}: Calling checkReplication() on for ${TEST_DATABASES[0]}`) - const result = await Utils.checkReplication(endpoint, AUTH_TOKENS[endpoint], TEST_DATABASES[0]) - assert.equal(result.data.status, 'success', 'Check replication completed successfully') - } - - // Sleep 5ms to have replication time to initialise - log('Sleeping so replication has time to do its thing...') - await Utils.sleep(5000) + for (let i in ENDPOINTS) { + const endpoint = ENDPOINTS[i] + log(`${endpoint}: Calling pingDatabases() on for ${TEST_DATABASES[0]} (${databaseHashes[TEST_DATABASES[0]]})`) + const result = await Utils.pingDatabases(endpoint, AUTH_TOKENS[endpoint], databaseHashes[TEST_DATABASES[0]]) + assert.equal(result.data.status, 'success', 'Check replication completed successfully') + } - for (let i in ENDPOINTS) { - const endpoint = ENDPOINTS[i] - const couch = new CouchDb({ - url: ENDPOINT_DSN[endpoint], - requestDefaults: { - rejectUnauthorized: process.env.DB_REJECT_UNAUTHORIZED_SSL.toLowerCase() !== "false" - } - }) - const conn = couch.db.use('_replicator') + // Sleep 5ms to have replication time to initialise + log('Sleeping so replication has time to do its thing...') + await Utils.sleep(5000) - // Check replications entries have been created for all the other endpoints (but not this endpoint) - for (let e in ENDPOINTS) { - const endpointCheckUri = ENDPOINTS[e] - if (endpointCheckUri == endpoint) { - continue - } + for (let i in ENDPOINTS) { + const endpoint = ENDPOINTS[i] + const couch = new CouchDb({ + url: ENDPOINT_DSN[endpoint], + requestDefaults: { + rejectUnauthorized: process.env.DB_REJECT_UNAUTHORIZED_SSL.toLowerCase() !== "false" + } + }) + const conn = couch.db.use('_replicator') - const replicatorId = ComponentUtils.generateReplicatorHash(endpointCheckUri, DID, CONTEXT_NAME) - const dbHash = ComponentUtils.generateDatabaseName(DID, CONTEXT_NAME, TEST_DATABASES[0]) - log(`${endpoint}: (${endpointCheckUri}) Locating _replication entry for ${TEST_DATABASES[0]} (${replicatorId}-${dbHash})`) - - let replicationEntry - try { - replicationEntry = await conn.get(`${replicatorId}-${dbHash}`) - } catch (err) { - log('pouchdb connection error') - log(err.message) - assert.fail(`Replication record not created (${replicatorId}-${dbHash})`) - } + // Check replications entries have been created for all the other endpoints (but not this endpoint) + for (let e in ENDPOINTS) { + const endpointCheckUri = ENDPOINTS[e] + if (endpointCheckUri == endpoint) { + continue + } - // Check info is accurate - assert.ok(replicationEntry) - assert.ok(replicationEntry.source, `Have a source for ${endpointCheckUri}`) - assert.ok(replicationEntry.target, `Have a target for ${endpointCheckUri}`) - assert.equal(replicationEntry.source.url, `http://localhost:5984/${dbHash}`, `Source URI is correct for ${endpointCheckUri}`) - assert.equal(replicationEntry.target.url, `${ENDPOINTS_COUCH[endpointCheckUri]}/${dbHash}`, `Destination URI is correct for ${endpointCheckUri}`) + const replicatorId = ComponentUtils.generateReplicatorHash(endpointCheckUri, DID, CONTEXT_NAME) + const dbHash = databaseHashes[TEST_DATABASES[0]] + log(`${endpoint}: (${endpointCheckUri}) Locating _replication entry for ${TEST_DATABASES[0]} (${replicatorId}-${dbHash})`) - if (!REPLICATOR_CREDS[endpointCheckUri]) { - REPLICATOR_CREDS[endpointCheckUri] = replicationEntry.target.headers - } + let replicationEntry + try { + replicationEntry = await conn.get(`${replicatorId}-${dbHash}`) + } catch (err) { + log('pouchdb connection error') + log(err.message) + assert.fail(`Replication record not created (${replicatorId}-${dbHash})`) + } - const replicationResponse = await Axios.get(`${ENDPOINT_DSN[endpoint]}/_scheduler/docs/_replicator/${replicatorId}-${dbHash}`) - assert.ok(replicationResponse, 'Have a replication job') + // Check info is accurate + assert.ok(replicationEntry) + assert.ok(replicationEntry.source, `Have a source for ${endpointCheckUri}`) + assert.ok(replicationEntry.target, `Have a target for ${endpointCheckUri}`) + assert.equal(replicationEntry.source.url, `http://localhost:5984/${dbHash}`, `Source URI is correct for ${endpointCheckUri}`) + assert.equal(replicationEntry.target.url, `${ENDPOINTS_COUCH[endpointCheckUri]}/${dbHash}`, `Destination URI is correct for ${endpointCheckUri}`) - const status = replicationResponse.data - assert.ok(['pending', 'running'].indexOf(status.state) !== -1, 'Replication is active') + if (!REPLICATOR_CREDS[endpointCheckUri]) { + REPLICATOR_CREDS[endpointCheckUri] = replicationEntry.target.headers } + + const replicationResponse = await Axios.get(`${ENDPOINT_DSN[endpoint]}/_scheduler/docs/_replicator/${replicatorId}-${dbHash}`) + assert.ok(replicationResponse, 'Have a replication job') + + const status = replicationResponse.data + assert.ok(['pending', 'running'].indexOf(status.state) !== -1, 'Replication is active') } - } catch (err) { - log(err) - assert.fail('error') } }) @@ -305,12 +303,15 @@ describe("Replication tests", function() { } }) - it('can initialise replication for all database via checkReplication()', async () => { + it('can initialise replication for all endpoints and databases via pingDatabases()', async () => { for (let i in ENDPOINTS) { const endpoint = ENDPOINTS[i] log(`${endpoint}: Calling checkReplication() on all databases for ${endpoint}`) - const result = await Utils.checkReplication(endpoint, AUTH_TOKENS[endpoint]) - assert.equal(result.data.status, 'success', 'Check replication completed successfully') + + for (let d in TEST_DATABASES) { + const result = await Utils.pingDatabases(endpoint, AUTH_TOKENS[endpoint], databaseHashes[TEST_DATABASES[d]]) + assert.equal(result.data.status, 'success', `pingDatabases completed successfully for ${TEST_DATABASES[d]}`) + } } }) @@ -361,7 +362,7 @@ describe("Replication tests", function() { for (let j in createdDatabaseIds) { const createdId = createdDatabaseIds[j] const result = await conn.get(createdId) - assert.equal(result._id, createdId, 'Record deleted') + assert.equal(result._id, createdId, 'Record exists') } } } catch (err) { @@ -371,7 +372,7 @@ describe("Replication tests", function() { } }) - it('verify non-replicated database is fixed with checkReplication()', async () => { + it('verify non-replicated database is fixed with pingDatabases()', async () => { // manually delete the database replication entry from endpoint 1 to endpoint 2 const endpoint1 = ENDPOINTS[0] const endpoint2 = ENDPOINTS[1] @@ -390,8 +391,8 @@ describe("Replication tests", function() { assert.fail(`Replication record not found (${replicatorId}-${dbHash})`) } - // call checkReplication() on endpoint 1 - const result = await Utils.checkReplication(endpoint1, AUTH_TOKENS[endpoint1], TEST_DATABASES[0]) + // call pingDatabases() on endpoint 1 + const result = await Utils.pingDatabases(endpoint1, AUTH_TOKENS[endpoint1], Object.values(databaseHashes)) assert.equal(result.data.status, 'success', 'checkReplication() success') // verify the replication entry exists and is valid @@ -427,6 +428,7 @@ describe("Replication tests", function() { }) // Do it again, but without specifying the database + // @todo it('verify missing database is correctly created with checkReplication()', async () => { // manually delete the database from endpoint 1 const endpoint1 = ENDPOINTS[0] @@ -454,7 +456,7 @@ describe("Replication tests", function() { // @todo inject a fake database into storage node 1, call checkReplication() on storage node 2, make sure it's not created - it('verify deleted database is correctly removed with checkReplication()', async () => { + it.skip('verify deleted database is correctly removed with checkReplication()', async () => { // @todo }) @@ -520,6 +522,7 @@ describe("Replication tests", function() { } }) + // @todo it('verify user database list is being replicated', async () => { for (let e in ENDPOINTS) { const endpoint = ENDPOINTS[e] @@ -554,7 +557,6 @@ describe("Replication tests", function() { // WARNING: This should never run on production! this.afterAll(async () => { - //return log('Destroying _replicator, verida_replicater_creds and test databases on ALL endpoints') for (let endpoint in ENDPOINT_DSN) { @@ -572,7 +574,16 @@ describe("Replication tests", function() { try { await conn.db.destroy('verida_replicater_creds') } catch (err) {} + + // Create _replicator database and index await conn.db.create('_replicator') + const expiryIndex = { + index: { fields: ['expiry'] }, + name: 'expiry' + }; + const replicatorDb = conn.db.use('_replicator'); + await replicatorDb.createIndex(expiryIndex); + await conn.db.create('verida_replicater_creds') // Delete test databases diff --git a/test/utils.js b/test/utils.js index e4dccbed..2536ee8b 100644 --- a/test/utils.js +++ b/test/utils.js @@ -117,6 +117,22 @@ class Utils { return response } + async pingDatabases(endpointUri, accessToken, databaseHashes) { + if (typeof(databaseHashes) === 'string') { + databaseHashes = [databaseHashes] + } + + const response = await Axios.post(`${endpointUri}/user/pingDatabases`, { + databaseHashes + }, { + headers: { + Authorization: `Bearer ${accessToken}` + } + }); + + return response + } + signString(str, privateKey) { if (privateKey == 'string') { privateKey = new Uint8Array(Buffer.from(privateKey.substr(2),'hex')) diff --git a/yarn.lock b/yarn.lock index a6c4880d..e1b230db 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1771,6 +1771,20 @@ deepcopy "^2.1.0" ethers "^5.5.1" +"@verida/did-client@^2.0.5": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@verida/did-client/-/did-client-2.0.5.tgz#bc58d99263d91ebe09b6123301dc5cc2a035340c" + integrity sha512-YuLQhXaYYY730HKMa+yyfD7l4Wwedztnr5WuBkIz3W1gjfPKwcGWPOJqhdGxvnhb1Kmrc1yhBjMSmbWtmRwY2A== + dependencies: + "@verida/did-document" "^2.0.5" + "@verida/types" "^2.0.5" + "@verida/vda-did-resolver" "^2.0.5" + "@verida/web3" "^2.0.5" + axios "^0.23.0" + deepcopy "^2.1.0" + did-resolver "^4.0.1" + ethers "^5.5.1" + "@verida/did-document@^2.0.0-rc4": version "2.0.0-rc4" resolved "https://registry.yarnpkg.com/@verida/did-document/-/did-document-2.0.0-rc4.tgz#59a4a758d3a04b0d829d0ba84acf94c1062c80a6" @@ -1785,6 +1799,21 @@ ethers "^5.7.2" lodash "^4.17.21" +"@verida/did-document@^2.0.5": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@verida/did-document/-/did-document-2.0.5.tgz#b2945aaefe6af3944b55d412625c38e13350c9d1" + integrity sha512-CKEUiDrjeKDo351cjx1wAvqtA/5M2HFSIPQ8z9RvpaEQ4gXpvZiaoo2tDTJfzMWKRX/LdYlWBMZrQMTLqzZvcw== + dependencies: + "@ethersproject/address" "^5.7.0" + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/transactions" "^5.7.0" + "@verida/encryption-utils" "^2.0.4" + "@verida/keyring" "^2.0.5" + "@verida/types" "^2.0.5" + did-resolver "^4.0.1" + ethers "^5.7.2" + lodash "^4.17.21" + "@verida/encryption-utils@^2.0.0-rc1": version "2.0.0-rc1" resolved "https://registry.yarnpkg.com/@verida/encryption-utils/-/encryption-utils-2.0.0-rc1.tgz#ad1eb6c5001d59930f87b2d7c0272cfe1b8c3196" @@ -1794,10 +1823,10 @@ tweetnacl "^1.0.3" tweetnacl-util "^0.15.1" -"@verida/encryption-utils@^2.0.0-rc5": - version "2.0.0-rc5" - resolved "https://registry.yarnpkg.com/@verida/encryption-utils/-/encryption-utils-2.0.0-rc5.tgz#00d387e17fcdabfed937e8949d83fe3497c42b10" - integrity sha512-YGOdkBxJ6FBdsdLpzYXRstUVqNZ1Rwb5Jzc18+Zyw6bOYM4AYtN5l+MNiMR4lUuoiQT7uU5Gmq/Btjb6HyrzDQ== +"@verida/encryption-utils@^2.0.4": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@verida/encryption-utils/-/encryption-utils-2.0.4.tgz#10b3a8547f6608e21302d701a40c4ecf88d1d1e5" + integrity sha512-ISkf8cDnC/4Nw/QVMTdrt7KKjeTayqVD888wEhGbd3PRXDHXcJ/imTzsXEVwfS18nmtgkD//dpKyuQP9g5+21A== dependencies: ethers "^5.5.1" json.sortify "^2.2.2" @@ -1814,6 +1843,17 @@ tweetnacl "^1.0.3" uuid "^8.3.2" +"@verida/keyring@^2.0.5": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@verida/keyring/-/keyring-2.0.5.tgz#3b9f331bb46161954eef9ca5ec4296f76476c8bc" + integrity sha512-izBYcJuX2B0QxEZf5K65hHHLOX3SFFvxBV2gRMiMgh8nstxm5ATnd1bORQwr1EnYgGZobA1Pyzny2clAtCMDkQ== + dependencies: + "@verida/encryption-utils" "^2.0.4" + "@verida/types" "^2.0.5" + ethers "^5.5.1" + tweetnacl "^1.0.3" + uuid "^8.3.2" + "@verida/storage-link@^2.0.0-rc4": version "2.0.0-rc4" resolved "https://registry.yarnpkg.com/@verida/storage-link/-/storage-link-2.0.0-rc4.tgz#b060f6149a3a16f85fc34c22536760ac0b71e615" @@ -1826,6 +1866,17 @@ did-resolver "^4.0.0" url-parse "^1.5.3" +"@verida/types@^2.0.5": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@verida/types/-/types-2.0.5.tgz#2cbfb4380c186c1a93d8023a1b3e4b3044b7d479" + integrity sha512-kHX52y8bijvPgyNBV8qwLSLTFzYwHXPPc3ei7YRMv8y2pjQikvje5N4JUWx3iNqoUHj5XD/npQuH+JNtOHJ1UA== + dependencies: + "@ethersproject/abstract-signer" "^5.7.0" + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/providers" "^5.7.2" + did-resolver "^4.0.1" + tweetnacl "^1.0.3" + "@verida/vda-did-resolver@^2.0.0-rc4": version "2.0.0-rc4" resolved "https://registry.yarnpkg.com/@verida/vda-did-resolver/-/vda-did-resolver-2.0.0-rc4.tgz#516781f95c60d9ffa2bac087533c455652d452a2" @@ -1838,6 +1889,19 @@ axios "1.2.0-alpha.1" lodash "^4.17.21" +"@verida/vda-did-resolver@^2.0.5": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@verida/vda-did-resolver/-/vda-did-resolver-2.0.5.tgz#e3d1b99523b23b2655fd393198e8bdd8917dac43" + integrity sha512-4T1cMEWmPcMIhYOedsJ5/0Oaz7gYL+JFyJfvoaMNxXVHTc72TEWl8Vp1CjtdY+Z0c338WS8EqTM2q/SasZ94Gg== + dependencies: + "@ethersproject/providers" "^5.7.2" + "@verida/did-document" "^2.0.5" + "@verida/encryption-utils" "^2.0.4" + "@verida/types" "^2.0.5" + "@verida/vda-did" "^2.0.5" + axios "1.2.0-alpha.1" + lodash "^4.17.21" + "@verida/vda-did@^2.0.0-rc4": version "2.0.0-rc4" resolved "https://registry.yarnpkg.com/@verida/vda-did/-/vda-did-2.0.0-rc4.tgz#760a4639f3e982427e399f033ca2c1d6acda2994" @@ -1850,6 +1914,18 @@ axios "1.2.0-alpha.1" lodash "^4.17.21" +"@verida/vda-did@^2.0.5": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@verida/vda-did/-/vda-did-2.0.5.tgz#64376a24b07c5cbd824699636c6bffa05d67923a" + integrity sha512-cjWTnJx05Y8MzEX8bsZlWkYnYLNNQyQhilCoy8ieAYpw4HO0AilfDUH/KAN2MTHRim+x4phTtMOdVY+GEL3dmw== + dependencies: + "@verida/did-document" "^2.0.5" + "@verida/encryption-utils" "^2.0.4" + "@verida/types" "^2.0.5" + "@verida/web3" "^2.0.5" + axios "1.2.0-alpha.1" + lodash "^4.17.21" + "@verida/web3@^2.0.0-rc4": version "2.0.0-rc4" resolved "https://registry.yarnpkg.com/@verida/web3/-/web3-2.0.0-rc4.tgz#1c58fb4e3480e644e54e0c8bda538fa1b6a958fb" @@ -1858,6 +1934,14 @@ axios "^0.27.2" ethers "^5.7.0" +"@verida/web3@^2.0.5": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@verida/web3/-/web3-2.0.5.tgz#18940d74e30fcca2a09666d96e1f1018d7261040" + integrity sha512-2Dx6Sql75KoFsjKy20x1KVjoxQnbmNmas2CewPO3r+Na7vT3Krq8fdZfl16NSnnsUpAzkQxKLGW1+CbTfNYNYA== + dependencies: + axios "^1.2.3" + ethers "^5.7.0" + abbrev@1: version "1.1.1" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" @@ -2135,6 +2219,15 @@ axios@^1.2.1: form-data "^4.0.0" proxy-from-env "^1.1.0" +axios@^1.2.3: + version "1.3.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.3.2.tgz#7ac517f0fa3ec46e0e636223fd973713a09c72b3" + integrity sha512-1M3O703bYqYuPhbHeya5bnhpYVsDDRyQSabNja04mZtboLNSuZ4YrltestrLXfHgmzua4TpUqRiVKbiQuo2epw== + dependencies: + follow-redirects "^1.15.0" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + babel-plugin-polyfill-corejs2@^0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.2.tgz#e4c31d4c89b56f3cf85b92558954c66b54bd972d"