corestore 6.0.0-beta2 → 6.0.1-alpha.10

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