corestore 6.0.0-alpha.4 → 6.0.0

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:
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,30 +109,39 @@ 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
- async _postload (core) {
69
- const name = await core.getUserData(USERDATA_NAME_KEY)
127
+ _getPrereadyUserData (core, key) {
128
+ // Need to manually read the header values before the Hypercore is ready, hence the ugliness.
129
+ for (const { key: savedKey, value } of core.core.header.userData) {
130
+ if (key === savedKey) return value
131
+ }
132
+ return null
133
+ }
134
+
135
+ async _preready (core) {
136
+ const name = this._getPrereadyUserData(core, USERDATA_NAME_KEY)
70
137
  if (!name) return
71
138
 
72
- const namespace = await core.getUserData(USERDATA_NAMESPACE_KEY)
73
- const { publicKey, sign } = await this.keys.createHypercoreKeyPair(name.toString(), namespace)
74
- if (!publicKey.equals(core.key)) throw new Error('Stored core key does not match the provided name')
139
+ const namespace = this._getPrereadyUserData(core, USERDATA_NAMESPACE_KEY)
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')
75
142
 
76
- // TODO: Should Hypercore expose a helper for this, or should postload return keypair/sign?
77
- core.sign = sign
143
+ // TODO: Should Hypercore expose a helper for this, or should preready return keypair/auth?
144
+ core.auth = auth
78
145
  core.key = publicKey
79
146
  core.writable = true
80
147
  }
@@ -82,20 +149,22 @@ module.exports = class Corestore extends EventEmitter {
82
149
  async _preload (opts) {
83
150
  await this.ready()
84
151
 
85
- const { discoveryKey, keyPair, sign } = await this._generateKeys(opts)
86
- const id = discoveryKey.toString('hex')
152
+ const { discoveryKey, keyPair, auth } = await this._generateKeys(opts)
153
+ const id = b4a.toString(discoveryKey, 'hex')
87
154
 
88
155
  while (this.cores.has(id)) {
89
156
  const existing = this.cores.get(id)
90
- if (existing) {
91
- if (!existing.closing) return { from: existing, keyPair, sign }
157
+ if (existing.opened && !existing.closing) return { from: existing, keyPair, auth }
158
+ if (existing.closing) {
92
159
  await existing.close()
160
+ } else {
161
+ await existing.ready().catch(safetyCatch)
93
162
  }
94
163
  }
95
164
 
96
165
  const userData = {}
97
166
  if (opts.name) {
98
- userData[USERDATA_NAME_KEY] = Buffer.from(opts.name)
167
+ userData[USERDATA_NAME_KEY] = b4a.from(opts.name)
99
168
  userData[USERDATA_NAMESPACE_KEY] = this._namespace
100
169
  }
101
170
 
@@ -103,98 +172,169 @@ module.exports = class Corestore extends EventEmitter {
103
172
 
104
173
  const storageRoot = [CORES_DIR, id.slice(0, 2), id.slice(2, 4), id].join('/')
105
174
  const core = new Hypercore(p => this.storage(storageRoot + '/' + p), {
175
+ _preready: this._preready.bind(this),
106
176
  autoClose: true,
107
177
  encryptionKey: opts.encryptionKey || null,
108
- keyPair: {
109
- publicKey: keyPair.publicKey,
110
- secretKey: null
111
- },
112
178
  userData,
113
- sign: null,
114
- postload: this._postload.bind(this),
115
- 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
116
188
  })
117
189
 
118
190
  this.cores.set(id, core)
119
- for (const stream of this._replicationStreams) {
120
- core.replicate(stream)
121
- }
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
+ })
122
201
  core.once('close', () => {
123
202
  this.cores.delete(id)
124
203
  })
125
204
 
126
- 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
127
226
  }
128
227
 
129
228
  get (opts = {}) {
130
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
+
131
235
  const core = new Hypercore(null, {
132
236
  ...opts,
133
237
  name: null,
134
238
  preload: () => this._preload(opts)
135
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
+
136
253
  return core
137
254
  }
138
255
 
139
- replicate (opts = {}) {
140
- const stream = isStream(opts) ? opts : (opts.stream || Hypercore.createProtocolStream(opts))
256
+ replicate (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 = []
141
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)
142
271
  core.replicate(stream)
143
272
  }
144
- stream.on('discovery-key', discoveryKey => {
145
- const core = this.get({ discoveryKey })
146
- core.ready().then(() => {
147
- core.replicate(stream)
148
- }, () => {
149
- stream.close(discoveryKey)
150
- })
151
- })
152
- this._replicationStreams.push(stream)
273
+
274
+ const streamRecord = { stream, isExternal }
275
+ this._replicationStreams.push(streamRecord)
276
+ this._streamSessions.set(stream, sessions)
277
+
153
278
  stream.once('close', () => {
154
- 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)
155
282
  })
156
283
  return stream
157
284
  }
158
285
 
159
286
  namespace (name) {
160
- if (!Buffer.isBuffer(name)) name = Buffer.from(name)
161
287
  return new Corestore(this.storage, {
162
- _namespace: generateNamespace(this._namespace, name),
288
+ primaryKey: this._opening.then(() => this.primaryKey),
289
+ namespace: generateNamespace(this._namespace, name),
290
+ cache: this.cache,
163
291
  _opening: this._opening,
164
292
  _cores: this.cores,
165
293
  _streams: this._replicationStreams,
166
- keys: this._opening.then(() => this.keys)
294
+ _streamSessions: this._streamSessions
167
295
  })
168
296
  }
169
297
 
170
298
  async _close () {
171
- if (this._closing) return this._closing
172
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
+ }
173
305
  const closePromises = []
174
306
  for (const core of this.cores.values()) {
175
307
  closePromises.push(core.close())
176
308
  }
177
309
  await Promise.allSettled(closePromises)
178
- for (const stream of this._replicationStreams) {
179
- 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()
180
313
  }
181
- 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
+ })
182
321
  }
183
322
 
184
323
  close () {
185
324
  if (this._closing) return this._closing
186
325
  this._closing = this._close()
187
- this._closing.catch(noop)
326
+ this._closing.catch(safetyCatch)
188
327
  return this._closing
189
328
  }
329
+ }
190
330
 
191
- static createToken () {
192
- return KeyManager.createToken()
193
- }
331
+ function sign (keyPair, message) {
332
+ if (!keyPair.secretKey) throw new Error('Invalid key pair')
333
+ return crypto.sign(message, keyPair.secretKey)
194
334
  }
195
335
 
196
336
  function validateGetOptions (opts) {
197
- if (Buffer.isBuffer(opts)) return { key: opts, publicKey: opts }
337
+ if (b4a.isBuffer(opts)) return { key: opts, publicKey: opts }
198
338
  if (opts.key) {
199
339
  opts.publicKey = opts.key
200
340
  }
@@ -204,23 +344,30 @@ function validateGetOptions (opts) {
204
344
  }
205
345
  if (opts.name && typeof opts.name !== 'string') throw new Error('name option must be a String')
206
346
  if (opts.name && opts.secretKey) throw new Error('Cannot provide both a name and a secret key')
207
- if (opts.publicKey && !Buffer.isBuffer(opts.publicKey)) throw new Error('publicKey option must be a Buffer')
208
- if (opts.secretKey && !Buffer.isBuffer(opts.secretKey)) throw new Error('secretKey option must be a Buffer')
209
- if (opts.discoveryKey && !Buffer.isBuffer(opts.discoveryKey)) throw new Error('discoveryKey option must be a Buffer')
210
- 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')
211
350
  return opts
212
351
  }
213
352
 
214
- function generateNamespace (first, second) {
215
- if (!Buffer.isBuffer(first)) first = Buffer.from(first)
216
- if (second && !Buffer.isBuffer(second)) second = Buffer.from(second)
217
- const out = Buffer.allocUnsafe(32)
218
- 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)
219
364
  return out
220
365
  }
221
366
 
367
+ function defaultCache () {
368
+ return new Xache({ maxSize: 65536, maxAge: 0 })
369
+ }
370
+
222
371
  function isStream (s) {
223
372
  return typeof s === 'object' && s && typeof s.pipe === 'function'
224
373
  }
225
-
226
- function noop () {}
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "corestore",
3
- "version": "6.0.0-alpha.4",
3
+ "version": "6.0.0",
4
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",
@@ -20,17 +20,16 @@
20
20
  },
21
21
  "homepage": "https://github.com/hypercore-protocol/corestore#readme",
22
22
  "devDependencies": {
23
- "random-access-file": "^2.2.0",
24
- "random-access-memory": "^3.1.2",
25
- "standardx": "^7.0.0",
26
- "tape": "^5.3.1",
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
- const test = require('tape')
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
 
@@ -13,15 +16,13 @@ test('basic get with caching', async function (t) {
13
16
 
14
17
  await Promise.all([core1a.ready(), core1b.ready(), core2.ready()])
15
18
 
16
- t.same(core1a.key, core1b.key)
17
- t.notSame(core1a.key, core2.key)
19
+ t.alike(core1a.key, core1b.key)
20
+ t.unlike(core1a.key, core2.key)
18
21
 
19
- t.true(core1a.writable)
20
- t.true(core1b.writable)
22
+ t.ok(core1a.writable)
23
+ t.ok(core1b.writable)
21
24
 
22
- t.same(store.cores.size, 2)
23
-
24
- t.end()
25
+ t.is(store.cores.size, 2)
25
26
  })
26
27
 
27
28
  test('basic get with custom keypair', async function (t) {
@@ -33,12 +34,10 @@ test('basic get with custom keypair', async function (t) {
33
34
  const core2 = store.get(kp2)
34
35
  await Promise.all([core1.ready(), core2.ready()])
35
36
 
36
- t.same(core1.key, kp1.publicKey)
37
- t.same(core2.key, kp2.publicKey)
38
- t.true(core1.writable)
39
- t.true(core2.writable)
40
-
41
- t.end()
37
+ t.alike(core1.key, kp1.publicKey)
38
+ t.alike(core2.key, kp2.publicKey)
39
+ t.ok(core1.writable)
40
+ t.ok(core2.writable)
42
41
  })
43
42
 
44
43
  test('basic namespaces', async function (t) {
@@ -52,12 +51,12 @@ test('basic namespaces', async function (t) {
52
51
  const core3 = ns3.get({ name: 'main' })
53
52
  await Promise.all([core1.ready(), core2.ready(), core3.ready()])
54
53
 
55
- t.false(core1.key.equals(core2.key))
56
- t.true(core1.key.equals(core3.key))
57
- t.true(core1.writable)
58
- t.true(core2.writable)
59
- t.true(core3.writable)
60
- t.same(store.cores.size, 2)
54
+ t.absent(core1.key.equals(core2.key))
55
+ t.ok(core1.key.equals(core3.key))
56
+ t.ok(core1.writable)
57
+ t.ok(core2.writable)
58
+ t.ok(core3.writable)
59
+ t.is(store.cores.size, 2)
61
60
 
62
61
  t.end()
63
62
  })
@@ -77,10 +76,46 @@ test('basic replication', async function (t) {
77
76
  const s = store1.replicate(true)
78
77
  s.pipe(store2.replicate(false)).pipe(s)
79
78
 
80
- t.same(await core3.get(0), Buffer.from('hello'))
81
- t.same(await core4.get(0), Buffer.from('world'))
79
+ t.alike(await core3.get(0), Buffer.from('hello'))
80
+ t.alike(await core4.get(0), Buffer.from('world'))
81
+ })
82
82
 
83
- t.end()
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'))
84
119
  })
85
120
 
86
121
  test('nested namespaces', async function (t) {
@@ -92,65 +127,208 @@ test('nested namespaces', async function (t) {
92
127
  const core2 = ns1b.get({ name: 'main' })
93
128
  await Promise.all([core1.ready(), core2.ready()])
94
129
 
95
- t.false(core1.key.equals(core2.key))
96
- t.true(core1.writable)
97
- t.true(core2.writable)
98
- t.same(store.cores.size, 2)
99
-
100
- t.end()
130
+ t.not(core1.key.equals(core2.key))
131
+ t.ok(core1.writable)
132
+ t.ok(core2.writable)
133
+ t.is(store.cores.size, 2)
101
134
  })
102
135
 
103
136
  test('core uncached when all sessions close', async function (t) {
104
137
  const store = new Corestore(ram)
105
138
  const core1 = store.get({ name: 'main' })
106
139
  await core1.ready()
107
- t.same(store.cores.size, 1)
140
+ t.is(store.cores.size, 1)
108
141
  await core1.close()
109
- t.same(store.cores.size, 0)
110
- t.end()
142
+ t.is(store.cores.size, 0)
111
143
  })
112
144
 
113
145
  test('writable core loaded from name userData', async function (t) {
114
- const dir = await tmp.dir({ unsafeCleanup: true })
146
+ const dir = tmpdir()
115
147
 
116
- let store = new Corestore(dir.path)
148
+ let store = new Corestore(dir)
117
149
  let core = store.get({ name: 'main' })
118
150
  await core.ready()
119
151
  const key = core.key
120
152
 
121
- t.true(core.writable)
153
+ t.ok(core.writable)
122
154
  await core.append('hello')
123
- t.same(core.length, 1)
155
+ t.is(core.length, 1)
124
156
 
125
157
  await store.close()
126
- store = new Corestore(dir.path)
158
+ store = new Corestore(dir)
127
159
  core = store.get(key)
128
160
  await core.ready()
129
161
 
130
- t.true(core.writable)
162
+ t.ok(core.writable)
131
163
  await core.append('world')
132
- t.same(core.length, 2)
133
- t.same(await core.get(0), Buffer.from('hello'))
134
- t.same(await core.get(1), Buffer.from('world'))
164
+ t.is(core.length, 2)
165
+ t.alike(await core.get(0), Buffer.from('hello'))
166
+ t.alike(await core.get(1), Buffer.from('world'))
167
+ })
135
168
 
136
- await dir.cleanup()
137
- t.end()
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()
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'))
138
191
  })
139
192
 
140
193
  test('storage locking', async function (t) {
141
- const dir = await tmp.dir({ unsafeCleanup: true })
194
+ const dir = tmpdir()
142
195
 
143
- const store1 = new Corestore(dir.path)
196
+ const store1 = new Corestore(dir)
144
197
  await store1.ready()
145
198
 
146
- const store2 = new Corestore(dir.path)
199
+ const store2 = new Corestore(dir)
147
200
  try {
148
201
  await store2.ready()
149
202
  t.fail('dir should have been locked')
150
203
  } catch {
151
204
  t.pass('dir was locked')
152
205
  }
206
+ })
153
207
 
154
- await dir.cleanup()
155
- t.end()
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()
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)
156
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,74 +0,0 @@
1
- const p = require('path')
2
- const fs = require('fs')
3
-
4
- const test = require('tape')
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.same(kp1.publicKey.length, 32)
17
- t.same(kp2.publicKey.length, 32)
18
- t.notSame(kp1.publicKey, kp2.publicKey)
19
-
20
- t.end()
21
- })
22
-
23
- test('distinct tokens create distinct hypercore keypairs', async t => {
24
- const keys = await KeyManager.fromStorage(ram)
25
- const token1 = KeyManager.createToken()
26
- const token2 = KeyManager.createToken()
27
-
28
- const kp1 = await keys.createHypercoreKeyPair('core1', token1)
29
- const kp2 = await keys.createHypercoreKeyPair('core1', token2)
30
-
31
- t.notSame(kp1.publicKey, kp2.publicKey)
32
-
33
- t.end()
34
- })
35
-
36
- test('short user-provided token will throw', async t => {
37
- const keys = await KeyManager.fromStorage(ram)
38
-
39
- try {
40
- await keys.createHypercoreKeyPair('core1', Buffer.from('hello'))
41
- t.fail('did not throw')
42
- } catch {
43
- t.pass('threw correctly')
44
- }
45
-
46
- t.end()
47
- })
48
-
49
- test('persistent storage regenerates keys correctly', async t => {
50
- const testPath = p.resolve(__dirname, 'test-data')
51
-
52
- const keys1 = await KeyManager.fromStorage((name) => raf(testPath, { directory: testPath }))
53
- const kp1 = await keys1.createHypercoreKeyPair('core1')
54
-
55
- const keys2 = await KeyManager.fromStorage((name) => raf(testPath, { directory: testPath }))
56
- const kp2 = await keys2.createHypercoreKeyPair('core1')
57
-
58
- t.same(kp1.publicKey, kp2.publicKey)
59
-
60
- await fs.promises.rm(testPath, { recursive: true })
61
- t.end()
62
- })
63
-
64
- test('different master keys -> different keys', async t => {
65
- const keys1 = await KeyManager.fromStorage(ram)
66
- const keys2 = await KeyManager.fromStorage(ram)
67
-
68
- const kp1 = await keys1.createHypercoreKeyPair('core1')
69
- const kp2 = await keys2.createHypercoreKeyPair('core1')
70
-
71
- t.notSame(kp1.publicKey, kp2.publicKey)
72
-
73
- t.end()
74
- })