corestore 6.0.1-alpha.7 → 6.0.2

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.
@@ -1,23 +1,22 @@
1
- name: Test on Node.js
2
-
1
+ name: Build Status
3
2
  on:
4
3
  push:
5
4
  branches:
6
- - main
5
+ - master
7
6
  pull_request:
8
7
  branches:
9
- - main
8
+ - master
10
9
  jobs:
11
10
  build:
12
11
  strategy:
13
12
  matrix:
14
- node-version: [12.x, 14.x, 16.x]
13
+ node-version: [lts/*]
15
14
  os: [ubuntu-latest, macos-latest, windows-latest]
16
15
  runs-on: ${{ matrix.os }}
17
16
  steps:
18
17
  - uses: actions/checkout@v2
19
18
  - name: Use Node.js ${{ matrix.node-version }}
20
- uses: actions/setup-node@v1
19
+ uses: actions/setup-node@v2
21
20
  with:
22
21
  node-version: ${{ matrix.node-version }}
23
22
  - run: npm install
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Corestore v6
1
+ # Corestore
2
2
 
3
3
  Corestore is a Hypercore factory that makes it easier to manage large collections of named Hypercores.
4
4
 
@@ -9,7 +9,7 @@ Corestore provides:
9
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
10
 
11
11
  ### Installation
12
- `npm install corestore@next`
12
+ `npm install corestore`
13
13
 
14
14
  ### Usage
15
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:
@@ -34,14 +34,26 @@ If that Hypercore has previously been loaded, subsequent calls to `get` will ret
34
34
 
35
35
  All other options besides `name` and `key` will be forwarded to the Hypercore constructor.
36
36
 
37
- #### `const stream = store.replicate(opts)`
37
+ #### `const stream = store.replicate(optsOrStream)`
38
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
39
 
40
40
  `opts` will be forwarded to Hypercore's `replicate` function.
41
41
 
42
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
43
 
44
- If the remote side dynamically adds a new Hypercore to the replication stream, Corestore will load and replicatethat core if possible.
44
+ If the remote side dynamically adds a new Hypercore to the replication stream, Corestore will load and replicate that core if possible.
45
+
46
+ Using [Hyperswarm](https://github.com/hyperswarm/hyperswarm) you can easily replicate corestores
47
+
48
+ ``` js
49
+ const swarm = new Hyperswarm()
50
+
51
+ // join the relevant topic
52
+ swarm.join(...)
53
+
54
+ // simply pass the connection stream to corestore
55
+ swarm.on('connection', (connection) => store.replicate(connection))
56
+ ```
45
57
 
46
58
  #### `const store = store.namespace(name)`
47
59
  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.
package/index.js CHANGED
@@ -1,47 +1,105 @@
1
1
  const { EventEmitter } = require('events')
2
+ const safetyCatch = require('safety-catch')
2
3
  const crypto = require('hypercore-crypto')
3
4
  const sodium = require('sodium-universal')
4
5
  const Hypercore = require('hypercore')
6
+ const Xache = require('xache')
7
+ const b4a = require('b4a')
5
8
 
6
- const KeyManager = require('./lib/keys')
9
+ const [NS] = crypto.namespace('corestore', 1)
10
+ const DEFAULT_NAMESPACE = b4a.alloc(32) // This is meant to be 32 0-bytes
7
11
 
8
12
  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
+ const PRIMARY_KEY_FILE_NAME = 'primary-key'
14
+ const USERDATA_NAME_KEY = 'corestore/name'
15
+ const USERDATA_NAMESPACE_KEY = 'corestore/namespace'
13
16
 
14
17
  module.exports = class Corestore extends EventEmitter {
15
18
  constructor (storage, opts = {}) {
16
19
  super()
17
20
 
18
- this.storage = Hypercore.defaultStorage(storage, { lock: PROFILES_DIR + '/default' })
19
-
21
+ this.storage = Hypercore.defaultStorage(storage, { lock: PRIMARY_KEY_FILE_NAME })
20
22
  this.cores = opts._cores || new Map()
21
- this.keys = opts.keys
23
+ this.primaryKey = null
24
+ this.cache = !!opts.cache
22
25
 
23
- this._namespace = opts._namespace || DEFAULT_NAMESPACE
26
+ this._keyStorage = null
27
+ this._primaryKey = opts.primaryKey
28
+ this._namespace = opts.namespace || DEFAULT_NAMESPACE
24
29
  this._replicationStreams = opts._streams || []
30
+ this._overwrite = opts.overwrite === true
31
+
32
+ this._streamSessions = opts._streamSessions || new Map()
33
+ this._sessions = new Set() // sessions for THIS namespace
34
+
35
+ this._findingPeersCount = 0
36
+ this._findingPeers = []
37
+
38
+ if (this._namespace.byteLength !== 32) throw new Error('Namespace must be a 32-byte Buffer or Uint8Array')
25
39
 
26
40
  this._opening = opts._opening ? opts._opening.then(() => this._open()) : this._open()
27
- this._opening.catch(noop)
41
+ this._opening.catch(safetyCatch)
28
42
  this.ready = () => this._opening
29
43
  }
30
44
 
45
+ findingPeers () {
46
+ let done = false
47
+ this._incFindingPeers()
48
+
49
+ return () => {
50
+ if (done) return
51
+ done = true
52
+ this._decFindingPeers()
53
+ }
54
+ }
55
+
56
+ _incFindingPeers () {
57
+ if (++this._findingPeersCount !== 1) return
58
+
59
+ for (const core of this._sessions) {
60
+ this._findingPeers.push(core.findingPeers())
61
+ }
62
+ }
63
+
64
+ _decFindingPeers () {
65
+ if (--this._findingPeersCount !== 0) return
66
+
67
+ while (this._findingPeers.length > 0) {
68
+ this._findingPeers.pop()()
69
+ }
70
+ }
71
+
31
72
  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))
73
+ if (this._primaryKey) {
74
+ this.primaryKey = await this._primaryKey
75
+ return this.primaryKey
36
76
  }
77
+ this._keyStorage = this.storage(PRIMARY_KEY_FILE_NAME)
78
+ this.primaryKey = await new Promise((resolve, reject) => {
79
+ this._keyStorage.stat((err, st) => {
80
+ if (err && err.code !== 'ENOENT') return reject(err)
81
+ if (err || st.size < 32 || this._overwrite) {
82
+ const key = crypto.randomBytes(32)
83
+ return this._keyStorage.write(0, key, err => {
84
+ if (err) return reject(err)
85
+ return resolve(key)
86
+ })
87
+ }
88
+ this._keyStorage.read(0, 32, (err, key) => {
89
+ if (err) return reject(err)
90
+ return resolve(key)
91
+ })
92
+ })
93
+ })
94
+ return this.primaryKey
37
95
  }
38
96
 
39
97
  async _generateKeys (opts) {
40
- if (opts.discoveryKey) {
98
+ if (opts._discoveryKey) {
41
99
  return {
42
100
  keyPair: null,
43
- sign: null,
44
- discoveryKey: opts.discoveryKey
101
+ auth: null,
102
+ discoveryKey: opts._discoveryKey
45
103
  }
46
104
  }
47
105
  if (!opts.name) {
@@ -51,21 +109,23 @@ module.exports = class Corestore extends EventEmitter {
51
109
  secretKey: opts.secretKey
52
110
  },
53
111
  sign: opts.sign,
112
+ auth: opts.auth,
54
113
  discoveryKey: crypto.discoveryKey(opts.publicKey)
55
114
  }
56
115
  }
57
- const { publicKey, sign } = await this.keys.createHypercoreKeyPair(opts.name, this._namespace)
116
+ const { publicKey, auth } = await this.createKeyPair(opts.name)
58
117
  return {
59
118
  keyPair: {
60
119
  publicKey,
61
120
  secretKey: null
62
121
  },
63
- sign,
122
+ auth,
64
123
  discoveryKey: crypto.discoveryKey(publicKey)
65
124
  }
66
125
  }
67
126
 
68
127
  _getPrereadyUserData (core, key) {
128
+ // Need to manually read the header values before the Hypercore is ready, hence the ugliness.
69
129
  for (const { key: savedKey, value } of core.core.header.userData) {
70
130
  if (key === savedKey) return value
71
131
  }
@@ -77,11 +137,11 @@ module.exports = class Corestore extends EventEmitter {
77
137
  if (!name) return
78
138
 
79
139
  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')
140
+ const { publicKey, auth } = await this.createKeyPair(b4a.toString(name), namespace)
141
+ if (!b4a.equals(publicKey, core.key)) throw new Error('Stored core key does not match the provided name')
82
142
 
83
- // TODO: Should Hypercore expose a helper for this, or should preready return keypair/sign?
84
- core.sign = sign
143
+ // TODO: Should Hypercore expose a helper for this, or should preready return keypair/auth?
144
+ core.auth = auth
85
145
  core.key = publicKey
86
146
  core.writable = true
87
147
  }
@@ -89,20 +149,22 @@ module.exports = class Corestore extends EventEmitter {
89
149
  async _preload (opts) {
90
150
  await this.ready()
91
151
 
92
- const { discoveryKey, keyPair, sign } = await this._generateKeys(opts)
93
- const id = discoveryKey.toString('hex')
152
+ const { discoveryKey, keyPair, auth } = await this._generateKeys(opts)
153
+ const id = b4a.toString(discoveryKey, 'hex')
94
154
 
95
155
  while (this.cores.has(id)) {
96
156
  const existing = this.cores.get(id)
97
- if (existing) {
98
- if (!existing.closing) return { from: existing, keyPair, sign }
157
+ if (existing.opened && !existing.closing) return { from: existing, keyPair, auth }
158
+ if (existing.closing) {
99
159
  await existing.close()
160
+ } else {
161
+ await existing.ready().catch(safetyCatch)
100
162
  }
101
163
  }
102
164
 
103
165
  const userData = {}
104
166
  if (opts.name) {
105
- userData[USERDATA_NAME_KEY] = Buffer.from(opts.name)
167
+ userData[USERDATA_NAME_KEY] = b4a.from(opts.name)
106
168
  userData[USERDATA_NAMESPACE_KEY] = this._namespace
107
169
  }
108
170
 
@@ -110,98 +172,169 @@ module.exports = class Corestore extends EventEmitter {
110
172
 
111
173
  const storageRoot = [CORES_DIR, id.slice(0, 2), id.slice(2, 4), id].join('/')
112
174
  const core = new Hypercore(p => this.storage(storageRoot + '/' + p), {
175
+ _preready: this._preready.bind(this),
113
176
  autoClose: true,
114
177
  encryptionKey: opts.encryptionKey || null,
115
- keyPair: {
116
- publicKey: keyPair.publicKey,
117
- secretKey: null
118
- },
119
178
  userData,
120
- sign: null,
121
- _preready: this._preready.bind(this),
122
- createIfMissing: !!opts.keyPair
179
+ auth,
180
+ cache: opts.cache,
181
+ createIfMissing: !opts._discoveryKey,
182
+ keyPair: keyPair && keyPair.publicKey
183
+ ? {
184
+ publicKey: keyPair.publicKey,
185
+ secretKey: null
186
+ }
187
+ : null
123
188
  })
124
189
 
125
190
  this.cores.set(id, core)
126
- for (const stream of this._replicationStreams) {
127
- core.replicate(stream)
128
- }
191
+ core.ready().then(() => {
192
+ for (const { stream } of this._replicationStreams) {
193
+ const sessions = this._streamSessions.get(stream)
194
+ const session = core.session()
195
+ sessions.push(session)
196
+ core.replicate(stream)
197
+ }
198
+ }, () => {
199
+ this.cores.delete(id)
200
+ })
129
201
  core.once('close', () => {
130
202
  this.cores.delete(id)
131
203
  })
132
204
 
133
- return { from: core, keyPair, sign }
205
+ return { from: core, keyPair, auth }
206
+ }
207
+
208
+ async createKeyPair (name, namespace = this._namespace) {
209
+ if (!this.primaryKey) await this._opening
210
+
211
+ const keyPair = {
212
+ publicKey: b4a.allocUnsafe(sodium.crypto_sign_PUBLICKEYBYTES),
213
+ secretKey: b4a.alloc(sodium.crypto_sign_SECRETKEYBYTES),
214
+ auth: {
215
+ sign: (msg) => sign(keyPair, msg),
216
+ verify: (signable, signature) => {
217
+ return crypto.verify(signable, signature, keyPair.publicKey)
218
+ }
219
+ }
220
+ }
221
+
222
+ const seed = deriveSeed(this.primaryKey, namespace, name)
223
+ sodium.crypto_sign_seed_keypair(keyPair.publicKey, keyPair.secretKey, seed)
224
+
225
+ return keyPair
134
226
  }
135
227
 
136
228
  get (opts = {}) {
137
229
  opts = validateGetOptions(opts)
230
+
231
+ if (opts.cache !== false) {
232
+ opts.cache = opts.cache === true || (this.cache && !opts.cache) ? defaultCache() : opts.cache
233
+ }
234
+
138
235
  const core = new Hypercore(null, {
139
236
  ...opts,
140
237
  name: null,
141
238
  preload: () => this._preload(opts)
142
239
  })
240
+
241
+ this._sessions.add(core)
242
+ if (this._findingPeersCount > 0) {
243
+ this._findingPeers.push(core.findingPeers())
244
+ }
245
+
246
+ core.once('close', () => {
247
+ // technically better to also clear _findingPeers if we added it,
248
+ // but the lifecycle for those are pretty short so prob not worth the complexity
249
+ // as _decFindingPeers clear them all.
250
+ this._sessions.delete(core)
251
+ })
252
+
143
253
  return core
144
254
  }
145
255
 
146
256
  replicate (isInitiator, opts) {
147
- const stream = Hypercore.createProtocolStream(isInitiator, opts)
257
+ const isExternal = isStream(isInitiator) || !!(opts && opts.stream)
258
+ const stream = Hypercore.createProtocolStream(isInitiator, {
259
+ ...opts,
260
+ ondiscoverykey: discoveryKey => {
261
+ const core = this.get({ _discoveryKey: discoveryKey })
262
+ return core.ready().catch(safetyCatch)
263
+ }
264
+ })
265
+
266
+ const sessions = []
148
267
  for (const core of this.cores.values()) {
268
+ if (!core.opened) continue // If the core is not opened, it will be replicated in preload.
269
+ const session = core.session()
270
+ sessions.push(session)
149
271
  core.replicate(stream)
150
272
  }
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)
273
+
274
+ const streamRecord = { stream, isExternal }
275
+ this._replicationStreams.push(streamRecord)
276
+ this._streamSessions.set(stream, sessions)
277
+
160
278
  stream.once('close', () => {
161
- this._replicationStreams.splice(this._replicationStreams.indexOf(stream), 1)
279
+ this._replicationStreams.splice(this._replicationStreams.indexOf(streamRecord), 1)
280
+ this._streamSessions.delete(stream)
281
+ Promise.all(sessions.map(s => s.close())).catch(safetyCatch)
162
282
  })
163
283
  return stream
164
284
  }
165
285
 
166
286
  namespace (name) {
167
- if (!Buffer.isBuffer(name)) name = Buffer.from(name)
168
287
  return new Corestore(this.storage, {
169
- _namespace: generateNamespace(this._namespace, name),
288
+ primaryKey: this._opening.then(() => this.primaryKey),
289
+ namespace: generateNamespace(this._namespace, name),
290
+ cache: this.cache,
170
291
  _opening: this._opening,
171
292
  _cores: this.cores,
172
293
  _streams: this._replicationStreams,
173
- keys: this._opening.then(() => this.keys)
294
+ _streamSessions: this._streamSessions
174
295
  })
175
296
  }
176
297
 
177
298
  async _close () {
178
- if (this._closing) return this._closing
179
299
  await this._opening
300
+ if (!b4a.equals(this._namespace, DEFAULT_NAMESPACE)) {
301
+ // namespaces should not release resources on close
302
+ // TODO: Refactor the namespace close logic to actually close sessions with ref counting
303
+ return
304
+ }
180
305
  const closePromises = []
181
306
  for (const core of this.cores.values()) {
182
307
  closePromises.push(core.close())
183
308
  }
184
309
  await Promise.allSettled(closePromises)
185
- for (const stream of this._replicationStreams) {
186
- stream.destroy()
310
+ for (const { stream, isExternal } of this._replicationStreams) {
311
+ // Only close streams that were created by the Corestore
312
+ if (!isExternal) stream.destroy()
187
313
  }
188
- await this.keys.close()
314
+ if (!this._keyStorage) return
315
+ await new Promise((resolve, reject) => {
316
+ this._keyStorage.close(err => {
317
+ if (err) return reject(err)
318
+ return resolve(null)
319
+ })
320
+ })
189
321
  }
190
322
 
191
323
  close () {
192
324
  if (this._closing) return this._closing
193
325
  this._closing = this._close()
194
- this._closing.catch(noop)
326
+ this._closing.catch(safetyCatch)
195
327
  return this._closing
196
328
  }
329
+ }
197
330
 
198
- static createToken () {
199
- return KeyManager.createToken()
200
- }
331
+ function sign (keyPair, message) {
332
+ if (!keyPair.secretKey) throw new Error('Invalid key pair')
333
+ return crypto.sign(message, keyPair.secretKey)
201
334
  }
202
335
 
203
336
  function validateGetOptions (opts) {
204
- if (Buffer.isBuffer(opts)) return { key: opts, publicKey: opts }
337
+ if (b4a.isBuffer(opts)) return { key: opts, publicKey: opts }
205
338
  if (opts.key) {
206
339
  opts.publicKey = opts.key
207
340
  }
@@ -211,19 +344,30 @@ function validateGetOptions (opts) {
211
344
  }
212
345
  if (opts.name && typeof opts.name !== 'string') throw new Error('name option must be a String')
213
346
  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')
347
+ if (opts.publicKey && !b4a.isBuffer(opts.publicKey)) throw new Error('publicKey option must be a Buffer or Uint8Array')
348
+ if (opts.secretKey && !b4a.isBuffer(opts.secretKey)) throw new Error('secretKey option must be a Buffer or Uint8Array')
349
+ if (!opts._discoveryKey && (!opts.name && !opts.publicKey)) throw new Error('Must provide either a name or a publicKey')
218
350
  return opts
219
351
  }
220
352
 
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)
353
+ function generateNamespace (namespace, name) {
354
+ if (!b4a.isBuffer(name)) name = b4a.from(name)
355
+ const out = b4a.allocUnsafe(32)
356
+ sodium.crypto_generichash_batch(out, [namespace, name])
357
+ return out
358
+ }
359
+
360
+ function deriveSeed (primaryKey, namespace, name) {
361
+ if (!b4a.isBuffer(name)) name = b4a.from(name)
362
+ const out = b4a.alloc(32)
363
+ sodium.crypto_generichash_batch(out, [NS, namespace, name], primaryKey)
226
364
  return out
227
365
  }
228
366
 
229
- function noop () {}
367
+ function defaultCache () {
368
+ return new Xache({ maxSize: 65536, maxAge: 0 })
369
+ }
370
+
371
+ function isStream (s) {
372
+ return typeof s === 'object' && s && typeof s.pipe === 'function'
373
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "corestore",
3
- "version": "6.0.1-alpha.7",
3
+ "version": "6.0.2",
4
4
  "description": "A Hypercore factory that simplifies managing collections of cores.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -20,17 +20,16 @@
20
20
  },
21
21
  "homepage": "https://github.com/hypercore-protocol/corestore#readme",
22
22
  "devDependencies": {
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"
23
+ "brittle": "^3.0.0",
24
+ "random-access-memory": "^4.0.0",
25
+ "standardx": "^7.0.0"
28
26
  },
29
27
  "dependencies": {
30
- "blake2b-universal": "^1.0.1",
31
- "derive-key": "^1.0.1",
32
- "hypercore": "next",
33
- "hypercore-crypto": "^2.3.0",
34
- "sodium-universal": "^3.0.4"
28
+ "b4a": "^1.3.1",
29
+ "hypercore": "v10.0.0",
30
+ "hypercore-crypto": "^3.2.1",
31
+ "safety-catch": "^1.0.1",
32
+ "sodium-universal": "^3.0.4",
33
+ "xache": "^1.1.0"
35
34
  }
36
35
  }
package/test/all.js CHANGED
@@ -1,7 +1,10 @@
1
1
  const test = require('brittle')
2
2
  const crypto = require('hypercore-crypto')
3
3
  const ram = require('random-access-memory')
4
- const tmp = require('tmp-promise')
4
+ const os = require('os')
5
+ const path = require('path')
6
+ const b4a = require('b4a')
7
+ const sodium = require('sodium-universal')
5
8
 
6
9
  const Corestore = require('..')
7
10
 
@@ -77,6 +80,44 @@ test('basic replication', async function (t) {
77
80
  t.alike(await core4.get(0), Buffer.from('world'))
78
81
  })
79
82
 
83
+ test('replicating cores created after replication begins', async function (t) {
84
+ const store1 = new Corestore(ram)
85
+ const store2 = new Corestore(ram)
86
+
87
+ const s = store1.replicate(true, { live: true })
88
+ s.pipe(store2.replicate(false, { live: true })).pipe(s)
89
+
90
+ const core1 = store1.get({ name: 'core-1' })
91
+ const core2 = store1.get({ name: 'core-2' })
92
+ await core1.append('hello')
93
+ await core2.append('world')
94
+
95
+ const core3 = store2.get({ key: core1.key })
96
+ const core4 = store2.get({ key: core2.key })
97
+
98
+ t.alike(await core3.get(0), Buffer.from('hello'))
99
+ t.alike(await core4.get(0), Buffer.from('world'))
100
+ })
101
+
102
+ test('replicating cores using discovery key hook', async function (t) {
103
+ const dir = tmpdir()
104
+ let store1 = new Corestore(dir)
105
+ const store2 = new Corestore(ram)
106
+
107
+ const core = store1.get({ name: 'main' })
108
+ await core.append('hello')
109
+ const key = core.key
110
+
111
+ await store1.close()
112
+ store1 = new Corestore(dir)
113
+
114
+ const s = store1.replicate(true, { live: true })
115
+ s.pipe(store2.replicate(false, { live: true })).pipe(s)
116
+
117
+ const core2 = store2.get(key)
118
+ t.alike(await core2.get(0), Buffer.from('hello'))
119
+ })
120
+
80
121
  test('nested namespaces', async function (t) {
81
122
  const store = new Corestore(ram)
82
123
  const ns1a = store.namespace('ns1').namespace('a')
@@ -102,9 +143,9 @@ test('core uncached when all sessions close', async function (t) {
102
143
  })
103
144
 
104
145
  test('writable core loaded from name userData', async function (t) {
105
- const dir = await tmp.dir({ unsafeCleanup: true })
146
+ const dir = tmpdir()
106
147
 
107
- let store = new Corestore(dir.path)
148
+ let store = new Corestore(dir)
108
149
  let core = store.get({ name: 'main' })
109
150
  await core.ready()
110
151
  const key = core.key
@@ -114,7 +155,7 @@ test('writable core loaded from name userData', async function (t) {
114
155
  t.is(core.length, 1)
115
156
 
116
157
  await store.close()
117
- store = new Corestore(dir.path)
158
+ store = new Corestore(dir)
118
159
  core = store.get(key)
119
160
  await core.ready()
120
161
 
@@ -123,23 +164,171 @@ test('writable core loaded from name userData', async function (t) {
123
164
  t.is(core.length, 2)
124
165
  t.alike(await core.get(0), Buffer.from('hello'))
125
166
  t.alike(await core.get(1), Buffer.from('world'))
167
+ })
168
+
169
+ test('writable core loaded from name and namespace userData', async function (t) {
170
+ const dir = tmpdir()
171
+
172
+ let store = new Corestore(dir)
173
+ let core = store.namespace('ns1').get({ name: 'main' })
174
+ await core.ready()
175
+ const key = core.key
176
+
177
+ t.ok(core.writable)
178
+ await core.append('hello')
179
+ t.is(core.length, 1)
180
+
181
+ await store.close()
182
+ store = new Corestore(dir)
183
+ core = store.get(key)
184
+ await core.ready()
126
185
 
127
- await dir.cleanup()
186
+ t.ok(core.writable)
187
+ await core.append('world')
188
+ t.is(core.length, 2)
189
+ t.alike(await core.get(0), Buffer.from('hello'))
190
+ t.alike(await core.get(1), Buffer.from('world'))
128
191
  })
129
192
 
130
193
  test('storage locking', async function (t) {
131
- const dir = await tmp.dir({ unsafeCleanup: true })
194
+ const dir = tmpdir()
132
195
 
133
- const store1 = new Corestore(dir.path)
196
+ const store1 = new Corestore(dir)
134
197
  await store1.ready()
135
198
 
136
- const store2 = new Corestore(dir.path)
199
+ const store2 = new Corestore(dir)
137
200
  try {
138
201
  await store2.ready()
139
202
  t.fail('dir should have been locked')
140
203
  } catch {
141
204
  t.pass('dir was locked')
142
205
  }
206
+ })
207
+
208
+ test('closing a namespace does not close cores', async function (t) {
209
+ const store = new Corestore(ram)
210
+ const ns1 = store.namespace('ns1')
211
+ const core1 = ns1.get({ name: 'core-1' })
212
+ const core2 = ns1.get({ name: 'core-2' })
213
+ await Promise.all([core1.ready(), core2.ready()])
214
+
215
+ await ns1.close()
143
216
 
144
- await dir.cleanup()
217
+ t.is(store.cores.size, 2)
218
+ t.not(core1.closed)
219
+ t.not(core1.closed)
220
+
221
+ await store.close()
222
+
223
+ t.is(store.cores.size, 0)
224
+ t.ok(core1.closed)
225
+ t.ok(core2.closed)
226
+ })
227
+
228
+ test('findingPeers', async function (t) {
229
+ t.plan(6)
230
+
231
+ const store = new Corestore(ram)
232
+
233
+ const ns1 = store.namespace('ns1')
234
+ const ns2 = store.namespace('ns2')
235
+
236
+ const a = ns1.get(Buffer.alloc(32).fill('a'))
237
+ const b = ns2.get(Buffer.alloc(32).fill('b'))
238
+
239
+ const done = ns1.findingPeers()
240
+
241
+ let aUpdated = false
242
+ let bUpdated = false
243
+ let cUpdated = false
244
+
245
+ const c = ns1.get(Buffer.alloc(32).fill('c'))
246
+
247
+ a.update().then(function (bool) {
248
+ aUpdated = true
249
+ })
250
+
251
+ b.update().then(function (bool) {
252
+ bUpdated = true
253
+ })
254
+
255
+ c.update().then(function (bool) {
256
+ cUpdated = true
257
+ })
258
+
259
+ await new Promise(resolve => setImmediate(resolve))
260
+
261
+ t.is(aUpdated, false)
262
+ t.is(bUpdated, true)
263
+ t.is(cUpdated, false)
264
+
265
+ done()
266
+
267
+ await new Promise(resolve => setImmediate(resolve))
268
+
269
+ t.is(aUpdated, true)
270
+ t.is(bUpdated, true)
271
+ t.is(cUpdated, true)
145
272
  })
273
+
274
+ test('different primary keys yield different keypairs', async function (t) {
275
+ const pk1 = randomBytes(32)
276
+ const pk2 = randomBytes(32)
277
+ t.unlike(pk1, pk2)
278
+
279
+ const store1 = new Corestore(ram, { primaryKey: pk1 })
280
+ const store2 = new Corestore(ram, { primaryKey: pk2 })
281
+
282
+ const kp1 = await store1.createKeyPair('hello')
283
+ const kp2 = await store2.createKeyPair('hello')
284
+
285
+ t.unlike(kp1.publicKey, kp2.publicKey)
286
+ })
287
+
288
+ test('keypair auth sign', async function (t) {
289
+ const store = new Corestore(ram)
290
+ const keyPair = await store.createKeyPair('foo')
291
+ const message = b4a.from('hello world')
292
+
293
+ const sig = keyPair.auth.sign(message)
294
+
295
+ t.is(sig.length, 64)
296
+ t.ok(crypto.verify(message, sig, keyPair.publicKey))
297
+ t.absent(crypto.verify(message, b4a.alloc(64), keyPair.publicKey))
298
+ })
299
+
300
+ test('keypair auth verify', async function (t) {
301
+ const store = new Corestore(ram)
302
+ const keyPair = await store.createKeyPair('foo')
303
+ const message = b4a.from('hello world')
304
+
305
+ const sig = crypto.sign(message, keyPair.secretKey)
306
+
307
+ t.is(sig.length, 64)
308
+ t.ok(keyPair.auth.verify(message, sig))
309
+ t.absent(keyPair.auth.verify(message, b4a.alloc(64)))
310
+ })
311
+
312
+ test('core caching after reopen regression', async function (t) {
313
+ const store = new Corestore(ram)
314
+ const core = store.get({ name: 'test-core' })
315
+ await core.ready()
316
+
317
+ core.close()
318
+ await core.opening
319
+
320
+ const core2 = store.get({ name: 'test-core' })
321
+ await core2.ready()
322
+
323
+ t.pass('did not infinite loop')
324
+ })
325
+
326
+ function tmpdir () {
327
+ return path.join(os.tmpdir(), 'corestore-' + Math.random().toString(16).slice(2))
328
+ }
329
+
330
+ function randomBytes (n) {
331
+ const buf = b4a.allocUnsafe(n)
332
+ sodium.randombytes_buf(buf)
333
+ return buf
334
+ }
package/test/cache.js ADDED
@@ -0,0 +1,46 @@
1
+ const test = require('brittle')
2
+ const RAM = require('random-access-memory')
3
+
4
+ const Corestore = require('..')
5
+
6
+ test('core cache', async function (t) {
7
+ const store = new Corestore(RAM, { cache: true })
8
+
9
+ const core = store.get({ name: 'core' })
10
+ await core.append(['a', 'b', 'c'])
11
+
12
+ const p = core.get(0)
13
+ const q = core.get(0)
14
+
15
+ t.is(await p, await q)
16
+ })
17
+
18
+ test('clear cache on truncate', async function (t) {
19
+ const store = new Corestore(RAM, { cache: true })
20
+
21
+ const core = store.get({ name: 'core' })
22
+ await core.append(['a', 'b', 'c'])
23
+
24
+ const p = core.get(0)
25
+
26
+ await core.truncate(0)
27
+ await core.append('d')
28
+
29
+ const q = core.get(0)
30
+
31
+ t.alike(await p, Buffer.from('a'))
32
+ t.alike(await q, Buffer.from('d'))
33
+ })
34
+
35
+ test('core cache on namespace', async function (t) {
36
+ const store = new Corestore(RAM, { cache: true })
37
+ const ns1 = store.namespace('test-namespace-1')
38
+
39
+ const c1 = store.get({ name: 'test-core' })
40
+ const c2 = ns1.get({ name: 'test-core' })
41
+
42
+ await Promise.all([c1.ready(), c2.ready()])
43
+
44
+ t.ok(c1.cache)
45
+ t.ok(c2.cache)
46
+ })
package/lib/keys.js DELETED
@@ -1,104 +0,0 @@
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/test/keys.js DELETED
@@ -1,65 +0,0 @@
1
- const p = require('path')
2
- const fs = require('fs')
3
-
4
- const test = require('brittle')
5
- const ram = require('random-access-memory')
6
- const raf = require('random-access-file')
7
-
8
- const KeyManager = require('../lib/keys')
9
-
10
- test('can create hypercore keypairs', async t => {
11
- const keys = await KeyManager.fromStorage(ram)
12
-
13
- const kp1 = await keys.createHypercoreKeyPair('core1')
14
- const kp2 = await keys.createHypercoreKeyPair('core2')
15
-
16
- t.is(kp1.publicKey.length, 32)
17
- t.is(kp2.publicKey.length, 32)
18
- t.unlike(kp1.publicKey, kp2.publicKey)
19
- })
20
-
21
- test('distinct tokens create distinct hypercore keypairs', async t => {
22
- const keys = await KeyManager.fromStorage(ram)
23
- const token1 = KeyManager.createToken()
24
- const token2 = KeyManager.createToken()
25
-
26
- const kp1 = await keys.createHypercoreKeyPair('core1', token1)
27
- const kp2 = await keys.createHypercoreKeyPair('core1', token2)
28
-
29
- t.unlike(kp1.publicKey, kp2.publicKey)
30
- })
31
-
32
- test('short user-provided token will throw', async t => {
33
- const keys = await KeyManager.fromStorage(ram)
34
-
35
- try {
36
- await keys.createHypercoreKeyPair('core1', Buffer.from('hello'))
37
- t.fail('did not throw')
38
- } catch {
39
- t.pass('threw correctly')
40
- }
41
- })
42
-
43
- test('persistent storage regenerates keys correctly', async t => {
44
- const testPath = p.resolve(__dirname, 'test-data')
45
-
46
- const keys1 = await KeyManager.fromStorage((name) => raf(testPath, { directory: testPath }))
47
- const kp1 = await keys1.createHypercoreKeyPair('core1')
48
-
49
- const keys2 = await KeyManager.fromStorage((name) => raf(testPath, { directory: testPath }))
50
- const kp2 = await keys2.createHypercoreKeyPair('core1')
51
-
52
- t.alike(kp1.publicKey, kp2.publicKey)
53
-
54
- await fs.promises.rm(testPath, { recursive: true })
55
- })
56
-
57
- test('different master keys -> different keys', async t => {
58
- const keys1 = await KeyManager.fromStorage(ram)
59
- const keys2 = await KeyManager.fromStorage(ram)
60
-
61
- const kp1 = await keys1.createHypercoreKeyPair('core1')
62
- const kp2 = await keys2.createHypercoreKeyPair('core1')
63
-
64
- t.unlike(kp1.publicKey, kp2.publicKey)
65
- })