diff --git a/declarations.d.ts b/declarations.d.ts index 32277e44..29ac117a 100644 --- a/declarations.d.ts +++ b/declarations.d.ts @@ -392,7 +392,7 @@ declare module 'hypercore' { sign?: (message: Uint8Array) => Uint8Array; verify: (message: Uint8Array, signature: Uint8Array) => boolean; } - export = class Hypercore extends EventEmitter<'close'> { + class Hypercore extends EventEmitter<'close'> { constructor(storage: any, key?: Opts | Uint8Array, opts?: Opts); length: number; @@ -443,6 +443,8 @@ Populated after ready has been emitted. Will be null before the event. sessions: Hypercore[]; }; + + export = Hypercore } // file://./node_modules/hyperblobs/index.js diff --git a/package-lock.json b/package-lock.json index a3f7310c..e8151691 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2676,6 +2676,18 @@ "resolved": "packages/slashtag", "link": true }, + "node_modules/@synonymdev/slashtags-auth": { + "version": "1.0.0-alpha.6", + "resolved": "https://registry.npmjs.org/@synonymdev/slashtags-auth/-/slashtags-auth-1.0.0-alpha.6.tgz", + "integrity": "sha512-cIIvcDuHN+c++NrXxp77Nh+Rf301TWzYeICTb9DkN+vcPMcnus9oYAp8K/MLdHF7HsCa5QDwx2flsHiNqDu0qw==", + "dev": true, + "dependencies": { + "@synonymdev/slashtags-rpc": "^1.0.0-alpha.2", + "@synonymdev/slashtags-url": "^1.0.0-alpha.1", + "compact-encoding": "^2.11.0", + "compact-encoding-struct": "^1.3.0" + } + }, "node_modules/@synonymdev/slashtags-cli": { "resolved": "packages/cli", "link": true @@ -3024,9 +3036,9 @@ "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==" }, "node_modules/acorn": { - "version": "8.8.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", - "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.9.0.tgz", + "integrity": "sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ==", "bin": { "acorn": "bin/acorn" }, @@ -4192,6 +4204,12 @@ "compact-encoding": "^2.4.1" } }, + "node_modules/compact-encoding-struct": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/compact-encoding-struct/-/compact-encoding-struct-1.3.0.tgz", + "integrity": "sha512-8wgarWCGjtTQlpLu7ebVeeW0hEterei/D8MoIASBiZIUKV2RtzEaViAK+EMemOOOaHQFbp6yRH/k4Q2I94Qpxg==", + "dev": true + }, "node_modules/compare-func": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", @@ -7126,9 +7144,9 @@ } }, "node_modules/hypercore": { - "version": "10.15.1", - "resolved": "https://registry.npmjs.org/hypercore/-/hypercore-10.15.1.tgz", - "integrity": "sha512-TYCn4qnTQBQbiBGqYWJu1ca5cXuObtu/csvAkIN73/mJ7FeQN9LbU8fOeWPqldKimU/NpPCMy2zOnC/dKohHmg==", + "version": "10.16.1", + "resolved": "https://registry.npmjs.org/hypercore/-/hypercore-10.16.1.tgz", + "integrity": "sha512-0WeaZ9Cu6YRBoEiCitj8usKnhFvYvovztSziPcBTSpzNrdUNy8nQz/aMRYiyw6siu2FDpjInJxH1us0EIPMnGQ==" "dependencies": { "@hyperswarm/secret-stream": "^6.0.0", "b4a": "^1.1.0", @@ -8779,41 +8797,25 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/map-obj": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", - "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", + "node_modules/make-fetch-happen/node_modules/@npmcli/fs": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.0.tgz", + "integrity": "sha512-7kZUAaLscfgbwBQRbvdMYaZOWyMEcPTH/tJjnyAWJ/dvvs9Ef+CERx/qJb9GExJpl1qipaDGn7KqHnFGGixd0w==", "dev": true, - "engines": { - "node": ">=8" + "dependencies": { + "semver": "^7.3.5" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/meow": { - "version": "8.1.2", - "resolved": "https://registry.npmjs.org/meow/-/meow-8.1.2.tgz", - "integrity": "sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q==", + "node_modules/make-fetch-happen/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, "dependencies": { - "@types/minimist": "^1.2.0", - "camelcase-keys": "^6.2.2", - "decamelize-keys": "^1.1.0", - "hard-rejection": "^2.1.0", - "minimist-options": "4.1.0", - "normalize-package-data": "^3.0.0", - "read-pkg-up": "^7.0.1", - "redent": "^3.0.0", - "trim-newlines": "^3.0.0", - "type-fest": "^0.18.0", - "yargs-parser": "^20.2.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "balanced-match": "^1.0.0" } }, "node_modules/meow/node_modules/hosted-git-info": { @@ -12151,9 +12153,9 @@ "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==" }, "node_modules/semver": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz", - "integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==", + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.2.tgz", + "integrity": "sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==", "dependencies": { "lru-cache": "^6.0.0" }, @@ -13988,18 +13990,12 @@ "name": "@synonymdev/slashdrive", "version": "1.0.0-alpha.21", "license": "MIT", - "dependencies": { - "b4a": "^1.6.0", - "corestore": "^6.2.1", - "hyperbee": "^2.0.1", - "hyperdrive": "^11.0.0-alpha.5", - "safety-catch": "^1.0.2" - }, "devDependencies": { + "@synonymdev/slashtags-core-data": "^1.0.0-alpha.9", + "b4a": "^1.6.4", "brittle": "^3.0.2", "depcheck": "^1.4.3", "hypercore-crypto": "^3.3.0", - "random-access-memory": "^5.0.1", "standard": "^17.0.0", "typescript": "^4.8.2" } @@ -14017,13 +14013,13 @@ }, "devDependencies": { "@hyperswarm/testnet": "^3.1.0", - "@synonymdev/slashtag": "^1.0.0-alpha.10", + "@synonymdev/slashtag": "^1.0.0-alpha.27", + "@synonymdev/slashtags-auth": "^1.0.0-alpha.6", "brittle": "^3.0.2", "compact-encoding": "^2.11.0", "depcheck": "^1.4.3", "standard": "^17.0.0", - "typescript": "^4.8.2", - "z32": "^1.0.0" + "typescript": "^4.8.2" } }, "packages/sdk": { @@ -14063,10 +14059,8 @@ "@synonymdev/slashtags-core-data": "^1.0.0-alpha.6", "@synonymdev/slashtags-profile": "^1.0.0-alpha.2", "@synonymdev/slashtags-url": "^1.0.0-alpha.3", - "corestore": "^6.2.1", "hyperdht": "^6.5.2", "hyperswarm": "^4.4.1", - "random-access-memory": "^5.0.1", "turbo-hash-map": "^1.0.3" }, "devDependencies": { diff --git a/packages/drive/README.md b/packages/drive/README.md index 7574b5c1..c73e6ffa 100644 --- a/packages/drive/README.md +++ b/packages/drive/README.md @@ -2,55 +2,3 @@ [DEPRECATED] use `@synonymdev/slashtags-core-data` instead. -Drivestore is a Hyperdrive factory that makes it easier to manage large collections of named Hyperdrives. - -## Features - -- Public unencrypted drive -- Creates and keep track of all created encrypted private drives - -## Installation - -``` -npm install @synonymdev/slashdrive -``` - -## Usage - -```js -import Corestore from 'corestore' -import Drivestore from '@synonymdev/slashdrive' - -const corestore = new Corestore('./corestore_dir') -const store = new Drivestore(corestore, keyPair) - -const publicDrive = store.get('public') // or store.get() - -const privateDrive = store.get('foo') // returns an encrypted Hyperdrive -``` - -## API - -#### `const drivestore = new Drivestore(corestore, keyPair)` - -Create new Drivestore. - -- `corestore` must be an instance of [Corestore](https://github.com/hypercore-protocol/corestore). - - If the instance is a [namespace](https://github.com/hypercore-protocol/corestore#const-store--storenamespacename), the internal corestore will reset its namespace to the `DEFAULT_NAMESPACE` (32 0-bytes). - -- `keyPair` public and secret keys to create the public Hyperdrive, the secret key will be used as the `primaryKey` for the internal corestore. - -#### `await drivestore.ready()` - -Awaits opening metadata hypercore. Useful before [async iterating](#for-await-let-name-of-drivestore) over all created drives. - -#### `const hyperdrive = drivestore.get([name])` - -Returns an encrypted [Hyperdrive](https://github.com/hypercore-protocol/hyperdrive-next) for a given name. - -If `name` is undefined or equal to `/public` it will return a public unencrypted drive, by the same keypair passed to the contsructor. - -#### `const stream = drivestore.replicate(stream)` - -Same as [drivestore.corestore.replicate(stream)](https://github.com/hypercore-protocol/corestore#const-stream--storereplicateoptsorstream) diff --git a/packages/drive/index.js b/packages/drive/index.js index f56c9e75..6b1e48f3 100644 --- a/packages/drive/index.js +++ b/packages/drive/index.js @@ -1,55 +1,25 @@ -const Hyperdrive = require('hyperdrive') -const Hyperbee = require('hyperbee') -const b4a = require('b4a') -const safetyCatch = require('safety-catch') - -const METADATA_KEY = 'slashtags-drivestore-metadata' - class Drivestore { /** - * @param {import('corestore')} corestore - * @param {import('hyperdht').KeyPair} keyPair + * @param {import('@synonymdev/slashtags-core-data')} coreData */ - constructor (corestore, keyPair) { - this.fava = Math.random() - this.keyPair = keyPair - /** @type {import('corestore')} */ - this.corestore = corestore.session({ primaryKey: this.keyPair.secretKey, namespace: null }) - - const metadataCore = this.corestore.get({ - name: METADATA_KEY, - encryptionKey: this.keyPair.secretKey - }) - this._metadata = new Hyperbee(metadataCore, { keyEncoding: 'utf8' }) - this._drives = this._metadata.sub('drives') - - this._opening = this._open().catch(safetyCatch) + constructor (coreData) { + this._coreData = coreData + this.corestore = this._coreData._corestoreSession } - /** @returns {import('hyperbee').Iterator<{name: string}>} */ + /** + * @deprecated + */ [Symbol.asyncIterator] () { - if (!this.opened) return emptyIterator - const iterator = this._drives.createReadStream()[Symbol.asyncIterator]() - return { - async next () { - const node = await iterator.next() - const value = node.value - return { done: node.done, value: value && { name: value.key } } - } - } + return new Error('not supported') } get closed () { - return this.corestore._root._closing - } - - async _open () { - await this._drives.feed.ready() - this.opened = true + return this.corestore._root.closing } ready () { - return this._opening + return this._coreData.ready() } /** @param {Parameters} args */ @@ -61,53 +31,8 @@ class Drivestore { * Get a Hyperdrive by its name. */ get (name = 'public') { - validateName(name) - const ns = this.corestore.namespace(name).session({ primaryKey: this.keyPair.secretKey }) - const _preload = ns._preload.bind(ns) - ns._preload = (opts) => this._preload.bind(this)(opts, _preload, ns, name) - return new Hyperdrive(ns) + return this._coreData._getLocalDrive(name) } - - /** - * Set the correct and current key and encryption Key (enables future key rotation) - * @param {Parameters[0]} opts - * @param {*} preload orginal ns._preload - * @param {import('corestore')} ns - * @param {string} name - * @returns {Promise} - */ - async _preload (opts, preload, ns, name) { - const isPublic = name === 'public' - - // Get keyPair programatically from name - const { from } = await preload(opts) - - // public drive needs no encryption - // No need currently to save a record about the public drive - if (isPublic) { - if (opts.name !== 'db') return { from } - const session = this.corestore.get({ keyPair: this.keyPair }) - await session.ready() - return { from: session } - } - - this._drives.ready().then(async () => { - const saved = await this._drives.get(name) - if (!saved) await this._drives.put(name, b4a.from('')) - // TODO enable key rotation, where we overwrite keys, or use saved ones. - // TODO block closing drivestore before this update is flushed - }) - - // Add encryption keys for non public drives - return { from, encryptionKey: ns._namespace } - } -} - -/** @param {string} name */ -function validateName (name) { - if (!/^[0-9a-zA-Z-._ ]*$/.test(name)) throw new Error('Invalid drive name') } -const emptyIterator = { async next () { return { done: true, value: null } } } - module.exports = Drivestore diff --git a/packages/drive/package.json b/packages/drive/package.json index ad219a07..f98fe7a4 100644 --- a/packages/drive/package.json +++ b/packages/drive/package.json @@ -28,18 +28,12 @@ "types", "!**/*.tsbuildinfo" ], - "dependencies": { - "b4a": "^1.6.0", - "corestore": "^6.2.1", - "hyperbee": "^2.0.1", - "hyperdrive": "^11.0.0-alpha.5", - "safety-catch": "^1.0.2" - }, "devDependencies": { + "@synonymdev/slashtags-core-data": "^1.0.0-alpha.9", + "b4a": "^1.6.4", "brittle": "^3.0.2", "depcheck": "^1.4.3", "hypercore-crypto": "^3.3.0", - "random-access-memory": "^5.0.1", "standard": "^17.0.0", "typescript": "^4.8.2" } diff --git a/packages/drive/test/get.js b/packages/drive/test/get.js index 7b067b13..570e1f60 100644 --- a/packages/drive/test/get.js +++ b/packages/drive/test/get.js @@ -1,14 +1,14 @@ const test = require('brittle') -const Corestore = require('corestore') const crypto = require('hypercore-crypto') const b4a = require('b4a') +const CoreData = require('@synonymdev/slashtags-core-data') const Drivestore = require('../index.js') -const { tmpdir } = require('./helpers/index.js') test('get - public drive', async (t) => { const keyPair = crypto.keyPair() - const drivestore = new Drivestore(new Corestore(tmpdir()), keyPair) + const coreData = new CoreData({ keyPair }) + const drivestore = new Drivestore(coreData) const publicA = drivestore.get('public') await publicA.ready() @@ -17,8 +17,6 @@ test('get - public drive', async (t) => { const publicB = drivestore.get() await publicB.ready() t.alike(publicB.key, keyPair.publicKey) - - t.not(publicA, publicB, 'should return a session') t.alike(publicA.key, publicB.key, 'same public key') t.absent(publicA.core.encryptionKey, 'do not encrypet public drive') t.absent(publicB.core.encryptionKey, 'do not encrypet public drive') @@ -36,11 +34,14 @@ test('get - public drive', async (t) => { const buf = b4a.from('bar') await publicA.put('foo', buf) t.alike(await publicA.get('foo'), buf) + + coreData.close() }) test('get - private drive basic', async (t) => { const keyPair = crypto.keyPair() - const drivestore = new Drivestore(new Corestore(tmpdir()), keyPair) + const coreData = new CoreData({ keyPair }) + const drivestore = new Drivestore(coreData) const foo = drivestore.get('foo') const bar = drivestore.get('bar') @@ -67,4 +68,6 @@ test('get - private drive basic', async (t) => { t.ok(foo.blobs?.core.writable) t.unlike(foo.blobs?.core.key, foo.key) t.unlike(bar.blobs?.core.key, foo.key) + + coreData.close() }) diff --git a/packages/drive/test/keys.js b/packages/drive/test/keys.js index 65db2cb3..639b193a 100644 --- a/packages/drive/test/keys.js +++ b/packages/drive/test/keys.js @@ -1,51 +1,30 @@ const test = require('brittle') -const Corestore = require('corestore') const crypto = require('hypercore-crypto') -const path = require('path') -const fs = require('fs') +const CoreData = require('@synonymdev/slashtags-core-data') const Drivestore = require('../index.js') -const { tmpdir } = require('./helpers/index.js') - -test('dont store secretKey at rest', async (t) => { - const dir = tmpdir() - const corestore = new Corestore(dir) - await corestore.ready() - - const drivestore = new Drivestore(corestore, crypto.keyPair()) - await drivestore.ready() - - t.ok(drivestore.corestore.primaryKey) - - await corestore.close() - const stored = fs.readFileSync(path.join(dir, 'primary-key')) - t.unlike(stored, drivestore.corestore.primaryKey) -}) test('unique private drives for unique keyPairs', async (t) => { - const dir = tmpdir() - const corestore = new Corestore(dir) - await corestore.ready() - const kp1 = crypto.keyPair() - const ds1 = new Drivestore(corestore, kp1) + const ds1 = new Drivestore(new CoreData({ keyPair: kp1 })) await ds1.ready() const ds1Public = ds1.get() const ds1Private = ds1.get('contacts') const kp2 = crypto.keyPair() - const ds2 = new Drivestore(corestore, kp2) + const ds2 = new Drivestore(new CoreData({ keyPair: kp2 })) await ds2.ready() const ds2Public = ds2.get('contacts') const ds2Private = ds2.get('contacts') await Promise.all([ds1Public, ds2Public, ds1Private, ds2Private].map(d => d.ready())) - t.unlike(ds1.keyPair.secretKey, ds2.keyPair.secretKey) - t.ok(ds1Public.key) t.unlike(ds2Public.key, ds1Public.key) t.ok(ds1Private.key) t.unlike(ds2Private.key, ds1Private.key) + + ds1._coreData.close() + ds2._coreData.close() }) diff --git a/packages/drive/test/lifecycle.js b/packages/drive/test/lifecycle.js deleted file mode 100644 index b18b406a..00000000 --- a/packages/drive/test/lifecycle.js +++ /dev/null @@ -1,92 +0,0 @@ -const test = require('brittle') -const Corestore = require('corestore') -const RAM = require('random-access-memory') -const crypto = require('hypercore-crypto') -const b4a = require('b4a') - -const Drivestore = require('../index.js') -const { tmpdir } = require('./helpers/index.js') - -test('constructor', async (t) => { - const keyPair = crypto.keyPair() - const corestore = new Corestore(RAM) - await corestore.ready() - - const drivestore = new Drivestore(corestore, keyPair) - await drivestore.ready() - - t.alike(drivestore.corestore.primaryKey, keyPair.secretKey) - t.unlike(drivestore.corestore.primaryKey, corestore.primaryKey) - t.ok(drivestore._metadata.feed.encryptionKey) -}) - -test('save metadata on ready', async (t) => { - const drivestore = new Drivestore(new Corestore(RAM), crypto.keyPair()) - - const foo = drivestore.get('foo') - const bar = drivestore.get('bar') - - const listBeforeFlush = [] - for await (const entry of drivestore) { - listBeforeFlush.push(entry?.name) - } - t.alike(listBeforeFlush, []) - - await Promise.all([foo.ready(), bar.ready()]) - - await new Promise(resolve => setImmediate(resolve)) - - const list = [] - for await (const entry of drivestore) { - list.push(entry?.name) - } - t.alike(list, ['bar', 'foo']) -}) - -test('reopen', async (t) => { - const dir = tmpdir() - const corestore = new Corestore(dir) - const drivestore = new Drivestore(corestore, crypto.keyPair()) - - await drivestore.get().ready() - await drivestore.get('foo').ready() - await drivestore.get('bar').ready() - await new Promise(resolve => setTimeout(resolve, 10)) - - await corestore.close() - t.ok(drivestore.closed, 'drivestore.closed true after corestore is closing') - - const reopened = new Drivestore(new Corestore(dir), drivestore.keyPair) - await reopened.ready() - - const list = [] - for await (const entry of reopened) { - list.push(entry?.name) - } - t.alike(list, ['bar', 'foo'], 'reopend metadata require(storage') -}) - -test('multiple drivestores', async (t) => { - const ns = new Corestore(RAM) - await ns.ready() - - const a = new Drivestore(ns, crypto.keyPair()) - await a.ready() - const b = new Drivestore(ns, crypto.keyPair()) - await b.ready() - - t.alike(a.corestore._namespace, b4a.alloc(32)) - t.alike(b.corestore._namespace, b4a.alloc(32)) - t.ok(a.corestore.primaryKey) - t.unlike(a.corestore.primaryKey, b.corestore.primaryKey) - t.unlike(a._metadata.feed.key, b._metadata.feed.key) - - t.pass('does not close corestore in drivestore') -}) - -test('open on closing corestore', async (t) => { - const corestore = new Corestore(RAM) - const drive = new Drivestore(corestore, crypto.keyPair()) - corestore.close() - t.pass(await drive.ready()) -}) diff --git a/packages/drive/test/replicate.js b/packages/drive/test/replicate.js deleted file mode 100644 index 45b610f4..00000000 --- a/packages/drive/test/replicate.js +++ /dev/null @@ -1,70 +0,0 @@ -const test = require('brittle') -const Corestore = require('corestore') -const RAM = require('random-access-memory') -const crypto = require('hypercore-crypto') -const b4a = require('b4a') - -const Drivestore = require('../index.js') - -test('replicate', async (t) => { - const corestore = new Corestore(RAM) - const drivestore = new Drivestore(corestore, crypto.keyPair()) - const remote = new Corestore(RAM) - - const s1 = remote.replicate(true) - s1.pipe(drivestore.replicate(false)).pipe(s1) - - { - const drive = drivestore.get('private') - await drive.put('/foo', b4a.from('bar')) - - const clone = remote.get({ key: drive.key }) - clone.findingPeers() - await clone.update() - t.is(clone.length, 2) - } - - { - const drive = drivestore.get('public') - await drive.put('/foo', b4a.from('bar')) - - const clone = remote.get({ key: drive.key }) - clone.findingPeers() - await clone.update() - t.is(clone.length, 2) - } -}) - -test('replicate through passed corestore', async (t) => { - const corestore = new Corestore(RAM) - const drivestore = new Drivestore(corestore, crypto.keyPair()) - const remote = new Corestore(RAM) - - const s3 = remote.replicate(true) - s3.pipe(corestore.replicate(false)).pipe(s3) - - { - const drive = drivestore.get('private') - await drive.put('/foo', b4a.from('bar')) - - const clone = remote.get({ key: drive.key }) - clone.findingPeers() - await clone.update() - t.is(clone.length, 2) - } - - { - const drive = drivestore.get('public') - await drive.put('/foo', b4a.from('bar')) - - const clone = remote.get({ key: drive.key }) - clone.findingPeers() - await clone.update() - t.is(clone.length, 2) - } -}) - -test('get - validate drive name', (t) => { - const drivestore = new Drivestore(new Corestore(RAM), crypto.keyPair()) - t.exception(() => drivestore.get('foo:'), /Invalid drive name/) -}) diff --git a/packages/rpc/index.js b/packages/rpc/index.js index 23e36b13..4ab07641 100644 --- a/packages/rpc/index.js +++ b/packages/rpc/index.js @@ -25,15 +25,7 @@ class SlashtagsRPC extends EventEmitter { this._allConnections = new HashMap() this._swarm = opts?.swarm - if (this._swarm) { - this._swarm.on('connection', (connection) => { - // Setup a new RPC instance on every connection - // waiting for next tick fixes an obscure bug when using @hyperswarm/dht-relay - setTimeout(() => this.setup(connection), 0) - }) - } else { - this.slashtag.on('connection', this.setup.bind(this)) - } + this._swarm.on('connection', this.setup.bind(this)) } /** @@ -110,17 +102,22 @@ class SlashtagsRPC extends EventEmitter { } /** - * @param {Uint8Array | string} key + * Connect to a peer if not already connected, and return Protomux RPC instance. + * @param {Parameters[0]} key + * @returns {Promise} */ - async _rpcFromSwarm (key) { + async rpc (key) { /** @type {Uint8Array} */ let publicKey + if (typeof key === 'string') { - if (key.startsWith('slash:')) { + if (key.startsWith('slash')) { publicKey = SlashURL.parse(key).key + } else { + publicKey = SlashURL.decode(key) } - publicKey = SlashURL.decode(key) } else { + // @ts-ignore publicKey = key } @@ -154,28 +151,6 @@ class SlashtagsRPC extends EventEmitter { this._swarm.on('connection', onConnection.bind(this)) }) } - - /** - * For backward compatibility. - * - * @param {Uint8Array | string} key - * - * @deprecated use new SlashtagsRPC({swarm}) instead - */ - async _rpcFromSlashtag (key) { - const socket = this.slashtag.connect(key) - await socket.opened - return this.setup(socket) - } - - /** - * Connect to a peer if not already connected, and return Protomux RPC instance. - * @param {Parameters[0]} key - * @returns {Promise} - */ - async rpc (key) { - return this._swarm ? this._rpcFromSwarm(key) : this._rpcFromSlashtag(key) - } } module.exports = SlashtagsRPC diff --git a/packages/rpc/package.json b/packages/rpc/package.json index 4948add8..c72172ed 100644 --- a/packages/rpc/package.json +++ b/packages/rpc/package.json @@ -12,7 +12,7 @@ "build": "tsc", "clean": "rimraf types", "lint": "standard --fix", - "test": "brittle test/*.js -cov", + "test": "brittle test/all.js -cov", "depcheck": "npx depcheck", "fullcheck": "npm run lint && npm run clean && npm run build && npm run test && npm run depcheck", "prepublishOnly": "npm run fullcheck" @@ -37,12 +37,12 @@ }, "devDependencies": { "@hyperswarm/testnet": "^3.1.0", - "@synonymdev/slashtag": "^1.0.0-alpha.10", + "@synonymdev/slashtag": "^1.0.0-alpha.27", + "@synonymdev/slashtags-auth": "^1.0.0-alpha.6", "brittle": "^3.0.2", "compact-encoding": "^2.11.0", "depcheck": "^1.4.3", "standard": "^17.0.0", - "typescript": "^4.8.2", - "z32": "^1.0.0" + "typescript": "^4.8.2" } } diff --git a/packages/rpc/test/all.js b/packages/rpc/test/all.js index f305c956..85ea550c 100644 --- a/packages/rpc/test/all.js +++ b/packages/rpc/test/all.js @@ -1,8 +1,8 @@ const test = require('brittle') -const Slashtag = require('@synonymdev/slashtag') const createTestnet = require('@hyperswarm/testnet') const c = require('compact-encoding') -const z32 = require('z32') +const b4a = require('b4a') +const Hyperswarm = require('hyperswarm') const SlashtagsRPC = require('../index.js') @@ -21,7 +21,7 @@ class Foo extends SlashtagsRPC { /** @param {import('@hyperswarm/secret-stream')} socket */ handshake (socket) { - return this.id + '-handshake:for:' + z32.encode(socket.remotePublicKey) + return this.id + '-handshake:for:' + b4a.toString(socket.remotePublicKey, 'hex') } /** @@ -29,7 +29,7 @@ class Foo extends SlashtagsRPC { * @param {import('@hyperswarm/secret-stream')} socket */ onopen (handshake, socket) { - this.emit('handshake', handshake, z32.encode(socket.remotePublicKey)) + this.emit('handshake', handshake, socket.remotePublicKey) } get methods () { @@ -45,7 +45,7 @@ class Foo extends SlashtagsRPC { /** @param {string} req */ _onEcho (req) { this.emit('echo', req) - return req + return req + '-echoed' } /** @@ -66,31 +66,18 @@ class Bar extends Foo { } } -test('missing id', async t => { - const testnet = await createTestnet(1, t.teardown) - const alice = new Slashtag(testnet) - - class MissingID extends SlashtagsRPC { } - - const instance = new MissingID(alice) - // @ts-ignore - await t.exception(() => instance.setup({}), /id should be defined/) - - alice.close() -}) - -test.skip('slashtag - basic', async t => { +test('hyperswarm - basic', async t => { const testnet = await createTestnet(3, t.teardown) - const alice = new Slashtag(testnet) - const bob = new Slashtag(testnet) + const alice = new Hyperswarm(testnet) + const bob = new Hyperswarm(testnet) - const aliceFoo = new Foo(alice) + const aliceFoo = new Foo({ swarm: alice }) ; (() => { // Multiple instances shouldn't create any issues - return new Foo(alice) + return new Foo({ swarm: alice }) })() - const bobFoo = new Foo(bob) + const bobFoo = new Foo({ swarm: bob }) await alice.listen() @@ -98,56 +85,56 @@ test.skip('slashtag - basic', async t => { ht.plan(4) aliceFoo.on('handshake', (handshake, remoteID) => { - ht.is(handshake, 'foo-handshake:for:' + alice.id, 'correct handshake') - ht.is(remoteID, bob.id, 'emit handshake event correctly') + ht.is(handshake, 'foo-handshake:for:' + b4a.toString(alice.keyPair.publicKey, 'hex'), 'correct handshake') + ht.alike(remoteID, bob.keyPair.publicKey, 'emit handshake event correctly') }) bobFoo.on('handshake', (handshake, remoteID) => { - ht.is(handshake, 'foo-handshake:for:' + bob.id, 'correct handshake') - ht.is(remoteID, alice.id, 'emit handshake event correctly') + ht.is(handshake, 'foo-handshake:for:' + b4a.toString(bob.keyPair.publicKey, 'hex'), 'correct handshake') + ht.alike(remoteID, alice.keyPair.publicKey, 'emit handshake event correctly') }) aliceFoo.once('echo', req => t.is(req, 'foobar')) - t.is(await bobFoo.echo(alice.key, 'foobar'), 'foobar') + t.is(await bobFoo.echo(alice.keyPair.publicKey, 'foobar'), 'foobar-echoed') aliceFoo.once('echo', req => t.is(req, 'foobar 2')) - t.is(await bobFoo.echo(alice.key, 'foobar 2'), 'foobar 2') + t.is(await bobFoo.echo(alice.keyPair.publicKey, 'foobar 2'), 'foobar 2-echoed') await ht - await alice.close() - await bob.close() + await alice.destroy() + await bob.destroy() }) -test.skip('multiple rpcs', async t => { +test('hyperswarm - multiple rpcs', async t => { const testnet = await createTestnet(3, t.teardown) t.plan(6) - const alice = new Slashtag(testnet) - const aliceFoo = new Foo(alice) - const aliceBar = new Bar(alice) + const alice = new Hyperswarm(testnet) + const aliceFoo = new Foo({ swarm: alice }) + const aliceBar = new Bar({ swarm: alice }) - const bob = new Slashtag(testnet) - const bobFoo = new Foo(bob) - const bobBar = new Bar(bob) + const bob = new Hyperswarm(testnet) + const bobFoo = new Foo({ swarm: bob }) + const bobBar = new Bar({ swarm: bob }) await alice.listen() aliceFoo.on('handshake', handshake => - t.is(handshake, 'foo-handshake:for:' + alice.id) + t.is(handshake, 'foo-handshake:for:' + b4a.toString(alice.keyPair.publicKey, 'hex')) ) aliceBar.on('handshake', handshake => - t.is(handshake, 'bar-handshake:for:' + alice.id) + t.is(handshake, 'bar-handshake:for:' + b4a.toString(alice.keyPair.publicKey, 'hex')) ) aliceFoo.once('echo', req => t.is(req, 'foo')) - t.is(await bobFoo.echo(alice.key, 'foo'), 'foo') + t.is(await bobFoo.echo(alice.keyPair.publicKey, 'foo'), 'foo-echoed') aliceBar.once('echo', req => t.is(req, 'bar')) - t.is(await bobBar.echo(alice.key, 'bar'), 'bar') + t.is(await bobBar.echo(alice.keyPair.publicKey, 'bar'), 'bar-echoed') - await alice.close() - await bob.close() + await alice.destroy() + await bob.destroy() }) diff --git a/packages/rpc/test/legacy.js b/packages/rpc/test/legacy.js new file mode 100644 index 00000000..29dca9b3 --- /dev/null +++ b/packages/rpc/test/legacy.js @@ -0,0 +1,24 @@ +const test = require('brittle') +const Slashtag = require('@synonymdev/slashtag') + +// Not in the test suite, but you can run it to verify that it works. `npx brittle test/legacy.js` +test('legacy - talking to older RPCs using Slashtag.connect', async (t) => { + const slashtag = new Slashtag() + + await slashtag.profile.create({ + name: 'RPC test' + }) + + const SlashAuth = await import('@synonymdev/slashtags-auth') + + const client = new SlashAuth.Client(slashtag) + + // Accounts demo from the Playground. https://github.com/synonymdev/slashtags-playground-auth-demo + const url = 'slash:unrreuy3r4qkadioomgrcfonqkf5d1smyegqpod3pp59aqxpruhy' + + const r2 = await client.magiclink(url) + + t.ok(r2.url.startsWith('https://www.synonym.to/products/slashtags/playground/accounts?user=')) + + slashtag.close() +}) diff --git a/packages/rpc/test/swarm.js b/packages/rpc/test/swarm.js deleted file mode 100644 index 85ea550c..00000000 --- a/packages/rpc/test/swarm.js +++ /dev/null @@ -1,140 +0,0 @@ -const test = require('brittle') -const createTestnet = require('@hyperswarm/testnet') -const c = require('compact-encoding') -const b4a = require('b4a') -const Hyperswarm = require('hyperswarm') - -const SlashtagsRPC = require('../index.js') - -class Foo extends SlashtagsRPC { - get id () { - return 'foo' - } - - get valueEncoding () { - return c.string - } - - get handshakeEncoding () { - return c.string - } - - /** @param {import('@hyperswarm/secret-stream')} socket */ - handshake (socket) { - return this.id + '-handshake:for:' + b4a.toString(socket.remotePublicKey, 'hex') - } - - /** - * @param {string} handshake - * @param {import('@hyperswarm/secret-stream')} socket - */ - onopen (handshake, socket) { - this.emit('handshake', handshake, socket.remotePublicKey) - } - - get methods () { - const self = this - return [ - { - name: 'echo', - handler: self._onEcho.bind(self) - } - ] - } - - /** @param {string} req */ - _onEcho (req) { - this.emit('echo', req) - return req + '-echoed' - } - - /** - * - * @param {Uint8Array} key - * @param {string} message - * @returns - */ - async echo (key, message) { - const rpc = await this.rpc(key) - return rpc?.request('echo', message) - } -} - -class Bar extends Foo { - get id () { - return 'bar' - } -} - -test('hyperswarm - basic', async t => { - const testnet = await createTestnet(3, t.teardown) - - const alice = new Hyperswarm(testnet) - const bob = new Hyperswarm(testnet) - - const aliceFoo = new Foo({ swarm: alice }) - ; (() => { - // Multiple instances shouldn't create any issues - return new Foo({ swarm: alice }) - })() - const bobFoo = new Foo({ swarm: bob }) - - await alice.listen() - - const ht = t.test('handshake') - ht.plan(4) - - aliceFoo.on('handshake', (handshake, remoteID) => { - ht.is(handshake, 'foo-handshake:for:' + b4a.toString(alice.keyPair.publicKey, 'hex'), 'correct handshake') - ht.alike(remoteID, bob.keyPair.publicKey, 'emit handshake event correctly') - }) - - bobFoo.on('handshake', (handshake, remoteID) => { - ht.is(handshake, 'foo-handshake:for:' + b4a.toString(bob.keyPair.publicKey, 'hex'), 'correct handshake') - ht.alike(remoteID, alice.keyPair.publicKey, 'emit handshake event correctly') - }) - - aliceFoo.once('echo', req => t.is(req, 'foobar')) - t.is(await bobFoo.echo(alice.keyPair.publicKey, 'foobar'), 'foobar-echoed') - - aliceFoo.once('echo', req => t.is(req, 'foobar 2')) - t.is(await bobFoo.echo(alice.keyPair.publicKey, 'foobar 2'), 'foobar 2-echoed') - - await ht - - await alice.destroy() - await bob.destroy() -}) - -test('hyperswarm - multiple rpcs', async t => { - const testnet = await createTestnet(3, t.teardown) - - t.plan(6) - - const alice = new Hyperswarm(testnet) - const aliceFoo = new Foo({ swarm: alice }) - const aliceBar = new Bar({ swarm: alice }) - - const bob = new Hyperswarm(testnet) - const bobFoo = new Foo({ swarm: bob }) - const bobBar = new Bar({ swarm: bob }) - - await alice.listen() - - aliceFoo.on('handshake', handshake => - t.is(handshake, 'foo-handshake:for:' + b4a.toString(alice.keyPair.publicKey, 'hex')) - ) - - aliceBar.on('handshake', handshake => - t.is(handshake, 'bar-handshake:for:' + b4a.toString(alice.keyPair.publicKey, 'hex')) - ) - - aliceFoo.once('echo', req => t.is(req, 'foo')) - t.is(await bobFoo.echo(alice.keyPair.publicKey, 'foo'), 'foo-echoed') - - aliceBar.once('echo', req => t.is(req, 'bar')) - t.is(await bobBar.echo(alice.keyPair.publicKey, 'bar'), 'bar-echoed') - - await alice.destroy() - await bob.destroy() -}) diff --git a/packages/sdk/index.js b/packages/sdk/index.js index a17a6da4..f033cbc0 100644 --- a/packages/sdk/index.js +++ b/packages/sdk/index.js @@ -140,38 +140,24 @@ class SDK extends EventEmitter { */ drive (key, opts = {}) { if (this.closed) throw new Error('SDK is closed') + // Temporary hack to use CoreData until this API is removed - // Temporary solution to handle encrypted hyperdrives! - // TODO: remove this once Hyperdrive next accept encryption keys - const corestore = this.corestore.session() - - // Disable _preready to avoid 'Stored core key does not match the provided name' error - // TODO: temporary hack to fix when slashtag public drive is opened as readonly first! - if (b4a.equals(this.slashtag().key, key)) { - return this.slashtag().drivestore.get() + if (b4a.equals(key, this.slashtag().key)) { + return this.slashtag().coreData._publicDrive } - if (opts.encryptionKey) { - const preload = this.corestore._preload.bind(this.corestore) - corestore._preload = _preload.bind(corestore) - async function _preload (/** @type {any} */ _opts) { - const { from } = await preload(_opts) - return { from, encryptionKey: opts.encryptionKey } - } - } + const coreData = this.slashtag().coreData - const drive = new Hyperdrive(corestore, key) + const id = SlashURL.encode(key) - // Announce the drive as a client - const discovery = this.join(crypto.discoveryKey(key), { server: false, client: true }) - if (discovery) { - drive.once('close', () => discovery.destroy()) - const done = drive.findingPeers() - this.swarm.flush().then(done, done) - } + const url = + `slash:${id}/#driveKey=${id}` + (opts.encryptionKey ? `&encryptionKey=${SlashURL.encode(opts.encryptionKey)}` : '') + + const parsed = SlashURL.parse(url) + + const name = opts.encryptionKey ? id : 'public' - // TODO read encrypted drives! - return drive + return coreData._getRemoteDrive(parsed, name) } /** diff --git a/packages/sdk/test/drive.js b/packages/sdk/test/drive.js index ad40cc56..a8342278 100644 --- a/packages/sdk/test/drive.js +++ b/packages/sdk/test/drive.js @@ -125,34 +125,6 @@ test('drive - internal hyperdrive', async (t) => { await sdk.close() }) -test('drive - close discovery sessions on closing drive', async (t) => { - const testnet = await createTestnet(3, t.teardown) - - const remote = new SDK({ ...testnet, storage: tmpdir() }) - const clone = remote.drive(b4a.from('69b04ea6e3b62245048a8efe8c17c6affb91e07ea1e28c911c2acdfd4d851f5c', 'hex')) - await clone.update() - - t.is(clone.core.peers.length, 0) - - const sdk = new SDK({ ...testnet, storage: tmpdir(), primaryKey: b4a.from('a'.repeat(64), 'hex') }) - const alice = sdk.slashtag('alice') - const drive = alice.drivestore.get() - await drive.put('/foo', b4a.from('bar')) - - await sdk.swarm.flush() - - for (let i = 0; i < 10; i++) { - const driveSession = remote.drive(alice.key) - await driveSession.close() - } - - const discovery = remote.swarm.status(drive.discoveryKey) - t.is(discovery?._sessions.length, 1, 'closed all discovery sessions after closing drives sessions') - - await remote.close() - await sdk.close() -}) - test('read only created first', async (t) => { const testnet = await createTestnet(3, t.teardown) const dir = tmpdir() diff --git a/packages/slashtag/index.js b/packages/slashtag/index.js index 5de9d32a..707050fc 100644 --- a/packages/slashtag/index.js +++ b/packages/slashtag/index.js @@ -1,5 +1,3 @@ -const Corestore = require('corestore') -const RAM = require('random-access-memory') const EventEmitter = require('events') const DHT = require('hyperdht') const HashMap = require('turbo-hash-map') @@ -33,9 +31,6 @@ class Slashtag extends EventEmitter { /** @type {HashMap} */ this.sockets = new HashMap() - // @deprecated use `slashtag.coreData` instead. - this.drivestore = new Drivestore(opts?.corestore || new Corestore(RAM), this.keyPair) - this.coreData = new SlashtagsCoreData({ keyPair: this.keyPair, corestore: opts.corestore, @@ -44,6 +39,9 @@ class Slashtag extends EventEmitter { }) this.profile = new SlashtagsProfile(this.coreData) + // @deprecated use `slashtag.coreData` instead. + this.drivestore = new Drivestore(this.coreData) + /** @type {Emitter['on']} */ this.on = super.on /** @type {Emitter['on']} */ this.once = super.once /** @type {Emitter['on']} */ this.off = super.off @@ -72,6 +70,9 @@ class Slashtag extends EventEmitter { /** * Connect to a remote Slashtag by its key, z-base-32 id, or `slash:` url. + * + * @deprecated use `slashtags.swarm` with `slashtags-rpc` instead. + * * @param {Uint8Array | string} key * @returns {SecretStream} */ diff --git a/packages/slashtag/package.json b/packages/slashtag/package.json index 0ec544e1..255dd16a 100644 --- a/packages/slashtag/package.json +++ b/packages/slashtag/package.json @@ -33,10 +33,8 @@ "@synonymdev/slashtags-core-data": "^1.0.0-alpha.6", "@synonymdev/slashtags-profile": "^1.0.0-alpha.2", "@synonymdev/slashtags-url": "^1.0.0-alpha.3", - "corestore": "^6.2.1", "hyperdht": "^6.5.2", "hyperswarm": "^4.4.1", - "random-access-memory": "^5.0.1", "turbo-hash-map": "^1.0.3" }, "devDependencies": {