@browser-network/network
Advanced tools
Comparing version 0.3.3 to 0.4.0
@@ -23,2 +23,11 @@ /// <reference types="node" /> | ||
suppliedOfferNegotiation?: t.OfferNegotiation; | ||
/** | ||
* @description If supplied, the SDP info will be encrypted with EC public key | ||
* encryption so that only the foreign address can read it. This is important | ||
* because the SDP info contains sensitive IP address related information and is | ||
* passed all around the network, and is publicly available on the | ||
* switchboard. This should always be supplied when possible, namely when the | ||
* network is in encrypted mode. | ||
*/ | ||
secret?: t.Secret; | ||
}; | ||
@@ -38,5 +47,3 @@ export declare class Connection extends EventEmitter { | ||
* @description The public key crypto address of the node on the other side of this | ||
* connection. If there is no address on the connection, that means it's an | ||
* "open connection", one the node is keeping around and broadcasting | ||
* connection information from in RTC "offer" form. | ||
* connection. | ||
*/ | ||
@@ -80,2 +87,3 @@ address: t.Address; | ||
answer?: t.AnswerNegotiation | t.PendingAnswerNegotiation; | ||
private _secret; | ||
/** | ||
@@ -105,8 +113,20 @@ * @description A Connection represents the linking between two network nodes. | ||
get state(): STATE; | ||
_handleAnswerNegotiation(answer: t.AnswerNegotiation): Promise<void>; | ||
private get _isPending(); | ||
_handleAnswerNegotiation(answer: t.AnswerNegotiation): void; | ||
/** | ||
* @description If the network is running in encrypted mode, we're encrypting our SDP. So this | ||
* encrypts it if we're in encrypted mode. | ||
*/ | ||
private _conditionallyEncryptSdp; | ||
/** | ||
* @description Similar to above. | ||
*/ | ||
private _conditionallyDecryptSdp; | ||
} | ||
/** | ||
* @description A helper mainly for creating multiple connections simultaneously and waiting for them | ||
* to move out of their pending state | ||
* to move out of their pending state in convenient promise form | ||
* | ||
* TODO Can we just have a method on connection itself called untilReady() that resolves a promise when | ||
* it's in a ready state? | ||
*/ | ||
@@ -113,0 +133,0 @@ export declare abstract class ConnectionFactory { |
@@ -16,2 +16,36 @@ var __extends = (this && this.__extends) || (function () { | ||
})(); | ||
var __assign = (this && this.__assign) || function () { | ||
__assign = Object.assign || function(t) { | ||
for (var s, i = 1, n = arguments.length; i < n; i++) { | ||
s = arguments[i]; | ||
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) | ||
t[p] = s[p]; | ||
} | ||
return t; | ||
}; | ||
return __assign.apply(this, arguments); | ||
}; | ||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { | ||
if (k2 === undefined) k2 = k; | ||
var desc = Object.getOwnPropertyDescriptor(m, k); | ||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { | ||
desc = { enumerable: true, get: function() { return m[k]; } }; | ||
} | ||
Object.defineProperty(o, k2, desc); | ||
}) : (function(o, m, k, k2) { | ||
if (k2 === undefined) k2 = k; | ||
o[k2] = m[k]; | ||
})); | ||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { | ||
Object.defineProperty(o, "default", { enumerable: true, value: v }); | ||
}) : function(o, v) { | ||
o["default"] = v; | ||
}); | ||
var __importStar = (this && this.__importStar) || function (mod) { | ||
if (mod && mod.__esModule) return mod; | ||
var result = {}; | ||
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); | ||
__setModuleDefault(result, mod); | ||
return result; | ||
}; | ||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
@@ -62,3 +96,3 @@ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } | ||
else if (typeof define === "function" && define.amd) { | ||
define(["require", "exports", "uuid", "simple-peer", "events"], factory); | ||
define(["require", "exports", "uuid", "simple-peer", "events", "@browser-network/crypto"], factory); | ||
} | ||
@@ -72,2 +106,3 @@ })(function (require, exports) { | ||
var events_1 = __importDefault(require("events")); | ||
var bnc = __importStar(require("@browser-network/crypto")); | ||
var IS_NODE = typeof process !== 'undefined'; | ||
@@ -91,5 +126,6 @@ var Connection = /** @class */ (function (_super) { | ||
var _this = _super.call(this) || this; | ||
var networkId = props.networkId, selfAddress = props.selfAddress, foreignAddress = props.foreignAddress, suppliedOfferNegotiation = props.suppliedOfferNegotiation; | ||
var networkId = props.networkId, selfAddress = props.selfAddress, foreignAddress = props.foreignAddress, suppliedOfferNegotiation = props.suppliedOfferNegotiation, secret = props.secret; | ||
_this.id = (0, uuid_1.v4)(); | ||
_this.address = foreignAddress; | ||
_this._secret = secret; | ||
// bringing in wrtc here costs us 2kb in the build size. 0.9kb in the minified version. | ||
@@ -101,12 +137,26 @@ _this.peer = new simple_peer_1.default({ | ||
}); | ||
_this.peer.on('signal', function (data) { | ||
if (data.type === 'offer') { | ||
_this.offer.sdp = data.sdp; | ||
_this.emit('state-change'); | ||
} | ||
else if (data.type === 'answer') { | ||
_this.answer.sdp = data.sdp; | ||
_this.emit('state-change'); | ||
} | ||
}); | ||
_this.peer.on('signal', function (data) { return __awaiter(_this, void 0, void 0, function () { | ||
var _a, _b; | ||
return __generator(this, function (_c) { | ||
switch (_c.label) { | ||
case 0: | ||
if (!(data.type === 'offer')) return [3 /*break*/, 2]; | ||
_a = this.offer; | ||
return [4 /*yield*/, this._conditionallyEncryptSdp(data.sdp)]; | ||
case 1: | ||
_a.sdp = _c.sent(); | ||
this.emit('state-change'); | ||
return [3 /*break*/, 4]; | ||
case 2: | ||
if (!(data.type === 'answer')) return [3 /*break*/, 4]; | ||
_b = this.answer; | ||
return [4 /*yield*/, this._conditionallyEncryptSdp(data.sdp)]; | ||
case 3: | ||
_b.sdp = _c.sent(); | ||
this.emit('state-change'); | ||
_c.label = 4; | ||
case 4: return [2 /*return*/]; | ||
} | ||
}); | ||
}); }); | ||
_this.peer.on('data', function (data) { | ||
@@ -133,3 +183,7 @@ var str = data.toString(); | ||
}; | ||
_this.peer.signal(suppliedOfferNegotiation); | ||
_this._conditionallyDecryptSdp(suppliedOfferNegotiation.sdp).then(function (sdp) { | ||
var processed = __assign({}, suppliedOfferNegotiation); | ||
processed.sdp = sdp; | ||
_this.peer.signal(processed); | ||
}); | ||
} | ||
@@ -170,2 +224,25 @@ else { // We're fixing to be an open connection until another node answers us | ||
}); | ||
Connection.prototype._handleAnswerNegotiation = function (answer) { | ||
return __awaiter(this, void 0, void 0, function () { | ||
var _a; | ||
return __generator(this, function (_b) { | ||
switch (_b.label) { | ||
case 0: | ||
// Store our answer in encrypted form | ||
this.answer = answer; | ||
// If we're in encrypted mode, unencrypt the sdp, otherwise just return it | ||
_a = answer; | ||
return [4 /*yield*/, this._conditionallyDecryptSdp(answer.sdp) | ||
// Punch through that nat | ||
]; | ||
case 1: | ||
// If we're in encrypted mode, unencrypt the sdp, otherwise just return it | ||
_a.sdp = _b.sent(); | ||
// Punch through that nat | ||
this.peer.signal(answer); | ||
return [2 /*return*/]; | ||
} | ||
}); | ||
}); | ||
}; | ||
Object.defineProperty(Connection.prototype, "_isPending", { | ||
@@ -183,7 +260,27 @@ get: function () { | ||
}); | ||
Connection.prototype._handleAnswerNegotiation = function (answer) { | ||
// Punch through that nat | ||
this.answer = answer; | ||
this.peer.signal(answer); | ||
/** | ||
* @description If the network is running in encrypted mode, we're encrypting our SDP. So this | ||
* encrypts it if we're in encrypted mode. | ||
*/ | ||
Connection.prototype._conditionallyEncryptSdp = function (sdp) { | ||
return __awaiter(this, void 0, void 0, function () { | ||
return __generator(this, function (_a) { | ||
if (!this._secret) | ||
return [2 /*return*/, sdp]; | ||
return [2 /*return*/, bnc.encrypt(sdp, this.address)]; | ||
}); | ||
}); | ||
}; | ||
/** | ||
* @description Similar to above. | ||
*/ | ||
Connection.prototype._conditionallyDecryptSdp = function (sdp) { | ||
return __awaiter(this, void 0, void 0, function () { | ||
return __generator(this, function (_a) { | ||
if (!this._secret) | ||
return [2 /*return*/, sdp]; | ||
return [2 /*return*/, bnc.decrypt(sdp, this._secret)]; | ||
}); | ||
}); | ||
}; | ||
return Connection; | ||
@@ -194,3 +291,6 @@ }(events_1.default)); | ||
* @description A helper mainly for creating multiple connections simultaneously and waiting for them | ||
* to move out of their pending state | ||
* to move out of their pending state in convenient promise form | ||
* | ||
* TODO Can we just have a method on connection itself called untilReady() that resolves a promise when | ||
* it's in a ready state? | ||
*/ | ||
@@ -197,0 +297,0 @@ var ConnectionFactory = /** @class */ (function () { |
@@ -28,3 +28,3 @@ import * as t from './types.d'; | ||
}; | ||
declare type SecureNetworkProps = CommonNetworkProps & { | ||
export declare type SecureNetworkProps = CommonNetworkProps & { | ||
/** | ||
@@ -37,3 +37,3 @@ * The EC private key that identifies this node on the network. From this, | ||
}; | ||
declare type InsecureNetworkProps = CommonNetworkProps & { | ||
export declare type InsecureNetworkProps = CommonNetworkProps & { | ||
/** | ||
@@ -45,3 +45,3 @@ * @description An arbitrary string used as an identifying address. If this | ||
}; | ||
declare type NetworkProps = InsecureNetworkProps | SecureNetworkProps; | ||
export declare type NetworkProps = InsecureNetworkProps | SecureNetworkProps; | ||
declare type MinimumMessage = Partial<Message> & { | ||
@@ -86,3 +86,3 @@ type: Message['type']; | ||
on(type: 'add-connection', handler: (connection: Connection) => void): void; | ||
on(type: 'destroy-connection', handler: (id: Connection['id']) => void): void; | ||
on(type: 'destroy-connection', handler: (connection: Connection) => void): void; | ||
on(type: 'switchboard-response', handler: (book: t.SwitchboardResponse) => void): void; | ||
@@ -89,0 +89,0 @@ on(type: 'connection-error', handler: ({ description: string, error: Error }: { |
@@ -133,4 +133,4 @@ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { | ||
presenceBroadcastInterval: 1000 * 5, | ||
fastSwitchboardRequestInterval: 500, | ||
slowSwitchboardRequestInterval: 1000 * 3, | ||
fastSwitchboardRequestInterval: 1000 * 1, | ||
slowSwitchboardRequestInterval: 1000 * 5, | ||
garbageCollectInterval: 1000 * 5, | ||
@@ -272,2 +272,3 @@ maxMessageRateBeforeRude: Infinity, | ||
this._messageMemory.add(toBroadcast.id); | ||
// TODO move this to Connection | ||
for (_i = 0, _c = this.activeConnections; _i < _c.length; _i++) { | ||
@@ -366,3 +367,4 @@ connection = _c[_i]; | ||
foreignAddress: item.from, | ||
suppliedOfferNegotiation: item.negotiation | ||
suppliedOfferNegotiation: item.negotiation, | ||
secret: _this._secret | ||
}); | ||
@@ -376,3 +378,3 @@ } | ||
if ((con === null || con === void 0 ? void 0 : con.state) === 'open') { | ||
_this._emit('connection-process', "Signaling initiator connection to ".concat(item.from, ", connectionId: ").concat(con.id)); | ||
_this._emit('connection-process', "switchboard process: Signaling initiator connection to ".concat(item.from, ", connectionId: ").concat(con.id)); | ||
con._handleAnswerNegotiation(item.negotiation); | ||
@@ -390,7 +392,9 @@ } | ||
_this._emit('connection-process', "switchboard process: creating new initiator connection to ".concat(address)); | ||
return Connection_1.ConnectionFactory.new({ | ||
var opts = { | ||
networkId: _this.networkId, | ||
selfAddress: _this.address, | ||
foreignAddress: address | ||
}); | ||
foreignAddress: address, | ||
secret: _this._secret | ||
}; | ||
return Connection_1.ConnectionFactory.new(opts); | ||
}).filter(Boolean); | ||
@@ -439,2 +443,6 @@ return [4 /*yield*/, Promise.all(newAnswerConnections)]; | ||
this._presenceBroadcastInterval = setInterval(function () { | ||
// We don't need to even broadcast our presence if we've hit our max connections | ||
if (_this.activeConnections.length >= _this.config.maxConnections) { | ||
return; | ||
} | ||
_this._broadcastInternal({ | ||
@@ -476,6 +484,2 @@ type: 'presence', | ||
this._messageMemory.add(message.id); | ||
// Only handle messages meant for either us or everybody | ||
if (!['*', this.address].includes(message.destination)) { | ||
return [2 /*return*/]; | ||
} | ||
if (!this._secret) return [3 /*break*/, 4]; | ||
@@ -505,2 +509,13 @@ // Firstly, if there are no signatures, it is not sound. | ||
case 4: | ||
// Instead of decrementing the ttl value, since the signatures depend on it | ||
// staying the same, we count the signatures to see how many hops the message | ||
// has taken. | ||
if (message.signatures.length < message.ttl) { | ||
this._broadcastInternal(message); | ||
} | ||
this._emit('message', message); | ||
// From here on out, we're only concerned with messages meant for either us or everybody | ||
if (!['*', this.address].includes(message.destination)) { | ||
return [2 /*return*/]; | ||
} | ||
massage = message; | ||
@@ -527,9 +542,2 @@ if (message.appId === APP_ID) { | ||
} | ||
// Instead of decrementing the ttl value, since the signatures depend on it | ||
// staying the same, we count the signatures to see how many hops the message | ||
// has taken. | ||
if (message.signatures.length < message.ttl) { | ||
this._broadcastInternal(message); | ||
} | ||
this._emit('message', message); | ||
return [2 /*return*/]; | ||
@@ -573,3 +581,4 @@ } | ||
selfAddress: this.address, | ||
foreignAddress: message.address | ||
foreignAddress: message.data.address, | ||
secret: this._secret | ||
})]; | ||
@@ -579,4 +588,9 @@ case 1: | ||
this.registerConnection(connection); | ||
this._emit('connection-process', "broadcasting offer message to ".concat(message.address, ", connectionId: ").concat(connection.id.slice(0, 5), "...")); | ||
this._broadcastInternal({ appId: APP_ID, type: 'offer', data: connection.offer }); | ||
this._emit('connection-process', "broadcasting offer message to ".concat(message.data.address, ", connectionId: ").concat(connection.id.slice(0, 5), "...")); | ||
this._broadcastInternal({ | ||
type: 'offer', | ||
appId: APP_ID, | ||
destination: message.data.address, | ||
data: connection.offer | ||
}); | ||
return [2 /*return*/]; | ||
@@ -600,2 +614,6 @@ } | ||
} | ||
// And we don't need to field an offer if we're full up | ||
if (this.activeConnections.length >= this.config.maxConnections) { | ||
return [2 /*return*/]; | ||
} | ||
this._emit('connection-process', "received offer message from ".concat(message.address)); | ||
@@ -616,4 +634,5 @@ inactiveConnections = this.connections.filter(function (con) { | ||
selfAddress: this.address, | ||
foreignAddress: message.address, | ||
suppliedOfferNegotiation: message.data | ||
foreignAddress: message.data.address, | ||
suppliedOfferNegotiation: message.data, | ||
secret: this._secret | ||
})]; | ||
@@ -624,3 +643,8 @@ case 1: | ||
this._emit('connection-process', "broadcasting answer message to ".concat(message.address, ", connectionId: ").concat(connection.id.slice(0, 5), "...")); | ||
this._broadcastInternal({ appId: APP_ID, type: 'answer', data: connection.answer }); | ||
this._broadcastInternal({ | ||
destination: message.data.address, | ||
appId: APP_ID, | ||
type: 'answer', | ||
data: connection.answer | ||
}); | ||
return [2 /*return*/]; | ||
@@ -638,2 +662,6 @@ } | ||
} | ||
// We may have broadcasted an offer and in the interim filled up on connections | ||
if (this.activeConnections.length >= this.config.maxConnections) { | ||
return; | ||
} | ||
var connection = this.connections.find(function (con) { | ||
@@ -661,3 +689,2 @@ return con.address === message.address && // for us | ||
connection.peer.on('connect', function () { | ||
_this._emit('add-connection', connection); | ||
// Let's take this opportunity to remove any other connections with the | ||
@@ -671,2 +698,9 @@ // same address that aren't connected. Keep the place clean. It's possible | ||
}); | ||
// Sometimes because of the racy nature of connecting we end up having more connections | ||
// than we bargained for. If that's the case, we'll just nip this one right in the bud. | ||
if (_this.activeConnections.length > _this.config.maxConnections) { | ||
connection.peer.destroy(); | ||
return; | ||
} | ||
_this._emit('add-connection', connection); | ||
// Send a welcome log message for the warm fuzzies | ||
@@ -722,3 +756,3 @@ _this._broadcastInternal({ | ||
delete this._connections[connection.id]; | ||
this._emit('destroy-connection', connection.id); | ||
this._emit('destroy-connection', connection); | ||
}; | ||
@@ -725,0 +759,0 @@ // Get ANY connection we have, no matter the state it's in. |
{ | ||
"name": "@browser-network/network", | ||
"version": "0.3.3", | ||
"version": "0.4.0", | ||
"description": "A WebRTC based direct peer to peer network in the browser.", | ||
@@ -45,6 +45,5 @@ "main": "./dist/src/index.js", | ||
"dependencies": { | ||
"@browser-network/crypto": "^0.0.1", | ||
"@browser-network/crypto": "^0.0.4", | ||
"@mapbox/node-pre-gyp": "^1.0.9", | ||
"axios": "^0.26.1", | ||
"eccrypto": "^1.1.6", | ||
"simple-peer": "^9.11.1", | ||
@@ -55,3 +54,2 @@ "uuid": "^8.3.2", | ||
"devDependencies": { | ||
"@types/eccrypto": "^1.1.3", | ||
"@types/node": "^16", | ||
@@ -58,0 +56,0 @@ "@types/simple-peer": "^9.11.4", |
@@ -30,3 +30,8 @@ # Browser Network | ||
> Therefore is not hampered by the switchboard's ability to simultaneously hold | ||
many websocket connections. | ||
many websocket connections. This is a huge drawback of networks that do use | ||
a websocket switchboard. The network can only be as big as that switchboard's | ||
address space, and if the switchboard goes down, the network will start to fall | ||
apart. They're still distributed networks, but they rely heavily on a single | ||
server entity. This network still relies on a server switchboard, but its self | ||
healing quality leads to a robust network even if the switchboard goes down. | ||
@@ -88,7 +93,15 @@ The Network can be dropped into any web app via | ||
* Cryptographic security - Network uses `eccrypto` to ensure veracity of messages. | ||
It's cryptographically difficult to spoof or modify a message that's not your own. | ||
This feature can be turned on for more security or off for faster performance if | ||
your network doesn't need to be secure. | ||
* Cryptographic security - Network uses elliptic curve public key encryption to: | ||
- Ensure the veracity of messages: It's cryptographically difficult to | ||
spoof or modify a message that's not your own. | ||
- Encrypt sensitive IP information: The network passes around SDP strings, information | ||
pertaining to WebRTC connections, which contains IP addresses. These are passed | ||
through nodes to get to a node that another is not directly connected to. | ||
To ensure no snooping, ECIES is used to encrypt the SDP information so that only | ||
the recipient it's meant for can read it. | ||
- These features can be turned on for more security or off for faster | ||
performance if your network doesn't care about these security features and | ||
it needs to rapidly send messages, for whatever reason. | ||
### How it works | ||
@@ -99,27 +112,10 @@ | ||
Once we connect to another node that's in a network (by we here, I mean a node, | ||
if the reader will allow), then we'll start to hear [messages](#messages) from | ||
our "neighbor" nodes, which is to say, those in the network we're directly | ||
connected to. The messages may originally come from those neighbors or they may not. Each | ||
message has a ttl (time to live). If we receive a message with a ttl > 1, we decrement it and | ||
pass it along on to our neighbors. In this way, the whole network can receive | ||
messages even though not every node is connected to each other. | ||
Once we connect to another node that's in a network we'll start to hear | ||
[messages](#messages) from the whole network, including nodes we're not connected to. | ||
Some of the messages we'll be hearing will be open connection information | ||
(rtc "offer" SDP info). If one of those is for someone we're not yet connected | ||
to, we'll generate a response (rtc "answer" SDP info based on the original offer), | ||
and send that response back out into the network to the node that originally sent it. | ||
If they receive it, a direct connection will be established. It's by this means | ||
that the network is self healing. | ||
Some of the messages we'll be hearing will be from nodes we're not directly | ||
connected to. In that case, the we'll rapidly negotiate a connection with them | ||
by relaying our negotiation via the nodes we are mutually connected to. It's by | ||
this means that the network is self healing. | ||
There are various schemes in place for efficiency. | ||
- Message id memory so as not to repeat rebroadcasts of messages | ||
- A rude list. If you get on the rude list, you get dropped and blocked. | ||
- Connection garbage collection. WebRTC connections are unstable. A garbage | ||
collector periodically cleans bad connections making room for new ones. | ||
- Tunable max connections - dial up or down the max number of connections you want | ||
to have in real time. Network won't make any new connections while there | ||
are more than that setting (`config.maxConnections`). | ||
### The Switching Service | ||
@@ -134,2 +130,8 @@ | ||
Once a node is connected to the network, it slows way down on its checking in | ||
with the switchboard. This helps regulate traffic to the switchboard while ensuring | ||
a speedy initial connection with the network. Once that initial connection is made, | ||
the node doesn't need to communicate with the switchboard at all, except to help | ||
future nodes discover the network. | ||
The switching service has negligable processing and memory footprints. It | ||
@@ -174,3 +176,3 @@ operates only in memory, it doesn't need a database or write to disk in any | ||
address: 'my-address-' + Date.now(), // Each window should have its own address, hence the Date.now() | ||
networkId: 'test-network' // Everyone using this id will receive messages from each other | ||
networkId: 'test-network' // Everyone using this id on the same switchboard will receive messages from each other | ||
}) | ||
@@ -265,9 +267,3 @@ | ||
// See more below... | ||
config:{ | ||
offerBroadcastInterval: 1000 * 5, | ||
switchboardRequestInterval: 1000 * 5, | ||
garbageCollectInterval: 1000 * 5, | ||
maxMessageRateBeforeRude: 1000, | ||
maxConnections: 10 | ||
} | ||
config: {} | ||
}) | ||
@@ -327,6 +323,6 @@ ``` | ||
Network also exposes a way to see all of the connections currently established: | ||
Network also exposes a getter to see all of the connections currently established: | ||
```ts | ||
network.connections() // -> Connection[] | ||
network.activeConnections // -> Connection[] | ||
``` | ||
@@ -354,2 +350,4 @@ | ||
a security vulnerability. | ||
* Ability to export an offer/answer into an easily exchanged, reasonably sized string. | ||
This would allow any switch at all to be used to create a connection, easily, breaking | ||
the reliance on any specific switchboard mechanism. |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
1648177
6
14
30301
346
+ Added@browser-network/crypto@0.0.4(transitive)
- Removedeccrypto@^1.1.6
- Removed@browser-network/crypto@0.0.1(transitive)