corestore 6.0.1-alpha.9 → 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
@@ -3,45 +3,102 @@ const safetyCatch = require('safety-catch')
3
3
  const crypto = require('hypercore-crypto')
4
4
  const sodium = require('sodium-universal')
5
5
  const Hypercore = require('hypercore')
6
+ const Xache = require('xache')
7
+ const b4a = require('b4a')
6
8
 
7
- 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
8
11
 
9
12
  const CORES_DIR = 'cores'
10
- const PROFILES_DIR = 'profiles'
11
- const USERDATA_NAME_KEY = '@corestore/name'
12
- const USERDATA_NAMESPACE_KEY = '@corestore/namespace'
13
- 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'
14
16
 
15
17
  module.exports = class Corestore extends EventEmitter {
16
18
  constructor (storage, opts = {}) {
17
19
  super()
18
20
 
19
- this.storage = Hypercore.defaultStorage(storage, { lock: PROFILES_DIR + '/default' })
20
-
21
+ this.storage = Hypercore.defaultStorage(storage, { lock: PRIMARY_KEY_FILE_NAME })
21
22
  this.cores = opts._cores || new Map()
22
- this.keys = opts.keys
23
+ this.primaryKey = null
24
+ this.cache = !!opts.cache
23
25
 
24
- this._namespace = opts._namespace || DEFAULT_NAMESPACE
26
+ this._keyStorage = null
27
+ this._primaryKey = opts.primaryKey
28
+ this._namespace = opts.namespace || DEFAULT_NAMESPACE
25
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')
26
39
 
27
40
  this._opening = opts._opening ? opts._opening.then(() => this._open()) : this._open()
28
- this._opening.catch(noop)
41
+ this._opening.catch(safetyCatch)
29
42
  this.ready = () => this._opening
30
43
  }
31
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
+
32
72
  async _open () {
33
- if (this.keys) {
34
- this.keys = await this.keys // opts.keys can be a Promise that resolves to a KeyManager
35
- } else {
36
- 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
37
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
38
95
  }
39
96
 
40
97
  async _generateKeys (opts) {
41
98
  if (opts._discoveryKey) {
42
99
  return {
43
100
  keyPair: null,
44
- sign: null,
101
+ auth: null,
45
102
  discoveryKey: opts._discoveryKey
46
103
  }
47
104
  }
@@ -52,21 +109,23 @@ module.exports = class Corestore extends EventEmitter {
52
109
  secretKey: opts.secretKey
53
110
  },
54
111
  sign: opts.sign,
112
+ auth: opts.auth,
55
113
  discoveryKey: crypto.discoveryKey(opts.publicKey)
56
114
  }
57
115
  }
58
- const { publicKey, sign } = await this.keys.createHypercoreKeyPair(opts.name, this._namespace)
116
+ const { publicKey, auth } = await this.createKeyPair(opts.name)
59
117
  return {
60
118
  keyPair: {
61
119
  publicKey,
62
120
  secretKey: null
63
121
  },
64
- sign,
122
+ auth,
65
123
  discoveryKey: crypto.discoveryKey(publicKey)
66
124
  }
67
125
  }
68
126
 
69
127
  _getPrereadyUserData (core, key) {
128
+ // Need to manually read the header values before the Hypercore is ready, hence the ugliness.
70
129
  for (const { key: savedKey, value } of core.core.header.userData) {
71
130
  if (key === savedKey) return value
72
131
  }
@@ -78,11 +137,11 @@ module.exports = class Corestore extends EventEmitter {
78
137
  if (!name) return
79
138
 
80
139
  const namespace = this._getPrereadyUserData(core, USERDATA_NAMESPACE_KEY)
81
- const { publicKey, sign } = await this.keys.createHypercoreKeyPair(name.toString(), namespace)
82
- 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')
83
142
 
84
- // TODO: Should Hypercore expose a helper for this, or should preready return keypair/sign?
85
- core.sign = sign
143
+ // TODO: Should Hypercore expose a helper for this, or should preready return keypair/auth?
144
+ core.auth = auth
86
145
  core.key = publicKey
87
146
  core.writable = true
88
147
  }
@@ -90,22 +149,22 @@ module.exports = class Corestore extends EventEmitter {
90
149
  async _preload (opts) {
91
150
  await this.ready()
92
151
 
93
- const { discoveryKey, keyPair, sign } = await this._generateKeys(opts)
94
- const id = discoveryKey.toString('hex')
152
+ const { discoveryKey, keyPair, auth } = await this._generateKeys(opts)
153
+ const id = b4a.toString(discoveryKey, 'hex')
95
154
 
96
155
  while (this.cores.has(id)) {
97
156
  const existing = this.cores.get(id)
98
- if (existing.opened && !existing.closing) return { from: existing, keyPair, sign }
99
- if (!existing.opened) {
100
- await existing.ready().catch(safetyCatch)
101
- } else if (existing.closing) {
157
+ if (existing.opened && !existing.closing) return { from: existing, keyPair, auth }
158
+ if (existing.closing) {
102
159
  await existing.close()
160
+ } else {
161
+ await existing.ready().catch(safetyCatch)
103
162
  }
104
163
  }
105
164
 
106
165
  const userData = {}
107
166
  if (opts.name) {
108
- userData[USERDATA_NAME_KEY] = Buffer.from(opts.name)
167
+ userData[USERDATA_NAME_KEY] = b4a.from(opts.name)
109
168
  userData[USERDATA_NAMESPACE_KEY] = this._namespace
110
169
  }
111
170
 
@@ -117,7 +176,8 @@ module.exports = class Corestore extends EventEmitter {
117
176
  autoClose: true,
118
177
  encryptionKey: opts.encryptionKey || null,
119
178
  userData,
120
- sign: null,
179
+ auth,
180
+ cache: opts.cache,
121
181
  createIfMissing: !opts._discoveryKey,
122
182
  keyPair: keyPair && keyPair.publicKey
123
183
  ? {
@@ -130,6 +190,9 @@ module.exports = class Corestore extends EventEmitter {
130
190
  this.cores.set(id, core)
131
191
  core.ready().then(() => {
132
192
  for (const { stream } of this._replicationStreams) {
193
+ const sessions = this._streamSessions.get(stream)
194
+ const session = core.session()
195
+ sessions.push(session)
133
196
  core.replicate(stream)
134
197
  }
135
198
  }, () => {
@@ -139,16 +202,54 @@ module.exports = class Corestore extends EventEmitter {
139
202
  this.cores.delete(id)
140
203
  })
141
204
 
142
- 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
143
226
  }
144
227
 
145
228
  get (opts = {}) {
146
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
+
147
235
  const core = new Hypercore(null, {
148
236
  ...opts,
149
237
  name: null,
150
238
  preload: () => this._preload(opts)
151
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
+
152
253
  return core
153
254
  }
154
255
 
@@ -161,31 +262,46 @@ module.exports = class Corestore extends EventEmitter {
161
262
  return core.ready().catch(safetyCatch)
162
263
  }
163
264
  })
265
+
266
+ const sessions = []
164
267
  for (const core of this.cores.values()) {
165
- if (core.opened) core.replicate(stream) // If the core is not opened, it will be replicated in preload.
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)
271
+ core.replicate(stream)
166
272
  }
273
+
167
274
  const streamRecord = { stream, isExternal }
168
275
  this._replicationStreams.push(streamRecord)
276
+ this._streamSessions.set(stream, sessions)
277
+
169
278
  stream.once('close', () => {
170
279
  this._replicationStreams.splice(this._replicationStreams.indexOf(streamRecord), 1)
280
+ this._streamSessions.delete(stream)
281
+ Promise.all(sessions.map(s => s.close())).catch(safetyCatch)
171
282
  })
172
283
  return stream
173
284
  }
174
285
 
175
286
  namespace (name) {
176
- if (!Buffer.isBuffer(name)) name = Buffer.from(name)
177
287
  return new Corestore(this.storage, {
178
- _namespace: generateNamespace(this._namespace, name),
288
+ primaryKey: this._opening.then(() => this.primaryKey),
289
+ namespace: generateNamespace(this._namespace, name),
290
+ cache: this.cache,
179
291
  _opening: this._opening,
180
292
  _cores: this.cores,
181
293
  _streams: this._replicationStreams,
182
- keys: this._opening.then(() => this.keys)
294
+ _streamSessions: this._streamSessions
183
295
  })
184
296
  }
185
297
 
186
298
  async _close () {
187
- if (this._closing) return this._closing
188
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
+ }
189
305
  const closePromises = []
190
306
  for (const core of this.cores.values()) {
191
307
  closePromises.push(core.close())
@@ -195,23 +311,30 @@ module.exports = class Corestore extends EventEmitter {
195
311
  // Only close streams that were created by the Corestore
196
312
  if (!isExternal) stream.destroy()
197
313
  }
198
- 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
+ })
199
321
  }
200
322
 
201
323
  close () {
202
324
  if (this._closing) return this._closing
203
325
  this._closing = this._close()
204
- this._closing.catch(noop)
326
+ this._closing.catch(safetyCatch)
205
327
  return this._closing
206
328
  }
329
+ }
207
330
 
208
- static createToken () {
209
- return KeyManager.createToken()
210
- }
331
+ function sign (keyPair, message) {
332
+ if (!keyPair.secretKey) throw new Error('Invalid key pair')
333
+ return crypto.sign(message, keyPair.secretKey)
211
334
  }
212
335
 
213
336
  function validateGetOptions (opts) {
214
- if (Buffer.isBuffer(opts)) return { key: opts, publicKey: opts }
337
+ if (b4a.isBuffer(opts)) return { key: opts, publicKey: opts }
215
338
  if (opts.key) {
216
339
  opts.publicKey = opts.key
217
340
  }
@@ -221,22 +344,30 @@ function validateGetOptions (opts) {
221
344
  }
222
345
  if (opts.name && typeof opts.name !== 'string') throw new Error('name option must be a String')
223
346
  if (opts.name && opts.secretKey) throw new Error('Cannot provide both a name and a secret key')
224
- if (opts.publicKey && !Buffer.isBuffer(opts.publicKey)) throw new Error('publicKey option must be a Buffer')
225
- if (opts.secretKey && !Buffer.isBuffer(opts.secretKey)) throw new Error('secretKey option must be a Buffer')
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')
226
349
  if (!opts._discoveryKey && (!opts.name && !opts.publicKey)) throw new Error('Must provide either a name or a publicKey')
227
350
  return opts
228
351
  }
229
352
 
230
- function generateNamespace (first, second) {
231
- if (!Buffer.isBuffer(first)) first = Buffer.from(first)
232
- if (second && !Buffer.isBuffer(second)) second = Buffer.from(second)
233
- const out = Buffer.allocUnsafe(32)
234
- 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])
235
357
  return out
236
358
  }
237
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)
364
+ return out
365
+ }
366
+
367
+ function defaultCache () {
368
+ return new Xache({ maxSize: 65536, maxAge: 0 })
369
+ }
370
+
238
371
  function isStream (s) {
239
372
  return typeof s === 'object' && s && typeof s.pipe === 'function'
240
373
  }
241
-
242
- function noop () {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "corestore",
3
- "version": "6.0.1-alpha.9",
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,18 +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",
28
+ "b4a": "^1.3.1",
29
+ "hypercore": "v10.0.0",
30
+ "hypercore-crypto": "^3.2.1",
34
31
  "safety-catch": "^1.0.1",
35
- "sodium-universal": "^3.0.4"
32
+ "sodium-universal": "^3.0.4",
33
+ "xache": "^1.1.0"
36
34
  }
37
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
 
@@ -97,8 +100,8 @@ test('replicating cores created after replication begins', async function (t) {
97
100
  })
98
101
 
99
102
  test('replicating cores using discovery key hook', async function (t) {
100
- const dir = await tmp.dir({ unsafeCleanup: true })
101
- let store1 = new Corestore(dir.path)
103
+ const dir = tmpdir()
104
+ let store1 = new Corestore(dir)
102
105
  const store2 = new Corestore(ram)
103
106
 
104
107
  const core = store1.get({ name: 'main' })
@@ -106,15 +109,13 @@ test('replicating cores using discovery key hook', async function (t) {
106
109
  const key = core.key
107
110
 
108
111
  await store1.close()
109
- store1 = new Corestore(dir.path)
112
+ store1 = new Corestore(dir)
110
113
 
111
114
  const s = store1.replicate(true, { live: true })
112
115
  s.pipe(store2.replicate(false, { live: true })).pipe(s)
113
116
 
114
117
  const core2 = store2.get(key)
115
118
  t.alike(await core2.get(0), Buffer.from('hello'))
116
-
117
- await dir.cleanup()
118
119
  })
119
120
 
120
121
  test('nested namespaces', async function (t) {
@@ -142,9 +143,9 @@ test('core uncached when all sessions close', async function (t) {
142
143
  })
143
144
 
144
145
  test('writable core loaded from name userData', async function (t) {
145
- const dir = await tmp.dir({ unsafeCleanup: true })
146
+ const dir = tmpdir()
146
147
 
147
- let store = new Corestore(dir.path)
148
+ let store = new Corestore(dir)
148
149
  let core = store.get({ name: 'main' })
149
150
  await core.ready()
150
151
  const key = core.key
@@ -154,7 +155,7 @@ test('writable core loaded from name userData', async function (t) {
154
155
  t.is(core.length, 1)
155
156
 
156
157
  await store.close()
157
- store = new Corestore(dir.path)
158
+ store = new Corestore(dir)
158
159
  core = store.get(key)
159
160
  await core.ready()
160
161
 
@@ -163,23 +164,171 @@ test('writable core loaded from name userData', async function (t) {
163
164
  t.is(core.length, 2)
164
165
  t.alike(await core.get(0), Buffer.from('hello'))
165
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)
166
180
 
167
- await dir.cleanup()
181
+ await store.close()
182
+ store = new Corestore(dir)
183
+ core = store.get(key)
184
+ await core.ready()
185
+
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'))
168
191
  })
169
192
 
170
193
  test('storage locking', async function (t) {
171
- const dir = await tmp.dir({ unsafeCleanup: true })
194
+ const dir = tmpdir()
172
195
 
173
- const store1 = new Corestore(dir.path)
196
+ const store1 = new Corestore(dir)
174
197
  await store1.ready()
175
198
 
176
- const store2 = new Corestore(dir.path)
199
+ const store2 = new Corestore(dir)
177
200
  try {
178
201
  await store2.ready()
179
202
  t.fail('dir should have been locked')
180
203
  } catch {
181
204
  t.pass('dir was locked')
182
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()
216
+
217
+ t.is(store.cores.size, 2)
218
+ t.not(core1.closed)
219
+ t.not(core1.closed)
220
+
221
+ await store.close()
183
222
 
184
- await dir.cleanup()
223
+ t.is(store.cores.size, 0)
224
+ t.ok(core1.closed)
225
+ t.ok(core2.closed)
185
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)
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
- })