corestore 6.0.0-beta1 → 6.0.1-alpha.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,16 +3,16 @@ name: Test on Node.js
3
3
  on:
4
4
  push:
5
5
  branches:
6
- - master
6
+ - main
7
7
  pull_request:
8
8
  branches:
9
- - master
9
+ - main
10
10
  jobs:
11
11
  build:
12
12
  strategy:
13
13
  matrix:
14
- node-version: [12.x, 14.x]
15
- os: [ubuntu-16.04, macos-latest, windows-latest]
14
+ node-version: [12.x, 14.x, 16.x]
15
+ os: [ubuntu-latest, macos-latest, windows-latest]
16
16
  runs-on: ${{ matrix.os }}
17
17
  steps:
18
18
  - uses: actions/checkout@v2
package/README.md CHANGED
@@ -1,2 +1,59 @@
1
- # neocorestore
2
- A new take on Corestore
1
+ # Corestore v6
2
+
3
+ Corestore is a Hypercore factory that makes it easier to manage large collections of named Hypercores.
4
+
5
+ Corestore provides:
6
+ 1. __Key Derivation__ - All writable Hypercore keys are derived from a single master key and a user-provided name.
7
+ 2. __Session Handling__ - If a single Hypercore is loaded multiple times through the `get` method, the underlying resources will only be opened once (using Hypercore 10's new session feature). Once all sessions are closed, the resources will be released.
8
+ 3. __Storage Management__ - Hypercores can be stored in any random-access-storage instance, where they will be keyed by their discovery keys.
9
+ 4. __Namespacing__ - You can share a single Corestore instance between multiple applications or components without worrying about naming collisions by creating "namespaces" (e.g. `corestore.namespace('my-app').get({ name: 'main' })
10
+
11
+ ### Installation
12
+ `npm install corestore@next`
13
+
14
+ ### Usage
15
+ A corestore instance can be constructed with a random-access-storage module, a function that returns a random-access-storage module given a path, or a string. If a string is specified, it will be assumed to be a path to a local storage directory:
16
+ ```js
17
+ const Corestore = require('corestore')
18
+
19
+ const store = new Corestore('./my-storage')
20
+ const core1 = store.get({ name: 'core-1' })
21
+ const core2 = store.get({ name: 'core-2' })
22
+ ```
23
+
24
+ ### API
25
+ #### `const store = new Corestore(storage)`
26
+ Create a new Corestore instance.
27
+
28
+ `storage` can be either a random-access-storage module, a string, or a function that takes a path and returns an random-access-storage instance.
29
+
30
+ #### `const core = store.get(key | { name: 'a-name', ...hypercoreOpts})`
31
+ Loads a Hypercore, either by name (if the `name` option is provided), or from the provided key (if the first argument is a Buffer, or if the `key` options is set).
32
+
33
+ If that Hypercore has previously been loaded, subsequent calls to `get` will return a new Hypercore session on the existing core.
34
+
35
+ All other options besides `name` and `key` will be forwarded to the Hypercore constructor.
36
+
37
+ #### `const stream = store.replicate(opts)`
38
+ Creates a replication stream that's capable of replicating all Hypercores that are managed by the Corestore, assuming the remote peer has the correct capabilities.
39
+
40
+ `opts` will be forwarded to Hypercore's `replicate` function.
41
+
42
+ Corestore replicates in an "all-to-all" fashion, meaning that when replication begins, it will attempt to replicate every Hypercore that's currently loaded and in memory. These attempts will fail if the remote side doesn't have a Hypercore's capability -- Corestore replication does not exchange Hypercore keys.
43
+
44
+ If the remote side dynamically adds a new Hypercore to the replication stream, Corestore will load and replicatethat core if possible.
45
+
46
+ #### `const store = store.namespace(name)`
47
+ Create a new namespaced Corestore. Namespacing is useful if you're going to be sharing a single Corestore instance between many applications or components, as it prevents name collisions.
48
+
49
+ Namespaces can be chained:
50
+ ```js
51
+ const ns1 = store.namespace('a')
52
+ const ns2 = ns1.namespace('b')
53
+ const core1 = ns1.get({ name: 'main' }) // These will load different Hypercores
54
+ const core2 = ns2.get({ name: 'main' })
55
+ ```
56
+
57
+ ### License
58
+ MIT
59
+
package/index.js CHANGED
@@ -1,122 +1,229 @@
1
- const { NanoresourcePromise: Nanoresource } = require('nanoresource-promise/emitter')
2
- const raf = require('random-access-file')
1
+ const { EventEmitter } = require('events')
2
+ const crypto = require('hypercore-crypto')
3
+ const sodium = require('sodium-universal')
4
+ const Hypercore = require('hypercore')
3
5
 
4
- const Replicator = require('./lib/replicator')
5
- const Index = require('./lib/db')
6
- const Loader = require('./lib/loader')
7
- const errors = require('./lib/errors')
6
+ const KeyManager = require('./lib/keys')
8
7
 
9
- module.exports = class Corestore extends Nanoresource {
8
+ const CORES_DIR = 'cores'
9
+ const PROFILES_DIR = 'profiles'
10
+ const USERDATA_NAME_KEY = '@corestore/name'
11
+ const USERDATA_NAMESPACE_KEY = '@corestore/namespace'
12
+ const DEFAULT_NAMESPACE = generateNamespace('@corestore/default')
13
+
14
+ module.exports = class Corestore extends EventEmitter {
10
15
  constructor (storage, opts = {}) {
11
16
  super()
12
17
 
13
- if (typeof storage === 'string') storage = defaultStorage(storage)
14
- if (typeof storage !== 'function') throw new errors.InvalidStorageError()
15
- this.storage = storage
18
+ this.storage = Hypercore.defaultStorage(storage, { lock: PROFILES_DIR + '/default' })
16
19
 
17
- this._namespace = opts.namespace || []
18
- this._db = opts._db || new Index(this.storage, opts)
19
- this._loader = opts._loader || new Loader(this.storage, this._db, opts)
20
- this._replicator = opts._replicator || new Replicator(this._loader, opts)
20
+ this.cores = opts._cores || new Map()
21
+ this.keys = opts.keys
21
22
 
22
- this._db.on('error', err => this.emit('db-error', err))
23
- this._loader.on('error', err => this.emit('error', err))
24
- this._loader.on('core', (core, opts) => this.emit('core', core, opts))
25
- this._loader.on('core', (core, opts) => this.emit('feed', core, opts))
23
+ this._namespace = opts._namespace || DEFAULT_NAMESPACE
24
+ this._replicationStreams = opts._streams || []
26
25
 
27
- this.ready = this.open.bind(this)
26
+ this._opening = opts._opening ? opts._opening.then(() => this._open()) : this._open()
27
+ this._opening.catch(noop)
28
+ this.ready = () => this._opening
28
29
  }
29
30
 
30
- get cache () {
31
- return this._loader.cache
31
+ async _open () {
32
+ if (this.keys) {
33
+ this.keys = await this.keys // opts.keys can be a Promise that resolves to a KeyManager
34
+ } else {
35
+ this.keys = await KeyManager.fromStorage(p => this.storage(PROFILES_DIR + '/' + p))
36
+ }
32
37
  }
33
38
 
34
- // Nanoresource Methods
39
+ async _generateKeys (opts) {
40
+ if (opts.discoveryKey) {
41
+ return {
42
+ keyPair: null,
43
+ sign: null,
44
+ discoveryKey: opts.discoveryKey
45
+ }
46
+ }
47
+ if (!opts.name) {
48
+ return {
49
+ keyPair: {
50
+ publicKey: opts.publicKey,
51
+ secretKey: opts.secretKey
52
+ },
53
+ sign: opts.sign,
54
+ discoveryKey: crypto.discoveryKey(opts.publicKey)
55
+ }
56
+ }
57
+ const { publicKey, sign } = await this.keys.createHypercoreKeyPair(opts.name, this._namespace)
58
+ return {
59
+ keyPair: {
60
+ publicKey,
61
+ secretKey: null
62
+ },
63
+ sign,
64
+ discoveryKey: crypto.discoveryKey(publicKey)
65
+ }
66
+ }
35
67
 
36
- async _open () {
37
- await this._db.open()
38
- return this._loader.open()
68
+ _getPrereadyUserData (core, key) {
69
+ for (const { key: savedKey, value } of core.core.header.userData) {
70
+ if (key === savedKey) return value
71
+ }
72
+ return null
39
73
  }
40
74
 
41
- async _close () {
42
- await this._replicator.close()
43
- await this._loader.close()
44
- return this._db.close()
75
+ async _preready (core) {
76
+ const name = this._getPrereadyUserData(core, USERDATA_NAME_KEY)
77
+ if (!name) return
78
+
79
+ const namespace = this._getPrereadyUserData(core, USERDATA_NAMESPACE_KEY)
80
+ const { publicKey, sign } = await this.keys.createHypercoreKeyPair(name.toString(), namespace)
81
+ if (!publicKey.equals(core.key)) throw new Error('Stored core key does not match the provided name')
82
+
83
+ // TODO: Should Hypercore expose a helper for this, or should preready return keypair/sign?
84
+ core.sign = sign
85
+ core.key = publicKey
86
+ core.writable = true
45
87
  }
46
88
 
47
- // Private Methods
89
+ async _preload (opts) {
90
+ await this.ready()
91
+
92
+ const { discoveryKey, keyPair, sign } = await this._generateKeys(opts)
93
+ const id = discoveryKey.toString('hex')
48
94
 
49
- _validateGetOptions (opts) {
50
- if (typeof opts === 'object') {
51
- if (!Buffer.isBuffer(opts)) {
52
- if (!opts.key && !opts.name && !opts.keyPair) throw new errors.InvalidOptionsError()
53
- } else {
54
- opts = { key: opts }
95
+ while (this.cores.has(id)) {
96
+ const existing = this.cores.get(id)
97
+ if (existing) {
98
+ if (!existing.closing) return { from: existing, keyPair, sign }
99
+ await existing.close()
55
100
  }
56
- } else {
57
- opts = { key: Buffer.from(opts, 'hex') }
58
101
  }
59
- if (opts.key && typeof opts.key === 'string') opts.key = Buffer.from(opts.key, 'hex')
60
- if (opts.key && opts.key.length !== 32) throw new errors.InvalidKeyError()
61
- if (opts.name && !Array.isArray(opts.name)) opts.name = [opts.name]
62
- return opts
63
- }
64
102
 
65
- // Public Methods
103
+ const userData = {}
104
+ if (opts.name) {
105
+ userData[USERDATA_NAME_KEY] = Buffer.from(opts.name)
106
+ userData[USERDATA_NAMESPACE_KEY] = this._namespace
107
+ }
66
108
 
67
- get (opts = {}) {
68
- opts = this._validateGetOptions(opts)
69
- if (!this.opened) this.open().catch(err => this.emit('error', err))
70
- return this._loader.get(this._namespace, opts)
109
+ // No more async ticks allowed after this point -- necessary for caching
110
+
111
+ const storageRoot = [CORES_DIR, id.slice(0, 2), id.slice(2, 4), id].join('/')
112
+ const core = new Hypercore(p => this.storage(storageRoot + '/' + p), {
113
+ autoClose: true,
114
+ encryptionKey: opts.encryptionKey || null,
115
+ keyPair: {
116
+ publicKey: keyPair.publicKey,
117
+ secretKey: null
118
+ },
119
+ userData,
120
+ sign: null,
121
+ _preready: this._preready.bind(this),
122
+ createIfMissing: !!opts.keyPair
123
+ })
124
+
125
+ this.cores.set(id, core)
126
+ for (const stream of this._replicationStreams) {
127
+ core.replicate(stream)
128
+ }
129
+ core.once('close', () => {
130
+ this.cores.delete(id)
131
+ })
132
+
133
+ return { from: core, keyPair, sign }
71
134
  }
72
135
 
73
- namespace (name) {
74
- if (!name) throw new Error('A name must be provided as the first argument.')
75
- if (Buffer.isBuffer(name)) name = name.toString('hex')
76
- return new Corestore(this.storage, {
77
- namespace: [...this._namespace, name],
78
- _db: this._db,
79
- _loader: this._loader,
80
- _replicator: this._replicator
136
+ get (opts = {}) {
137
+ opts = validateGetOptions(opts)
138
+ const core = new Hypercore(null, {
139
+ ...opts,
140
+ name: null,
141
+ preload: () => this._preload(opts)
81
142
  })
143
+ return core
82
144
  }
83
145
 
84
146
  replicate (isInitiator, opts) {
85
- return this._replicator.replicate(isInitiator, opts)
147
+ const stream = Hypercore.createProtocolStream(isInitiator, opts)
148
+ for (const core of this.cores.values()) {
149
+ core.replicate(stream)
150
+ }
151
+ stream.on('discovery-key', discoveryKey => {
152
+ const core = this.get({ discoveryKey })
153
+ core.ready().then(() => {
154
+ core.replicate(stream)
155
+ }, () => {
156
+ stream.close(discoveryKey)
157
+ })
158
+ })
159
+ this._replicationStreams.push(stream)
160
+ stream.once('close', () => {
161
+ this._replicationStreams.splice(this._replicationStreams.indexOf(stream), 1)
162
+ })
163
+ return stream
86
164
  }
87
165
 
88
- // Backup/Restore
166
+ namespace (name) {
167
+ if (!Buffer.isBuffer(name)) name = Buffer.from(name)
168
+ return new Corestore(this.storage, {
169
+ _namespace: generateNamespace(this._namespace, name),
170
+ _opening: this._opening,
171
+ _cores: this.cores,
172
+ _streams: this._replicationStreams,
173
+ keys: this._opening.then(() => this.keys)
174
+ })
175
+ }
89
176
 
90
- async backup () {
91
- if (!this.opened) await this.open()
92
- const allCores = await this._db.getAllCores()
93
- return {
94
- masterKey: this._loader.masterKey.toString('hex'),
95
- cores: allCores
177
+ async _close () {
178
+ if (this._closing) return this._closing
179
+ await this._opening
180
+ const closePromises = []
181
+ for (const core of this.cores.values()) {
182
+ closePromises.push(core.close())
183
+ }
184
+ await Promise.allSettled(closePromises)
185
+ for (const stream of this._replicationStreams) {
186
+ stream.destroy()
96
187
  }
188
+ await this.keys.close()
97
189
  }
98
190
 
99
- restore (manifest) {
100
- return this._db.restore(manifest)
191
+ close () {
192
+ if (this._closing) return this._closing
193
+ this._closing = this._close()
194
+ this._closing.catch(noop)
195
+ return this._closing
101
196
  }
102
197
 
103
- static async restore (manifest, target) {
104
- if (!manifest || !manifest.masterKey) throw new Error('Malformed manifest.')
105
- const store = new this(target, {
106
- overwriteMasterKey: true,
107
- masterKey: Buffer.from(manifest.masterKey, 'hex')
108
- })
109
- await store.restore(manifest)
110
- return store
198
+ static createToken () {
199
+ return KeyManager.createToken()
111
200
  }
112
201
  }
113
202
 
114
- function defaultStorage (dir) {
115
- return function (name) {
116
- let lock = null
117
- try {
118
- lock = name.endsWith('/bitfield') ? require('fd-lock') : null
119
- } catch (err) {}
120
- return raf(name, { directory: dir, lock: lock })
203
+ function validateGetOptions (opts) {
204
+ if (Buffer.isBuffer(opts)) return { key: opts, publicKey: opts }
205
+ if (opts.key) {
206
+ opts.publicKey = opts.key
207
+ }
208
+ if (opts.keyPair) {
209
+ opts.publicKey = opts.keyPair.publicKey
210
+ opts.secretKey = opts.keyPair.secretKey
121
211
  }
212
+ if (opts.name && typeof opts.name !== 'string') throw new Error('name option must be a String')
213
+ if (opts.name && opts.secretKey) throw new Error('Cannot provide both a name and a secret key')
214
+ if (opts.publicKey && !Buffer.isBuffer(opts.publicKey)) throw new Error('publicKey option must be a Buffer')
215
+ if (opts.secretKey && !Buffer.isBuffer(opts.secretKey)) throw new Error('secretKey option must be a Buffer')
216
+ if (opts.discoveryKey && !Buffer.isBuffer(opts.discoveryKey)) throw new Error('discoveryKey option must be a Buffer')
217
+ if (!opts.name && !opts.publicKey) throw new Error('Must provide either a name or a publicKey')
218
+ return opts
122
219
  }
220
+
221
+ function generateNamespace (first, second) {
222
+ if (!Buffer.isBuffer(first)) first = Buffer.from(first)
223
+ if (second && !Buffer.isBuffer(second)) second = Buffer.from(second)
224
+ const out = Buffer.allocUnsafe(32)
225
+ sodium.crypto_generichash(out, second ? Buffer.concat([first, second]) : first)
226
+ return out
227
+ }
228
+
229
+ function noop () {}
package/lib/keys.js ADDED
@@ -0,0 +1,104 @@
1
+ // TODO: Extract this into a standalone module
2
+
3
+ const sodium = require('sodium-universal')
4
+ const blake2b = require('blake2b-universal')
5
+
6
+ const DEFAULT_TOKEN = Buffer.alloc(0)
7
+ const NAMESPACE = Buffer.from('@hyperspace/key-manager')
8
+
9
+ module.exports = class KeyManager {
10
+ constructor (storage, profile, opts = {}) {
11
+ this.storage = storage
12
+ this.profile = profile
13
+ }
14
+
15
+ _sign (keyPair, message) {
16
+ if (!keyPair._secretKey) throw new Error('Invalid key pair')
17
+ const signature = Buffer.allocUnsafe(sodium.crypto_sign_BYTES)
18
+ sodium.crypto_sign_detached(signature, message, keyPair._secretKey)
19
+ return signature
20
+ }
21
+
22
+ createSecret (name, token) {
23
+ return deriveSeed(this.profile, token, name)
24
+ }
25
+
26
+ createHypercoreKeyPair (name, token) {
27
+ const keyPair = {
28
+ publicKey: Buffer.allocUnsafe(sodium.crypto_sign_PUBLICKEYBYTES),
29
+ _secretKey: Buffer.alloc(sodium.crypto_sign_SECRETKEYBYTES),
30
+ sign: (msg) => this._sign(keyPair, msg)
31
+ }
32
+
33
+ sodium.crypto_sign_seed_keypair(keyPair.publicKey, keyPair._secretKey, this.createSecret(name, token))
34
+
35
+ return keyPair
36
+ }
37
+
38
+ createNetworkIdentity (name, token) {
39
+ const keyPair = {
40
+ publicKey: Buffer.alloc(32),
41
+ secretKey: Buffer.alloc(64)
42
+ }
43
+
44
+ sodium.crypto_sign_seed_keypair(keyPair.publicKey, keyPair.secretKey, this.createSecret(name, token))
45
+
46
+ return keyPair
47
+ }
48
+
49
+ close () {
50
+ return new Promise((resolve, reject) => {
51
+ this.storage.close(err => {
52
+ if (err) return reject(err)
53
+ return resolve()
54
+ })
55
+ })
56
+ }
57
+
58
+ static createToken () {
59
+ return randomBytes(32)
60
+ }
61
+
62
+ static async fromStorage (storage, opts = {}) {
63
+ const profileStorage = storage(opts.name || 'default')
64
+
65
+ const profile = await new Promise((resolve, reject) => {
66
+ profileStorage.stat((err, st) => {
67
+ if (err && err.code !== 'ENOENT') return reject(err)
68
+ if (err || st.size < 32 || opts.overwrite) {
69
+ const key = randomBytes(32)
70
+ return profileStorage.write(0, key, err => {
71
+ if (err) return reject(err)
72
+ return resolve(key)
73
+ })
74
+ }
75
+ profileStorage.read(0, 32, (err, key) => {
76
+ if (err) return reject(err)
77
+ return resolve(key)
78
+ })
79
+ })
80
+ })
81
+
82
+ return new this(profileStorage, profile, opts)
83
+ }
84
+ }
85
+
86
+ function deriveSeed (profile, token, name, output) {
87
+ if (token && token.length < 32) throw new Error('Token must be a Buffer with length >= 32')
88
+ if (!name || typeof name !== 'string') throw new Error('name must be a String')
89
+ if (!output) output = Buffer.alloc(32)
90
+
91
+ blake2b.batch(output, [
92
+ NAMESPACE,
93
+ token || DEFAULT_TOKEN,
94
+ Buffer.from(Buffer.byteLength(name, 'ascii') + '\n' + name, 'ascii')
95
+ ], profile)
96
+
97
+ return output
98
+ }
99
+
100
+ function randomBytes (n) {
101
+ const buf = Buffer.allocUnsafe(n)
102
+ sodium.randombytes_buf(buf)
103
+ return buf
104
+ }
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "corestore",
3
- "version": "6.0.0-beta1",
4
- "description": "A Hypercore factory that stores and replicates linked cores.",
3
+ "version": "6.0.1-alpha.7",
4
+ "description": "A Hypercore factory that simplifies managing collections of cores.",
5
5
  "main": "index.js",
6
6
  "scripts": {
7
- "test": "standard && tape test/*.js"
7
+ "test": "standard && brittle test/*.js"
8
8
  },
9
9
  "repository": {
10
10
  "type": "git",
11
- "url": "git+https://github.com/andrewosh/corestore.git"
11
+ "url": "git+https://github.com/hypercore-protocol/corestore.git"
12
12
  },
13
13
  "keywords": [
14
14
  "corestore"
@@ -16,30 +16,21 @@
16
16
  "author": "Andrew Osheroff <andrewosh@gmail.com>",
17
17
  "license": "MIT",
18
18
  "bugs": {
19
- "url": "https://github.com/andrewosh/corestore/issues"
19
+ "url": "https://github.com/hypercore-protocol/corestore/issues"
20
20
  },
21
- "homepage": "https://github.com/andrewosh/corestore#readme",
21
+ "homepage": "https://github.com/hypercore-protocol/corestore#readme",
22
22
  "devDependencies": {
23
- "@andrewosh/corestore-migration": "^5.8.2",
24
- "hypercore-promisifier": "^1.0.3",
25
- "random-access-memory": "^3.1.1",
26
- "standard": "^16.0.3",
27
- "tape": "^5.0.1"
23
+ "brittle": "^1.6.0",
24
+ "random-access-file": "^2.2.0",
25
+ "random-access-memory": "^3.1.2",
26
+ "standardx": "^7.0.0",
27
+ "tmp-promise": "^3.0.2"
28
28
  },
29
29
  "dependencies": {
30
- "@jsdevtools/readdir-enhanced": "^6.0.4",
31
- "custom-error-class": "^1.0.0",
30
+ "blake2b-universal": "^1.0.1",
32
31
  "derive-key": "^1.0.1",
33
- "end-of-stream": "^1.4.4",
34
- "fd-lock": "^1.1.1",
35
- "hyperbee": "^1.1.1",
36
- "hypercore": "^9.6.0",
37
- "hypercore-crypto": "^2.2.0",
38
- "hypercore-protocol": "^8.0.7",
39
- "nanoresource-promise": "^2.0.0",
40
- "random-access-file": "^2.1.4",
41
- "random-access-storage": "^1.4.1",
42
- "refpool": "^1.2.2",
43
- "varint": "^6.0.0"
32
+ "hypercore": "next",
33
+ "hypercore-crypto": "^2.3.0",
34
+ "sodium-universal": "^3.0.4"
44
35
  }
45
36
  }