initial commit

This commit is contained in:
Daniel Lugo 2019-11-27 16:50:44 -04:00
parent 732a35ddf2
commit 3ee39e63c5
39 changed files with 15767 additions and 2 deletions

10
.babelrc Normal file
View file

@ -0,0 +1,10 @@
{
"env": {
"test": {
"plugins": [
"transform-es2015-modules-commonjs",
"@babel/plugin-proposal-class-properties"
]
}
}
}

70
.eslintrc.json Normal file
View file

@ -0,0 +1,70 @@
{
"extends": ["eslint:all", "prettier", "plugin:jest/all"],
"plugins": ["prettier", "jest", "babel"],
"rules": {
"prettier/prettier": "error",
"strict": "off",
"id-length": ["error", { "exceptions": ["_"] }],
"no-console": "off",
"max-statements": "off",
"global-require": "off",
"new-cap": "off",
"one-var": "off",
"max-lines-per-function": "off",
"no-underscore-dangle": "off",
"no-implicit-coercion": "off",
"no-magic-numbers": "off",
"no-negated-condition": "off",
"capitalized-comments": "off",
"max-params": "off",
"multiline-comment-style": "off",
"spaced-comment": "off",
"no-inline-comments": "off",
"sort-keys": "off",
"max-lines": "off",
"prefer-template": "off",
"callback-return": "off",
"no-ternary": "off",
"no-invalid-this": "off",
"babel/no-invalid-this": "error",
"complexity": "off",
"yoda": "off",
"prefer-promise-reject-errors": "off",
"camelcase": "off",
"consistent-return": "off",
"no-shadow": "off"
},
"parser": "babel-eslint",
"env": {
"node": true,
"es6": true
}
}

2
.gitattributes vendored Normal file
View file

@ -0,0 +1,2 @@
# Auto detect text files and perform LF normalization
* text=auto

9
.gitignore vendored Normal file
View file

@ -0,0 +1,9 @@
node_modules
services/auth/secrets.json
.env
*.log
.directory
radata/
*.cert
*.key

6
.prettierrc Normal file
View file

@ -0,0 +1,6 @@
{
"requirePragma": true,
"semi": false,
"singleQuote": true,
"endOfLine": "lf"
}

3
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,3 @@
{
"eslint.enable": true
}

View file

@ -1,2 +1,29 @@
# api
backend for wallet that communicates with gun+lnd
This is an early test release of the [ShockWallet](shockwallet.app) API.
For easy setup on your Laptop/Desktop, [a wizard is available here.](https://github.com/shocknet/wizard)
### Manual Installation
#### Notes:
* The service defaults to port `9835`
* Looks for local LND in its default path
* Default gun peer is `gun.shock.network`
* Change defaults in `defaults.js`
* Requires [Node.js](https://nodejs.org) LTS
#### Steps:
1) Run [LND](https://github.com/lightningnetwork/lnd/releases) - *Example testnet startup*:
```./lnd --bitcoin.active --bitcoin.testnet --bitcoin.node=neutrino --neutrino.connect=faucet.lightning.community```
2) Download and Install API
```
git pull https://github.com/shocknet/api
cd api
yarn install
```
3) Run with `node main -h 0.0.0.0`
4) Connect with ShockWallet

52
config/defaults.js Normal file
View file

@ -0,0 +1,52 @@
const os = require("os");
const path = require("path");
const platform = os.platform();
const homeDir = os.homedir();
const getLndDirectory = () => {
if (platform === "darwin") {
return homeDir + "/Library/Application Support/Lnd";
} else if (platform === "win32") {
// eslint-disable-next-line no-process-env
const { APPDATA = "" } = process.env;
return path.resolve(APPDATA, "../Local/Lnd");
}
return homeDir + "/.lnd";
};
const parsePath = (filePath = "") => {
if (platform === "win32") {
return filePath.replace("/", "\\");
}
return filePath;
};
const lndDirectory = getLndDirectory();
module.exports = (mainnet = false) => {
const network = mainnet ? "mainnet" : "testnet";
return {
serverPort: 9835,
serverHost: "localhost",
sessionSecret: "my session secret",
sessionMaxAge: 300000,
lndAddress: "127.0.0.1:9735",
maxNumRoutesToQuery: 20,
lndProto: parsePath(`${__dirname}/rpc.proto`),
lndHost: "localhost:10009",
lndCertPath: parsePath(`${lndDirectory}/tls.cert`),
macaroonPath: parsePath(
`${lndDirectory}/data/chain/bitcoin/${network}/admin.macaroon`
),
dataPath: parsePath(`${lndDirectory}/data`),
loglevel: "info",
logfile: "shockapi.log",
lndLogFile: parsePath(`${lndDirectory}/logs/bitcoin/${network}/lnd.log`),
lndDirPath: lndDirectory,
peers: ["http://gun.shock.network:8765/gun"],
tokenExpirationMS: 4500000
};
};

22
config/log.js Normal file
View file

@ -0,0 +1,22 @@
// config/log.js
const winston = require("winston");
require("winston-daily-rotate-file");
module.exports = function(logFileName, logLevel) {
winston.cli();
winston.level = logLevel;
winston.add(winston.transports.DailyRotateFile, {
filename: logFileName,
datePattern: "yyyy-MM-dd.",
prepend: true,
json: false,
maxSize: 1000000,
maxFiles: 7,
level: logLevel
});
return winston;
};

2799
config/rpc.proto Normal file

File diff suppressed because it is too large Load diff

18
constants/errors.js Normal file
View file

@ -0,0 +1,18 @@
module.exports = {
MACAROON_PATH: path => `
The specified macaroon path "${path}" was not found.
This issue can be caused by:
1. Setting an invalid path for your Macaroon file.
2. Not initializing your wallet before using the ShockAPI
`,
CERT_PATH: path => `
The specified LND certificate file "${path}" was not found.
This issue can be caused by:
1. Setting an invalid path for your Certificates.
2. Not initializing your wallet before using the ShockAPI
`,
CERT_MISSING: () =>
"Required LND certificate path missing from application configuration."
};

29
main.js Normal file
View file

@ -0,0 +1,29 @@
const program = require("commander");
// parse command line parameters
program
.version("1.0.0")
.option("-s, --serverport [port]", "web server http listening port (defaults to 8280)")
.option("-x, --httpsport [port]", "web server https listening port (defaults to 8283)")
.option("-h, --serverhost [host]", "web server listening host (defaults to localhost)")
.option("-l, --lndhost [host:port]", "RPC lnd host (defaults to localhost:10009)")
.option("-t, --usetls [path]", "path to a directory containing key.pem and cert.pem files")
.option("-u, --user [login]", "basic authentication login")
.option("-p, --pwd [password]", "basic authentication password")
.option("-r, --limituser [login]", "basic authentication login for readonly account")
.option("-w, --limitpwd [password]", "basic authentication password for readonly account")
.option("-m, --macaroon-path [file path]", "path to admin.macaroon file")
.option("-d, --lnd-cert-path [file path]", "path to LND cert file")
.option("-f, --logfile [file path]", "path to file where to store the application logs")
.option("-e, --loglevel [level]", "level of logs to display (debug, info, warn, error)")
.option("-n, --lndlogfile <file path>", "path to lnd log file to send to browser")
.option("-k, --le-email [email]", "lets encrypt required contact email")
.option("-c, --mainnet", "run server on mainnet mode")
.parse(process.argv);
// load server
if (program.serverhost && program.leEmail) {
require("./app/server-le")(program); // Let"s Encrypt server version
} else {
require("./src/server")(program); // Standard server version
}

82
package.json Normal file
View file

@ -0,0 +1,82 @@
{
"name": "sw-server",
"version": "1.0.0",
"description": "",
"main": "src/server.js",
"scripts": {
"start": "node --experimental-modules src/server.js",
"dev": "node main.js -h 0.0.0.0",
"dev:watch": "nodemon main.js -- -h 0.0.0.0",
"test": "jest --no-cache",
"test:watch": "jest --no-cache --watch",
"typecheck": "tsc",
"eslint": "eslint services/**/*.js src/**/*.js utils/**/*.js config/**/*.js constants/**/*.js",
"lint": "eslint \"services/gunDB/**/*.js\"",
"format": "prettier --write \"./**/*.js\""
},
"husky": {
"hooks": {
"precommit": "eslint"
}
},
"author": "",
"license": "ISC",
"dependencies": {
"@grpc/proto-loader": "^0.5.1",
"axios": "^0.19.0",
"basic-auth": "^2.0.0",
"bitcore-lib": "^0.15.0",
"body-parser": "^1.16.0",
"colors": "^1.3.0",
"command-exists": "^1.2.6",
"commander": "^2.9.0",
"cors": "^2.8.4",
"debug": "^3.1.0",
"dotenv": "^8.1.0",
"express": "^4.14.1",
"express-session": "^1.15.1",
"google-proto-files": "^1.0.3",
"graphviz": "0.0.8",
"grpc": "^1.21.1",
"gun": "^0.2019.1120",
"husky": "^3.0.9",
"jsonfile": "^4.0.0",
"jsonwebtoken": "^8.3.0",
"localtunnel": "^1.9.0",
"lodash": "^4.17.15",
"method-override": "^2.3.7",
"promise": "^8.0.1",
"request": "^2.87.0",
"request-promise": "^4.2.2",
"response-time": "^2.3.2",
"shelljs": "^0.8.2",
"socket.io": "^2.1.1",
"text-encoding": "^0.7.0",
"tingodb": "^0.6.1",
"winston": "^2.3.1",
"winston-daily-rotate-file": "^1.4.4"
},
"devDependencies": {
"@babel/plugin-proposal-class-properties": "^7.5.5",
"@types/dotenv": "^6.1.1",
"@types/express": "^4.17.1",
"@types/gun": "^0.9.1",
"@types/jest": "^24.0.18",
"@types/lodash": "^4.14.141",
"@types/socket.io": "^2.1.3",
"@types/socket.io-client": "^1.4.32",
"@types/uuid": "^3.4.5",
"babel-eslint": "^10.0.3",
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.2",
"eslint": "^6.6.0",
"eslint-config-prettier": "^6.5.0",
"eslint-plugin-babel": "^5.3.0",
"eslint-plugin-jest": "^22.20.1",
"eslint-plugin-prettier": "^3.1.1",
"jest": "^24.9.0",
"nodemon": "^1.19.3",
"prettier": "^1.18.2",
"socket.io-client": "^2.2.0",
"typescript": "^3.6.3"
}
}

129
services/auth/auth.js Normal file
View file

@ -0,0 +1,129 @@
/*
* @prettier
*/
// @ts-nocheck
const jwt = require('jsonwebtoken')
const uuidv1 = require('uuid/v1')
const jsonfile = require('jsonfile')
const path = require('path')
const logger = require('winston')
const FS = require('../../utils/fs')
const secretsFilePath = path.resolve(__dirname, 'secrets.json')
class Auth {
verifySecretsFile = async () => {
try {
const fileExists = await FS.access(secretsFilePath)
if (!fileExists) {
return { exists: false }
}
const secretsFile = await FS.readFile(secretsFilePath, {
encoding: 'utf8'
})
// Check if secrets file has valid JSON
JSON.parse(secretsFile)
return { exists: true, parsable: true }
} catch (err) {
console.error(err)
return { exists: true, parsable: false }
}
}
initSecretsFile = async () => {
const { exists, parsable } = await this.verifySecretsFile()
if (exists && parsable) {
return true
}
if (exists && !parsable) {
await FS.unlink(secretsFilePath)
}
await FS.writeFile(secretsFilePath, '{}')
logger.info('New secrets file generated!')
return true
}
readSecrets = () =>
new Promise((resolve, reject) => {
this.initSecretsFile().then(() => {
jsonfile.readFile(secretsFilePath, (err, allSecrets) => {
if (err) {
logger.info('readSecrets err', err)
reject('Problem reading secrets file')
} else {
resolve(allSecrets)
}
})
})
})
writeSecrets(key, value) {
return this.initSecretsFile()
.then(() => this.readSecrets())
.then(allSecrets => {
return new Promise((resolve, reject) => {
allSecrets[key] = value
jsonfile.writeFile(
secretsFilePath,
allSecrets,
{ spaces: 2, EOL: '\r\n' },
err => {
if (err) {
logger.info('writeSecrets err', err)
reject(err)
} else {
resolve(true)
}
}
)
})
})
}
async generateToken() {
const timestamp = Date.now()
const secret = uuidv1()
const token = jwt.sign(
{
data: { timestamp }
},
secret,
{ expiresIn: '500h' }
)
await this.writeSecrets(timestamp, secret)
return token
}
async validateToken(token) {
try {
await this.initSecretsFile()
const key = jwt.decode(token).data.timestamp
const secrets = await this.readSecrets()
const secret = secrets[key]
return new Promise((resolve, reject) => {
jwt.verify(token, secret, (err, decoded) => {
if (err) {
logger.info('validateToken err', err)
reject(err)
} else {
logger.info('decoded', decoded)
resolve({ valid: true })
}
})
})
} catch (err) {
console.error(err)
throw err
}
}
}
module.exports = new Auth()

View file

@ -0,0 +1,917 @@
/**
* @format
*/
const Gun = require('gun')
const debounce = require('lodash/debounce')
const once = require('lodash/once')
/** @type {import('../contact-api/SimpleGUN').ISEA} */
// @ts-ignore
const SEAx = require('gun/sea')
/** @type {import('../contact-api/SimpleGUN').ISEA} */
const mySEA = {}
const $$__SHOCKWALLET__MSG__ = '$$__SHOCKWALLET__MSG__'
const $$__SHOCKWALLET__ENCRYPTED__ = '$$_SHOCKWALLET__ENCRYPTED__'
mySEA.encrypt = (msg, secret) => {
if (typeof msg !== 'string') {
throw new TypeError('mySEA.encrypt() -> expected msg to be an string')
}
if (msg.length === 0) {
throw new TypeError(
'mySEA.encrypt() -> expected msg to be a populated string'
)
}
// Avoid this: https://github.com/amark/gun/issues/804 and any other issues
const sanitizedMsg = $$__SHOCKWALLET__MSG__ + msg
return SEAx.encrypt(sanitizedMsg, secret).then(encMsg => {
return $$__SHOCKWALLET__ENCRYPTED__ + encMsg
})
}
mySEA.decrypt = (encMsg, secret) => {
if (typeof encMsg !== 'string') {
throw new TypeError('mySEA.encrypt() -> expected encMsg to be an string')
}
if (encMsg.length === 0) {
throw new TypeError(
'mySEA.encrypt() -> expected encMsg to be a populated string'
)
}
if (typeof secret !== 'string') {
throw new TypeError('mySea.decrypt() -> expected secret to be an string')
}
if (secret.length === 0) {
throw new TypeError(
'mySea.decrypt() -> expected secret to be a populated string'
)
}
if (encMsg.indexOf($$__SHOCKWALLET__ENCRYPTED__) !== 0) {
throw new TypeError(
'Trying to pass a non prefixed encrypted string to mySea.decrypt(): ' +
encMsg
)
}
return SEAx.decrypt(
encMsg.slice($$__SHOCKWALLET__ENCRYPTED__.length),
secret
).then(decodedMsg => {
if (typeof decodedMsg !== 'string') {
throw new TypeError('Could not decrypt')
}
return decodedMsg.slice($$__SHOCKWALLET__MSG__.length)
})
}
mySEA.secret = (recipientOrSenderEpub, recipientOrSenderSEA) => {
if (recipientOrSenderEpub === recipientOrSenderSEA.pub) {
throw new Error('Do not use pub for mysecret')
}
return SEAx.secret(recipientOrSenderEpub, recipientOrSenderSEA).then(sec => {
if (typeof sec !== 'string') {
throw new TypeError('Could not generate secret')
}
return sec
})
}
const auth = require('../../auth/auth')
const Action = require('../action-constants.js')
const API = require('../contact-api/index')
const Config = require('../config')
const Event = require('../event-constants')
/**
* @typedef {import('../contact-api/SimpleGUN').GUNNode} GUNNode
* @typedef {import('../contact-api/SimpleGUN').UserGUNNode} UserGUNNode
*/
/**
* @typedef {object} Emission
* @prop {boolean} ok
* @prop {string|null|Record<string, any>} msg
* @prop {Record<string, any>} origBody
*/
/**
* @typedef {object} SimpleSocket
* @prop {(eventName: string, data: Emission) => void} emit
* @prop {(eventName: string, handler: (data: any) => void) => void} on
*/
/* eslint-disable init-declarations */
/** @type {GUNNode} */
// @ts-ignore
let gun
/** @type {UserGUNNode} */
let user
/* eslint-enable init-declarations */
let _currentAlias = ''
let _currentPass = ''
let _isAuthenticating = false
let _isRegistering = false
const isAuthenticated = () => typeof user.is === 'object' && user.is !== null
const isAuthenticating = () => _isAuthenticating
const isRegistering = () => _isRegistering
/**
* Returns a promise containing the public key of the newly created user.
* @param {string} alias
* @param {string} pass
* @returns {Promise<string>}
*/
const authenticate = async (alias, pass) => {
if (isAuthenticated()) {
// move this to a subscription; implement off() ? todo
API.Jobs.onAcceptedRequests(user, mySEA)
return user._.sea.pub
}
if (isAuthenticating()) {
throw new Error(
'Cannot authenticate while another authentication attempt is going on'
)
}
_isAuthenticating = true
const ack = await new Promise(res => {
user.auth(alias, pass, _ack => {
res(_ack)
})
})
_isAuthenticating = false
if (typeof ack.err === 'string') {
throw new Error(ack.err)
} else if (typeof ack.sea === 'object') {
API.Jobs.onAcceptedRequests(user, mySEA)
const mySec = await mySEA.secret(user._.sea.epub, user._.sea)
if (typeof mySec !== 'string') {
throw new TypeError('mySec not an string')
}
_currentAlias = user.is ? user.is.alias : ''
_currentPass = await mySEA.encrypt(pass, mySec)
await new Promise(res => setTimeout(res, 5000))
return ack.sea.pub
} else {
throw new Error('Unknown error.')
}
}
const instantiateGun = async () => {
let mySecret = ''
if (user && user.is) {
mySecret = /** @type {string} */ (await mySEA.secret(
user._.sea.epub,
user._.sea
))
}
if (typeof mySecret !== 'string') {
throw new TypeError("typeof mySec !== 'string'")
}
const _gun = new Gun({
axe: false,
peers: Config.PEERS
})
// please typescript
const __gun = /** @type {unknown} */ (_gun)
gun = /** @type {GUNNode} */ (__gun)
user = gun.user()
if (_currentAlias && _currentPass) {
const pass = await mySEA.decrypt(_currentPass, mySecret)
if (typeof pass !== 'string') {
throw new Error('could not decrypt stored in memory current pass')
}
user.leave()
await authenticate(_currentAlias, pass)
}
}
instantiateGun()
/**
* @param {string} token
* @returns {Promise<boolean>}
*/
const isValidToken = async token => {
const validation = await auth.validateToken(token)
if (typeof validation !== 'object') {
return false
}
if (validation === null) {
return false
}
if (typeof validation.valid !== 'boolean') {
return false
}
return validation.valid
}
/**
* @param {string} token
* @throws {Error} If the token is invalid
* @returns {Promise<void>}
*/
const throwOnInvalidToken = async token => {
const isValid = await isValidToken(token)
if (!isValid) {
throw new Error('Token expired.')
}
}
class Mediator {
/**
* @param {Readonly<SimpleSocket>} socket
*/
constructor(socket) {
this.socket = socket
this.connected = true
socket.on('disconnect', this.onDisconnect)
socket.on(Action.ACCEPT_REQUEST, this.acceptRequest)
socket.on(Action.BLACKLIST, this.blacklist)
socket.on(Action.GENERATE_NEW_HANDSHAKE_NODE, this.generateHandshakeNode)
socket.on(Action.SEND_HANDSHAKE_REQUEST, this.sendHandshakeRequest)
socket.on(
Action.SEND_HANDSHAKE_REQUEST_WITH_INITIAL_MSG,
this.sendHRWithInitialMsg
)
socket.on(Action.SEND_MESSAGE, this.sendMessage)
socket.on(Action.SET_AVATAR, this.setAvatar)
socket.on(Action.SET_DISPLAY_NAME, this.setDisplayName)
socket.on(Event.ON_AVATAR, this.onAvatar)
socket.on(Event.ON_BLACKLIST, this.onBlacklist)
socket.on(Event.ON_CHATS, this.onChats)
socket.on(Event.ON_DISPLAY_NAME, this.onDisplayName)
socket.on(Event.ON_HANDSHAKE_ADDRESS, this.onHandshakeAddress)
socket.on(Event.ON_RECEIVED_REQUESTS, this.onReceivedRequests)
socket.on(Event.ON_SENT_REQUESTS, this.onSentRequests)
}
/**
* @param {Readonly<{ requestID: string , token: string }>} body
*/
acceptRequest = async body => {
try {
const { requestID, token } = body
await throwOnInvalidToken(token)
await API.Actions.acceptRequest(requestID, gun, user, mySEA)
this.socket.emit(Action.ACCEPT_REQUEST, {
ok: true,
msg: null,
origBody: body
})
// refresh received requests
API.Events.onSimplerReceivedRequests(
debounce(
once(receivedRequests => {
if (Config.SHOW_LOG) {
console.log('---received requests---')
console.log(receivedRequests)
console.log('-----------------------')
}
this.socket.emit(Event.ON_RECEIVED_REQUESTS, {
msg: receivedRequests,
ok: true,
origBody: body
})
}),
300
),
gun,
user,
mySEA
)
} catch (err) {
console.log(err)
this.socket.emit(Action.ACCEPT_REQUEST, {
ok: false,
msg: err.message,
origBody: body
})
}
}
/**
* @param {Readonly<{ publicKey: string , token: string }>} body
*/
blacklist = async body => {
try {
const { publicKey, token } = body
await throwOnInvalidToken(token)
await API.Actions.blacklist(publicKey, user)
this.socket.emit(Action.BLACKLIST, {
ok: true,
msg: null,
origBody: body
})
} catch (err) {
console.log(err)
this.socket.emit(Action.BLACKLIST, {
ok: false,
msg: err.message,
origBody: body
})
}
}
onDisconnect = () => {
this.connected = false
}
/**
* @param {Readonly<{ token: string }>} body
*/
generateHandshakeNode = async body => {
try {
const { token } = body
await throwOnInvalidToken(token)
await API.Actions.generateHandshakeAddress(user)
this.socket.emit(Action.GENERATE_NEW_HANDSHAKE_NODE, {
ok: true,
msg: null,
origBody: body
})
} catch (err) {
console.log(err)
this.socket.emit(Action.GENERATE_NEW_HANDSHAKE_NODE, {
ok: false,
msg: err.message,
origBody: body
})
}
}
/**
* @param {Readonly<{ recipientPublicKey: string , token: string }>} body
*/
sendHandshakeRequest = async body => {
try {
if (Config.SHOW_LOG) {
console.log('\n')
console.log('------------------------------')
console.log('will now try to send a handshake request')
console.log('------------------------------')
console.log('\n')
}
const { recipientPublicKey, token } = body
await throwOnInvalidToken(token)
await API.Actions.sendHandshakeRequest(
recipientPublicKey,
gun,
user,
mySEA
)
if (Config.SHOW_LOG) {
console.log('\n')
console.log('------------------------------')
console.log('handshake request successfuly sent')
console.log('------------------------------')
console.log('\n')
}
this.socket.emit(Action.SEND_HANDSHAKE_REQUEST, {
ok: true,
msg: null,
origBody: body
})
API.Events.onSimplerSentRequests(
debounce(
once(srs => {
this.socket.emit(Event.ON_SENT_REQUESTS, {
ok: true,
msg: srs,
origBody: body
})
}),
350
),
gun,
user,
mySEA
)
} catch (err) {
if (Config.SHOW_LOG) {
console.log('\n')
console.log('------------------------------')
console.log('handshake request send fail: ' + err.message)
console.log('------------------------------')
console.log('\n')
}
this.socket.emit(Action.SEND_HANDSHAKE_REQUEST, {
ok: false,
msg: err.message,
origBody: body
})
}
}
/**
* @param {Readonly<{ initialMsg: string , recipientPublicKey: string , token: string }>} body
*/
sendHRWithInitialMsg = async body => {
try {
const { initialMsg, recipientPublicKey, token } = body
await throwOnInvalidToken(token)
await API.Actions.sendHRWithInitialMsg(
initialMsg,
recipientPublicKey,
gun,
user,
mySEA
)
this.socket.emit(Action.SEND_HANDSHAKE_REQUEST_WITH_INITIAL_MSG, {
ok: true,
msg: null,
origBody: body
})
} catch (err) {
console.log(err)
this.socket.emit(Action.SEND_HANDSHAKE_REQUEST_WITH_INITIAL_MSG, {
ok: false,
msg: err.message,
origBody: body
})
}
}
/**
* @param {Readonly<{ body: string , recipientPublicKey: string , token: string }>} reqBody
*/
sendMessage = async reqBody => {
try {
const { body, recipientPublicKey, token } = reqBody
await throwOnInvalidToken(token)
await API.Actions.sendMessage(recipientPublicKey, body, user, mySEA)
this.socket.emit(Action.SEND_MESSAGE, {
ok: true,
msg: null,
origBody: reqBody
})
} catch (err) {
console.log(err)
this.socket.emit(Action.SEND_MESSAGE, {
ok: false,
msg: err.message,
origBody: reqBody
})
}
}
/**
* @param {Readonly<{ avatar: string|null , token: string }>} body
*/
setAvatar = async body => {
try {
const { avatar, token } = body
await throwOnInvalidToken(token)
await API.Actions.setAvatar(avatar, user)
this.socket.emit(Action.SET_AVATAR, {
ok: true,
msg: null,
origBody: body
})
} catch (err) {
console.log(err)
this.socket.emit(Action.SET_AVATAR, {
ok: false,
msg: err.message,
origBody: body
})
}
}
/**
* @param {Readonly<{ displayName: string , token: string }>} body
*/
setDisplayName = async body => {
try {
const { displayName, token } = body
await throwOnInvalidToken(token)
await API.Actions.setDisplayName(displayName, user)
this.socket.emit(Action.SET_DISPLAY_NAME, {
ok: true,
msg: null,
origBody: body
})
} catch (err) {
console.log(err)
this.socket.emit(Action.SET_DISPLAY_NAME, {
ok: false,
msg: err.message,
origBody: body
})
}
}
//////////////////////////////////////////////////////////////////////////////
/**
* @param {Readonly<{ token: string }>} body
*/
onAvatar = async body => {
try {
const { token } = body
await throwOnInvalidToken(token)
API.Events.onAvatar(avatar => {
if (Config.SHOW_LOG) {
console.log('---avatar---')
console.log(avatar)
console.log('-----------------------')
}
this.socket.emit(Event.ON_AVATAR, {
msg: avatar,
ok: true,
origBody: body
})
}, user)
} catch (err) {
console.log(err)
this.socket.emit(Event.ON_AVATAR, {
ok: false,
msg: err.message,
origBody: body
})
}
}
/**
* @param {Readonly<{ token: string }>} body
*/
onBlacklist = async body => {
try {
const { token } = body
await throwOnInvalidToken(token)
API.Events.onBlacklist(blacklist => {
if (Config.SHOW_LOG) {
console.log('---blacklist---')
console.log(blacklist)
console.log('-----------------------')
}
this.socket.emit(Event.ON_BLACKLIST, {
msg: blacklist,
ok: true,
origBody: body
})
}, user)
} catch (err) {
console.log(err)
this.socket.emit(Event.ON_BLACKLIST, {
ok: false,
msg: err.message,
origBody: body
})
}
}
/**
* @param {Readonly<{ token: string }>} body
*/
onChats = async body => {
try {
const { token } = body
await throwOnInvalidToken(token)
API.Events.onChats(
chats => {
if (Config.SHOW_LOG) {
console.log('---chats---')
console.log(chats)
console.log('-----------------------')
}
this.socket.emit(Event.ON_CHATS, {
msg: chats,
ok: true,
origBody: body
})
},
gun,
user,
mySEA
)
} catch (err) {
console.log(err)
this.socket.emit(Event.ON_CHATS, {
ok: false,
msg: err.message,
origBody: body
})
}
}
/**
* @param {Readonly<{ token: string }>} body
*/
onDisplayName = async body => {
try {
const { token } = body
await throwOnInvalidToken(token)
API.Events.onDisplayName(displayName => {
if (Config.SHOW_LOG) {
console.log('---displayName---')
console.log(displayName)
console.log('-----------------------')
}
this.socket.emit(Event.ON_DISPLAY_NAME, {
msg: displayName,
ok: true,
origBody: body
})
}, user)
} catch (err) {
console.log(err)
this.socket.emit(Event.ON_DISPLAY_NAME, {
ok: false,
msg: err.message,
origBody: body
})
}
}
/**
* @param {Readonly<{ token: string }>} body
*/
onHandshakeAddress = async body => {
try {
const { token } = body
await throwOnInvalidToken(token)
API.Events.onCurrentHandshakeAddress(addr => {
if (Config.SHOW_LOG) {
console.log('---addr---')
console.log(addr)
console.log('-----------------------')
}
this.socket.emit(Event.ON_HANDSHAKE_ADDRESS, {
ok: true,
msg: addr,
origBody: body
})
}, user)
} catch (err) {
console.log(err)
this.socket.emit(Event.ON_HANDSHAKE_ADDRESS, {
ok: false,
msg: err.message,
origBody: body
})
}
}
/**
* @param {Readonly<{ token: string }>} body
*/
onReceivedRequests = async body => {
try {
const { token } = body
await throwOnInvalidToken(token)
API.Events.onSimplerReceivedRequests(
receivedRequests => {
if (Config.SHOW_LOG) {
console.log('---receivedRequests---')
console.log(receivedRequests)
console.log('-----------------------')
}
this.socket.emit(Event.ON_RECEIVED_REQUESTS, {
msg: receivedRequests,
ok: true,
origBody: body
})
},
gun,
user,
mySEA
)
} catch (err) {
console.log(err)
this.socket.emit(Event.ON_RECEIVED_REQUESTS, {
msg: err.message,
ok: false,
origBody: body
})
}
}
/**
* @param {Readonly<{ token: string }>} body
*/
onSentRequests = async body => {
try {
const { token } = body
await throwOnInvalidToken(token)
await API.Events.onSimplerSentRequests(
sentRequests => {
if (Config.SHOW_LOG) {
console.log('---sentRequests---')
console.log(sentRequests)
console.log('-----------------------')
}
this.socket.emit(Event.ON_SENT_REQUESTS, {
msg: sentRequests,
ok: true,
origBody: body
})
},
gun,
user,
mySEA
)
} catch (err) {
console.log(err)
this.socket.emit(Event.ON_SENT_REQUESTS, {
msg: err.message,
ok: false,
origBody: body
})
}
}
}
/**
* Creates an user for gun. Returns a promise containing the public key of the
* newly created user.
* @param {string} alias
* @param {string} pass
* @throws {Error} If gun is authenticated or is in the process of
* authenticating. Use `isAuthenticating()` and `isAuthenticated()` to check for
* this first. It can also throw if the alias is already registered on gun.
* @returns {Promise<string>}
*/
const register = async (alias, pass) => {
if (isRegistering()) {
throw new Error('Already registering.')
}
if (isAuthenticating()) {
throw new Error(
'Cannot register while gun is being authenticated (reminder: there should only be one user created for each node).'
)
}
if (isAuthenticated()) {
throw new Error(
'Cannot register if gun is already authenticated (reminder: there should only be one user created for each node).'
)
}
_isRegistering = true
/** @type {import('../contact-api/SimpleGUN').CreateAck} */
const ack = await new Promise(res =>
user.create(alias, pass, ack => res(ack))
)
_isRegistering = false
const mySecret = await mySEA.secret(user._.sea.epub, user._.sea)
if (typeof mySecret !== 'string') {
throw new Error('Could not generate secret for user.')
}
if (typeof ack.err === 'string') {
throw new Error(ack.err)
} else if (typeof ack.pub === 'string') {
_currentAlias = alias
_currentPass = await mySEA.encrypt(pass, mySecret)
} else {
throw new Error('unknown error')
}
// restart instances so write to user graph work, there's an issue with gun
// (at least on node) where after initial user creation, writes to user graph
// don't work
await instantiateGun()
user.leave()
return authenticate(alias, pass).then(async pub => {
await API.Actions.setDisplayName('anon' + pub.slice(0, 8), user)
await API.Actions.generateHandshakeAddress(user)
return pub
})
}
/**
* @param {SimpleSocket} socket
* @throws {Error} If gun is not authenticated or is in the process of
* authenticating. Use `isAuthenticating()` and `isAuthenticated()` to check for
* this first.
* @returns {Mediator}
*/
const createMediator = socket => {
// if (isAuthenticating() || !isAuthenticated()) {
// throw new Error("Gun must be authenticated to create a Mediator");
// }
return new Mediator(socket)
}
const getGun = () => {
return gun
}
const getUser = () => {
return user
}
module.exports = {
authenticate,
createMediator,
isAuthenticated,
isAuthenticating,
isRegistering,
register,
instantiateGun,
getGun,
getUser
}

View file

@ -0,0 +1,12 @@
const Actions = {
ACCEPT_REQUEST: "ACCEPT_REQUEST",
BLACKLIST: "BLACKLIST",
GENERATE_NEW_HANDSHAKE_NODE: "GENERATE_NEW_HANDSHAKE_NODE",
SEND_HANDSHAKE_REQUEST: "SEND_HANDSHAKE_REQUEST",
SEND_HANDSHAKE_REQUEST_WITH_INITIAL_MSG: "SEND_HANDSHAKE_REQUEST_WITH_INITIAL_MSG",
SEND_MESSAGE: "SEND_MESSAGE",
SET_AVATAR: "SET_AVATAR",
SET_DISPLAY_NAME: "SET_DISPLAY_NAME"
};
module.exports = Actions;

23
services/gunDB/config.js Normal file
View file

@ -0,0 +1,23 @@
/**
* @format
*/
/* eslint-disable no-process-env */
const dotenv = require('dotenv')
const defaults = require('../../config/defaults')(false)
dotenv.config()
// @ts-ignore Let it crash if undefined
exports.DATA_FILE_NAME = process.env.DATA_FILE_NAME || defaults.dataFileName
// @ts-ignore Let it crash if undefined
exports.PEERS = process.env.PEERS
? JSON.parse(process.env.PEERS)
: defaults.peers
exports.MS_TO_TOKEN_EXPIRATION = Number(
process.env.MS_TO_TOKEN_EXPIRATION || defaults.tokenExpirationMS
)
exports.SHOW_LOG = process.env.SHOW_GUN_DB_LOG === 'true'

View file

@ -0,0 +1,106 @@
/**
* @prettier
*/
type Primitive = boolean | string | number
export interface Data {
[K: string]: ValidDataValue
}
export type ValidDataValue = Primitive | null | Data
export interface Ack {
err: string | undefined
}
type ListenerObjSoul = {
'#': string
}
export type ListenerObj = Record<string, ListenerObjSoul | Primitive | null> & {
_: ListenerObjSoul
}
export type ListenerData = Primitive | null | ListenerObj | undefined
export type Listener = (data: ListenerData, key: string) => void
export type Callback = (ack: Ack) => void
export interface Soul {
get: string
put: Primitive | null | object | undefined
}
export interface GUNNode {
_: Soul
get(key: string): GUNNode
map(): GUNNode
put(data: ValidDataValue | GUNNode, cb?: Callback): GUNNode
on(this: GUNNode, cb: Listener): void
once(this: GUNNode, cb?: Listener): GUNNode
set(data: ValidDataValue | GUNNode, cb?: Callback): GUNNode
off(): void
user(): UserGUNNode
user(epub: string): GUNNode
then(): Promise<ListenerData>
then<T>(cb: (v: ListenerData) => T): Promise<ListenerData>
}
export interface CreateAck {
pub: string | undefined
err: string | undefined
}
export type CreateCB = (ack: CreateAck) => void
export interface AuthAck {
err: string | undefined
sea:
| {
pub: string
}
| undefined
}
export type AuthCB = (ack: AuthAck) => void
export interface UserPair {
epriv: string
epub: string
priv: string
pub: string
}
export interface UserSoul extends Soul {
sea: UserPair
}
export interface UserGUNNode extends GUNNode {
_: UserSoul
auth(user: string, pass: string, cb: AuthCB): void
is?: {
alias: string
pub: string
}
create(user: string, pass: string, cb: CreateCB): void
leave(): void
}
export interface ISEA {
encrypt(message: string, senderSecret: string): Promise<string>
decrypt(encryptedMessage: string, recipientSecret: string): Promise<string>
secret(
recipientOrSenderEpub: string,
recipientOrSenderUserPair: UserPair
): Promise<string>
}
export interface MySEA {
encrypt(message: string, senderSecret: string): Promise<string>
decrypt(encryptedMessage: string, recipientSecret: string): Promise<string>
secret(
recipientOrSenderEpub: string,
recipientOrSenderUserPair: UserPair
): Promise<string>
}

View file

@ -0,0 +1,798 @@
/**
* @format
*/
const uuidv1 = require('uuid/v1')
const ErrorCode = require('./errorCode')
const Key = require('./key')
const Utils = require('./utils')
const { isHandshakeRequest } = require('./schema')
/**
* @typedef {import('./SimpleGUN').GUNNode} GUNNode
* @typedef {import('./SimpleGUN').ISEA} ISEA
* @typedef {import('./SimpleGUN').UserGUNNode} UserGUNNode
* @typedef {import('./schema').HandshakeRequest} HandshakeRequest
* @typedef {import('./schema').StoredRequest} StoredReq
* @typedef {import('./schema').Message} Message
* @typedef {import('./schema').Outgoing} Outgoing
* @typedef {import('./schema').PartialOutgoing} PartialOutgoing
*/
/**
* An special message signaling the acceptance.
*/
const INITIAL_MSG = '$$__SHOCKWALLET__INITIAL__MESSAGE'
/**
* @returns {Message}
*/
const __createInitialMessage = () => ({
body: INITIAL_MSG,
timestamp: Date.now()
})
/**
* Create a an outgoing feed. The feed will have an initial special acceptance
* message. Returns a promise that resolves to the id of the newly-created
* outgoing feed.
*
* If an outgoing feed is already created for the recipient, then returns the id
* of that one.
* @param {string} withPublicKey Public key of the intended recipient of the
* outgoing feed that will be created.
* @throws {Error} If the outgoing feed cannot be created or if the initial
* message for it also cannot be created. These errors aren't coded as they are
* not meant to be caught outside of this module.
* @param {UserGUNNode} user
* @param {ISEA} SEA
* @returns {Promise<string>}
*/
const __createOutgoingFeed = async (withPublicKey, user, SEA) => {
if (!user.is) {
throw new Error(ErrorCode.NOT_AUTH)
}
const mySecret = await SEA.secret(user._.sea.epub, user._.sea)
if (typeof mySecret !== 'string') {
throw new TypeError(
"__createOutgoingFeed() -> typeof mySecret !== 'string'"
)
}
const encryptedForMeRecipientPub = await SEA.encrypt(withPublicKey, mySecret)
const maybeEncryptedForMeOutgoingFeedID = await Utils.tryAndWait(
(_, user) =>
new Promise(res => {
user
.get(Key.RECIPIENT_TO_OUTGOING)
.get(withPublicKey)
.once(data => {
res(data)
})
})
)
let outgoingFeedID = ''
// if there was no stored outgoing, create an outgoing feed
if (typeof maybeEncryptedForMeOutgoingFeedID !== 'string') {
/** @type {PartialOutgoing} */
const newPartialOutgoingFeed = {
with: encryptedForMeRecipientPub
}
/** @type {string} */
const newOutgoingFeedID = await new Promise((res, rej) => {
const _outFeedNode = user
.get(Key.OUTGOINGS)
.set(newPartialOutgoingFeed, ack => {
if (ack.err) {
rej(new Error(ack.err))
} else {
res(_outFeedNode._.get)
}
})
})
if (typeof newOutgoingFeedID !== 'string') {
throw new TypeError('typeof newOutgoingFeedID !== "string"')
}
await new Promise((res, rej) => {
user
.get(Key.OUTGOINGS)
.get(newOutgoingFeedID)
.get(Key.MESSAGES)
.set(__createInitialMessage(), ack => {
if (ack.err) {
rej(new Error(ack.err))
} else {
res()
}
})
})
const encryptedForMeNewOutgoingFeedID = await SEA.encrypt(
newOutgoingFeedID,
mySecret
)
if (typeof encryptedForMeNewOutgoingFeedID === 'undefined') {
throw new TypeError(
"typeof encryptedForMeNewOutgoingFeedID === 'undefined'"
)
}
await new Promise((res, rej) => {
user
.get(Key.RECIPIENT_TO_OUTGOING)
.get(withPublicKey)
.put(encryptedForMeNewOutgoingFeedID, ack => {
if (ack.err) {
rej(Error(ack.err))
} else {
res()
}
})
})
outgoingFeedID = newOutgoingFeedID
}
// otherwise decrypt stored outgoing
else {
const decryptedOID = await SEA.decrypt(
maybeEncryptedForMeOutgoingFeedID,
mySecret
)
if (typeof decryptedOID !== 'string') {
throw new TypeError(
"__createOutgoingFeed() -> typeof decryptedOID !== 'string'"
)
}
outgoingFeedID = decryptedOID
}
if (typeof outgoingFeedID === 'undefined') {
throw new TypeError(
'__createOutgoingFeed() -> typeof outgoingFeedID === "undefined"'
)
}
if (typeof outgoingFeedID !== 'string') {
throw new TypeError(
'__createOutgoingFeed() -> expected outgoingFeedID to be an string'
)
}
if (outgoingFeedID.length === 0) {
throw new TypeError(
'__createOutgoingFeed() -> expected outgoingFeedID to be a populated string.'
)
}
return outgoingFeedID
}
/**
* Given a request's ID, that should be found on the user's current handshake
* node, accept the request by creating an outgoing feed intended for the
* requestor, then encrypting and putting the id of this newly created outgoing
* feed on the response prop of the request.
* @param {string} requestID The id for the request to accept.
* @param {GUNNode} gun
* @param {UserGUNNode} user Pass only for testing purposes.
* @param {ISEA} SEA
* @param {typeof __createOutgoingFeed} outgoingFeedCreator Pass only
* for testing. purposes.
* @throws {Error} Throws if trying to accept an invalid request, or an error on
* gun's part.
* @returns {Promise<void>}
*/
const acceptRequest = async (
requestID,
gun,
user,
SEA,
outgoingFeedCreator = __createOutgoingFeed
) => {
if (!user.is) {
throw new Error(ErrorCode.NOT_AUTH)
}
const handshakeAddress = await Utils.tryAndWait(async (_, user) => {
const addr = await user.get(Key.CURRENT_HANDSHAKE_ADDRESS).then()
if (typeof addr !== 'string') {
throw new TypeError("typeof addr !== 'string'")
}
return addr
})
const {
response: encryptedForUsIncomingID,
from: senderPublicKey
} = await Utils.tryAndWait(async gun => {
const hr = await gun
.get(Key.HANDSHAKE_NODES)
.get(handshakeAddress)
.get(requestID)
.then()
if (!isHandshakeRequest(hr)) {
throw new Error(ErrorCode.TRIED_TO_ACCEPT_AN_INVALID_REQUEST)
}
return hr
})
/** @type {string} */
const requestorEpub = await Utils.pubToEpub(senderPublicKey)
const ourSecret = await SEA.secret(requestorEpub, user._.sea)
if (typeof ourSecret !== 'string') {
throw new TypeError("typeof ourSecret !== 'string'")
}
const incomingID = await SEA.decrypt(encryptedForUsIncomingID, ourSecret)
if (typeof incomingID !== 'string') {
throw new TypeError("typeof incomingID !== 'string'")
}
const newlyCreatedOutgoingFeedID = await outgoingFeedCreator(
senderPublicKey,
user,
SEA
)
const mySecret = await SEA.secret(user._.sea.epub, user._.sea)
if (typeof mySecret !== 'string') {
throw new TypeError("acceptRequest() -> typeof mySecret !== 'string'")
}
const encryptedForMeIncomingID = await SEA.encrypt(incomingID, mySecret)
await new Promise((res, rej) => {
user
.get(Key.USER_TO_INCOMING)
.get(senderPublicKey)
.put(encryptedForMeIncomingID, ack => {
if (ack.err) {
rej(new Error(ack.err))
} else {
res()
}
})
})
////////////////////////////////////////////////////////////////////////////
// NOTE: perform non-reversable actions before destructive actions
// In case any of the non-reversable actions reject.
// In this case, writing to the response is the non-revesarble op.
////////////////////////////////////////////////////////////////////////////
const encryptedForUsOutgoingID = await SEA.encrypt(
newlyCreatedOutgoingFeedID,
ourSecret
)
await new Promise((res, rej) => {
gun
.get(Key.HANDSHAKE_NODES)
.get(handshakeAddress)
.get(requestID)
.put(
{
response: encryptedForUsOutgoingID
},
ack => {
if (ack.err) {
rej(new Error(ack.err))
} else {
res()
}
}
)
})
}
/**
* @param {string} user
* @param {string} pass
* @param {UserGUNNode} userNode
*/
const authenticate = (user, pass, userNode) =>
new Promise((resolve, reject) => {
if (typeof user !== 'string') {
throw new TypeError('expected user to be of type string')
}
if (typeof pass !== 'string') {
throw new TypeError('expected pass to be of type string')
}
if (user.length === 0) {
throw new TypeError('expected user to have length greater than zero')
}
if (pass.length === 0) {
throw new TypeError('expected pass to have length greater than zero')
}
if (typeof userNode.is === 'undefined') {
throw new Error(ErrorCode.ALREADY_AUTH)
}
userNode.auth(user, pass, ack => {
if (ack.err) {
reject(new Error(ack.err))
} else if (!userNode.is) {
reject(new Error('authentication failed'))
} else {
resolve()
}
})
})
/**
* @param {string} publicKey
* @param {UserGUNNode} user Pass only for testing.
* @throws {Error} If there's an error saving to the blacklist.
* @returns {Promise<void>}
*/
const blacklist = (publicKey, user) =>
new Promise((resolve, reject) => {
if (!user.is) {
throw new Error(ErrorCode.NOT_AUTH)
}
user.get(Key.BLACKLIST).set(publicKey, ack => {
if (ack.err) {
reject(new Error(ack.err))
} else {
resolve()
}
})
})
/**
* @param {UserGUNNode} user
* @returns {Promise<void>}
*/
const generateHandshakeAddress = user =>
new Promise((res, rej) => {
if (!user.is) {
throw new Error(ErrorCode.NOT_AUTH)
}
const address = uuidv1()
user.get(Key.CURRENT_HANDSHAKE_ADDRESS).put(address, ack => {
if (ack.err) {
rej(new Error(ack.err))
} else {
res()
}
})
})
/**
* @param {string} recipientPublicKey
* @param {GUNNode} gun
* @param {UserGUNNode} user
* @param {ISEA} SEA
* @throws {Error|TypeError}
* @returns {Promise<void>}
*/
const sendHandshakeRequest = async (recipientPublicKey, gun, user, SEA) => {
if (!user.is) {
throw new Error(ErrorCode.NOT_AUTH)
}
if (typeof recipientPublicKey !== 'string') {
throw new TypeError(
`recipientPublicKey is not string, got: ${typeof recipientPublicKey}`
)
}
if (recipientPublicKey.length === 0) {
throw new TypeError('recipientPublicKey is an string of length 0')
}
console.log('sendHR() -> before recipientEpub')
/** @type {string} */
const recipientEpub = await Utils.pubToEpub(recipientPublicKey)
console.log('sendHR() -> before mySecret')
const mySecret = await SEA.secret(user._.sea.epub, user._.sea)
if (typeof mySecret !== 'string') {
throw new TypeError(
"sendHandshakeRequest() -> typeof mySecret !== 'string'"
)
}
console.log('sendHR() -> before ourSecret')
const ourSecret = await SEA.secret(recipientEpub, user._.sea)
if (typeof ourSecret !== 'string') {
throw new TypeError(
"sendHandshakeRequest() -> typeof ourSecret !== 'string'"
)
}
// check if successful handshake is present
console.log('sendHR() -> before alreadyHandshaked')
/** @type {boolean} */
const alreadyHandshaked = await Utils.successfulHandshakeAlreadyExists(
recipientPublicKey
)
if (alreadyHandshaked) {
throw new Error(ErrorCode.ALREADY_HANDSHAKED)
}
console.log('sendHR() -> before maybeLastRequestIDSentToUser')
// check that we have already sent a request to this user, on his current
// handshake node
const maybeLastRequestIDSentToUser = await Utils.tryAndWait((_, user) =>
user
.get(Key.USER_TO_LAST_REQUEST_SENT)
.get(recipientPublicKey)
.then()
)
console.log('sendHR() -> before currentHandshakeAddress')
const currentHandshakeAddress = await Utils.tryAndWait(gun =>
gun
.user(recipientPublicKey)
.get(Key.CURRENT_HANDSHAKE_ADDRESS)
.then()
)
if (typeof currentHandshakeAddress !== 'string') {
throw new TypeError(
'expected current handshake address found on recipients user node to be an string'
)
}
if (typeof maybeLastRequestIDSentToUser === 'string') {
if (maybeLastRequestIDSentToUser.length < 5) {
throw new TypeError(
'sendHandshakeRequest() -> maybeLastRequestIDSentToUser.length < 5'
)
}
const lastRequestIDSentToUser = maybeLastRequestIDSentToUser
console.log('sendHR() -> before alreadyContactedOnCurrHandshakeNode')
/** @type {boolean} */
const alreadyContactedOnCurrHandshakeNode = await Utils.tryAndWait(
gun =>
new Promise(res => {
gun
.get(Key.HANDSHAKE_NODES)
.get(currentHandshakeAddress)
.get(lastRequestIDSentToUser)
.once(data => {
res(typeof data !== 'undefined')
})
})
)
if (alreadyContactedOnCurrHandshakeNode) {
throw new Error(ErrorCode.ALREADY_REQUESTED_HANDSHAKE)
}
}
console.log('sendHR() -> before __createOutgoingFeed')
const outgoingFeedID = await __createOutgoingFeed(
recipientPublicKey,
user,
SEA
)
console.log('sendHR() -> before encryptedForUsOutgoingFeedID')
const encryptedForUsOutgoingFeedID = await SEA.encrypt(
outgoingFeedID,
ourSecret
)
const timestamp = Date.now()
/** @type {HandshakeRequest} */
const handshakeRequestData = {
from: user.is.pub,
response: encryptedForUsOutgoingFeedID,
timestamp
}
console.log('sendHR() -> before newHandshakeRequestID')
/** @type {string} */
const newHandshakeRequestID = await new Promise((res, rej) => {
const hr = gun
.get(Key.HANDSHAKE_NODES)
.get(currentHandshakeAddress)
.set(handshakeRequestData, ack => {
if (ack.err) {
rej(new Error(`Error trying to create request: ${ack.err}`))
} else {
res(hr._.get)
}
})
})
await new Promise((res, rej) => {
user
.get(Key.USER_TO_LAST_REQUEST_SENT)
.get(recipientPublicKey)
.put(newHandshakeRequestID, ack => {
if (ack.err) {
rej(new Error(ack.err))
} else {
res()
}
})
})
// save request id to REQUEST_TO_USER
const encryptedForMeRecipientPublicKey = await SEA.encrypt(
recipientPublicKey,
mySecret
)
// This needs to come before the write to sent requests. Because that write
// triggers Jobs.onAcceptedRequests and it in turn reads from request-to-user
// This also triggers Events.onSimplerSentRequests
await new Promise((res, rej) => {
user
.get(Key.REQUEST_TO_USER)
.get(newHandshakeRequestID)
.put(encryptedForMeRecipientPublicKey, ack => {
if (ack.err) {
rej(
new Error(
`Error saving recipient public key to request to user: ${ack.err}`
)
)
} else {
res()
}
})
})
/**
* @type {StoredReq}
*/
const storedReq = {
sentReqID: await SEA.encrypt(newHandshakeRequestID, mySecret),
recipientPub: encryptedForMeRecipientPublicKey,
handshakeAddress: await SEA.encrypt(currentHandshakeAddress, mySecret),
timestamp
}
await new Promise((res, rej) => {
user.get(Key.STORED_REQS).set(storedReq, ack => {
if (ack.err) {
rej(
new Error(
`Error saving newly created request to sent requests: ${ack.err}`
)
)
} else {
res()
}
})
})
}
/**
* @param {string} recipientPublicKey
* @param {string} body
* @param {UserGUNNode} user
* @param {ISEA} SEA
* @returns {Promise<void>}
*/
const sendMessage = async (recipientPublicKey, body, user, SEA) => {
if (!user.is) {
throw new Error(ErrorCode.NOT_AUTH)
}
if (typeof recipientPublicKey !== 'string') {
throw new TypeError(
`expected recipientPublicKey to be an string, but instead got: ${typeof recipientPublicKey}`
)
}
if (recipientPublicKey.length === 0) {
throw new TypeError(
'expected recipientPublicKey to be an string of length greater than zero'
)
}
if (typeof body !== 'string') {
throw new TypeError(
`expected message to be an string, instead got: ${typeof body}`
)
}
if (body.length === 0) {
throw new TypeError(
'expected message to be an string of length greater than zero'
)
}
const outgoingID = await Utils.recipientToOutgoingID(
recipientPublicKey,
user,
SEA
)
if (outgoingID === null) {
throw new Error(
`Could not fetch an outgoing id for user: ${recipientPublicKey}`
)
}
const recipientEpub = await Utils.pubToEpub(recipientPublicKey)
const ourSecret = await SEA.secret(recipientEpub, user._.sea)
if (typeof ourSecret !== 'string') {
throw new TypeError("sendMessage() -> typeof ourSecret !== 'string'")
}
const encryptedBody = await SEA.encrypt(body, ourSecret)
const newMessage = {
body: encryptedBody,
timestamp: Date.now()
}
return new Promise((res, rej) => {
user
.get(Key.OUTGOINGS)
.get(outgoingID)
.get(Key.MESSAGES)
.set(newMessage, ack => {
if (ack.err) {
rej(ack.err)
} else {
res()
}
})
})
}
/**
* @param {string|null} avatar
* @param {UserGUNNode} user
* @throws {TypeError} Rejects if avatar is not an string or an empty string.
* @returns {Promise<void>}
*/
const setAvatar = (avatar, user) =>
new Promise((resolve, reject) => {
if (!user.is) {
throw new Error(ErrorCode.NOT_AUTH)
}
if (typeof avatar === 'string' && avatar.length === 0) {
throw new TypeError(
"'avatar' must be an string and have length greater than one or be null"
)
}
if (typeof avatar !== 'string' && avatar !== null) {
throw new TypeError(
"'avatar' must be an string and have length greater than one or be null"
)
}
user
.get(Key.PROFILE)
.get(Key.AVATAR)
.put(avatar, ack => {
if (ack.err) {
reject(new Error(ack.err))
} else {
resolve()
}
})
})
/**
* @param {string} displayName
* @param {UserGUNNode} user
* @throws {TypeError} Rejects if displayName is not an string or an empty
* string.
* @returns {Promise<void>}
*/
const setDisplayName = (displayName, user) =>
new Promise((resolve, reject) => {
if (!user.is) {
throw new Error(ErrorCode.NOT_AUTH)
}
if (typeof displayName !== 'string') {
throw new TypeError()
}
if (displayName.length === 0) {
throw new TypeError()
}
user
.get(Key.PROFILE)
.get(Key.DISPLAY_NAME)
.put(displayName, ack => {
if (ack.err) {
reject(new Error(ack.err))
} else {
resolve()
}
})
})
/**
* @param {string} initialMsg
* @param {string} recipientPublicKey
* @param {GUNNode} gun
* @param {UserGUNNode} user
* @param {ISEA} SEA
* @throws {Error|TypeError}
* @returns {Promise<void>}
*/
const sendHRWithInitialMsg = async (
initialMsg,
recipientPublicKey,
gun,
user,
SEA
) => {
/** @type {boolean} */
const alreadyHandshaked = await Utils.tryAndWait(
(_, user) =>
new Promise((res, rej) => {
user
.get(Key.USER_TO_INCOMING)
.get(recipientPublicKey)
.once(inc => {
if (typeof inc !== 'string') {
res(false)
} else if (inc.length === 0) {
rej(
new Error(
`sendHRWithInitialMsg()-> obtained encryptedIncomingId from user-to-incoming an string but of length 0`
)
)
} else {
res(true)
}
})
})
)
if (!alreadyHandshaked) {
await sendHandshakeRequest(recipientPublicKey, gun, user, SEA)
}
await sendMessage(recipientPublicKey, initialMsg, user, SEA)
}
module.exports = {
INITIAL_MSG,
__createOutgoingFeed,
acceptRequest,
authenticate,
blacklist,
generateHandshakeAddress,
sendHandshakeRequest,
sendMessage,
sendHRWithInitialMsg,
setAvatar,
setDisplayName
}

View file

@ -0,0 +1,43 @@
/**
* @prettier
*/
exports.ALREADY_AUTH = 'ALREADY_AUTH'
exports.NOT_AUTH = 'NOT_AUTH'
exports.COULDNT_ACCEPT_REQUEST = 'COULDNT_ACCEPT_REQUEST'
exports.COULDNT_SENT_REQUEST = 'COULDNT_SENT_REQUEST'
exports.COULDNT_PUT_REQUEST_RESPONSE = 'COULDNT_PUT_REQUEST_RESPONSE'
/**
* Error thrown when trying to accept a request, and on retrieval of that
* request invalid data (not resembling a request) is received.
*/
exports.TRIED_TO_ACCEPT_AN_INVALID_REQUEST =
'TRIED_TO_ACCEPT_AN_INVALID_REQUEST'
exports.UNSUCCESSFUL_LOGOUT = 'UNSUCCESSFUL_LOGOUT'
exports.UNSUCCESSFUL_REQUEST_ACCEPT = 'UNSUCCESSFUL_REQUEST_ACCEPT'
/**
* Error thrown when trying to send a handshake request to an user for which
* there's already an successful handshake.
*/
exports.ALREADY_HANDSHAKED = 'ALREADY_HANDSHAKED'
/**
* Error thrown when trying to send a handshake request to an user for which
* there's already a handshake request on his current handshake node.
*/
exports.ALREADY_REQUESTED_HANDSHAKE = 'ALREADY_REQUESTED_HANDSHAKE'
/**
* Error thrown when trying to send a handshake request to an user on an stale
* handshake address.
*/
exports.STALE_HANDSHAKE_ADDRESS = 'STALE_HANDSHAKE_ADDRESS'
exports.TIMEOUT_ERR = 'TIMEOUT_ERR'

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,10 @@
/**
* @prettier
*/
const Actions = require('./actions')
const Events = require('./events')
const Jobs = require('./jobs')
const Key = require('./key')
const Schema = require('./schema')
module.exports = { Actions, Events, Jobs, Key, Schema }

View file

@ -0,0 +1,153 @@
/**
* @prettier
* Taks are subscriptions to events that perform actions (write to GUN) on
* response to certain ways events can happen. These tasks need to be fired up
* at app launch otherwise certain features won't work as intended. Tasks should
* ideally be idempotent, that is, if they were to be fired up after a certain
* amount of time after app launch, everything should work as intended. For this
* to work, special care has to be put into how these respond to events. These
* tasks could be hardcoded inside events but then they wouldn't be easily
* auto-testable. These tasks accept factories that are homonymous to the events
* on the same
*/
const ErrorCode = require('./errorCode')
const Key = require('./key')
const Schema = require('./schema')
const Utils = require('./utils')
/**
* @typedef {import('./SimpleGUN').GUNNode} GUNNode
* @typedef {import('./SimpleGUN').ISEA} ISEA
* @typedef {import('./SimpleGUN').UserGUNNode} UserGUNNode
*/
/**
* @throws {Error} NOT_AUTH
* @param {UserGUNNode} user
* @param {ISEA} SEA
* @returns {Promise<void>}
*/
const onAcceptedRequests = async (user, SEA) => {
if (!user.is) {
throw new Error(ErrorCode.NOT_AUTH)
}
const mySecret = await SEA.secret(user._.sea.epub, user._.sea)
if (typeof mySecret !== 'string') {
console.log("Jobs.onAcceptedRequests() -> typeof mySecret !== 'string'")
return
}
user
.get(Key.STORED_REQS)
.map()
.once(async storedReq => {
try {
if (!Schema.isStoredRequest(storedReq)) {
throw new TypeError('Stored request not an StoredRequest')
}
const recipientPub = await SEA.decrypt(storedReq.recipientPub, mySecret)
if (typeof recipientPub !== 'string') {
throw new TypeError()
}
if (await Utils.successfulHandshakeAlreadyExists(recipientPub)) {
return
}
const requestAddress = await SEA.decrypt(
storedReq.handshakeAddress,
mySecret
)
if (typeof requestAddress !== 'string') {
throw new TypeError()
}
const sentReqID = await SEA.decrypt(storedReq.sentReqID, mySecret)
if (typeof sentReqID !== 'string') {
throw new TypeError()
}
const latestReqSentID = await Utils.recipientPubToLastReqSentID(
recipientPub
)
const isStaleRequest = latestReqSentID !== sentReqID
if (isStaleRequest) {
return
}
const recipientEpub = await Utils.pubToEpub(recipientPub)
const ourSecret = await SEA.secret(recipientEpub, user._.sea)
if (typeof ourSecret !== 'string') {
throw new TypeError("typeof ourSecret !== 'string'")
}
await Utils.tryAndWait(
(gun, user) =>
new Promise((res, rej) => {
gun
.get(Key.HANDSHAKE_NODES)
.get(requestAddress)
.get(sentReqID)
.on(async sentReq => {
if (!Schema.isHandshakeRequest(sentReq)) {
rej(
new Error(
'sent request found in handshake node not a handshake request'
)
)
return
}
// The response can be decrypted with the same secret regardless of who
// wrote to it last (see HandshakeRequest definition).
// This could be our feed ID for the recipient, or the recipient's feed
// id if he accepted the request.
const feedID = await SEA.decrypt(sentReq.response, ourSecret)
if (typeof feedID !== 'string') {
throw new TypeError("typeof feedID !== 'string'")
}
const feedIDExistsOnRecipientsOutgoings = await Utils.tryAndWait(
gun =>
new Promise(res => {
gun
.user(recipientPub)
.get(Key.OUTGOINGS)
.get(feedID)
.once(feed => {
res(typeof feed !== 'undefined')
})
})
)
if (!feedIDExistsOnRecipientsOutgoings) {
return
}
const encryptedForMeIncomingID = await SEA.encrypt(
feedID,
mySecret
)
user
.get(Key.USER_TO_INCOMING)
.get(recipientPub)
.put(encryptedForMeIncomingID)
// ensure this listeners gets called at least once
res()
})
})
)
} catch (err) {
console.warn(`Jobs.onAcceptedRequests() -> ${err.message}`)
}
})
}
module.exports = {
onAcceptedRequests
}

View file

@ -0,0 +1,26 @@
/**
* @format
*/
exports.HANDSHAKE_NODES = 'handshakeNodes'
exports.CURRENT_HANDSHAKE_ADDRESS = 'currentHandshakeAddress'
exports.MESSAGES = 'messages'
exports.OUTGOINGS = 'outgoings'
exports.RECIPIENT_TO_OUTGOING = 'recipientToOutgoing'
exports.USER_TO_INCOMING = 'userToIncoming'
exports.STORED_REQS = 'storedReqs'
exports.REQUEST_TO_USER = 'requestToUser'
exports.BLACKLIST = 'blacklist'
exports.PROFILE = 'Profile'
exports.AVATAR = 'avatar'
exports.DISPLAY_NAME = 'displayName'
/**
* Maps user to the last request sent to them.
*/
exports.USER_TO_LAST_REQUEST_SENT = 'USER_TO_LAST_REQUEST_SENT'

View file

@ -0,0 +1,339 @@
/**
* @prettier
*/
/**
* @typedef {object} HandshakeRequest
* @prop {string} from Public key of the requestor.
* @prop {string} response Encrypted string where, if the recipient accepts the
* request, his outgoing feed id will be put. Before that the sender's outgoing
* feed ID will be placed here, encrypted so only the recipient can access it.
* @prop {number} timestamp Unix time.
*/
/**
* @typedef {object} Message
* @prop {string} body
* @prop {number} timestamp
*/
/**
* @typedef {object} ChatMessage
* @prop {string} body
* @prop {string} id
* @prop {boolean} outgoing True if the message is an outgoing message,
* otherwise it is an incoming message.
* @prop {number} timestamp
*/
/**
*
* @param {any} item
* @returns {item is ChatMessage}
*/
exports.isChatMessage = item => {
if (typeof item !== 'object') {
return false
}
if (item === null) {
return false
}
const obj = /** @type {ChatMessage} */ (item)
if (typeof obj.body !== 'string') {
return false
}
if (typeof obj.id !== 'string') {
return false
}
if (typeof obj.outgoing !== 'boolean') {
return false
}
if (typeof obj.timestamp !== 'number') {
return false
}
return true
}
/**
* A simpler representation of a conversation between two users than the
* outgoing/incoming feed paradigm. It combines both the outgoing and incoming
* messages into one data structure plus metada about the chat.
* @typedef {object} Chat
* @prop {string|null} recipientAvatar Base64 encoded image.
* @prop {string} recipientPublicKey A way to uniquely identify each chat.
* @prop {ChatMessage[]} messages Sorted from most recent to least recent.
* @prop {string|null} recipientDisplayName
*/
/**
* @param {any} item
* @returns {item is Chat}
*/
exports.isChat = item => {
if (typeof item !== 'object') {
return false
}
if (item === null) {
return false
}
const obj = /** @type {Chat} */ (item)
if (typeof obj.recipientAvatar !== 'string' && obj.recipientAvatar !== null) {
return false
}
if (!Array.isArray(obj.messages)) {
return false
}
if (typeof obj.recipientPublicKey !== 'string') {
return false
}
if (obj.recipientPublicKey.length === 0) {
return false
}
return obj.messages.every(msg => exports.isChatMessage(msg))
}
/**
* @typedef {object} Outgoing
* @prop {Record<string, Message>} messages
* @prop {string} with Public key for whom the outgoing messages are intended.
*/
/**
* @typedef {object} PartialOutgoing
* @prop {string} with (Encrypted) Public key for whom the outgoing messages are
* intended.
*/
/**
* @typedef {object} StoredRequest
* @prop {string} sentReqID
* @prop {string} recipientPub
* @prop {string} handshakeAddress
* @prop {number} timestamp
*/
/**
* @param {any} item
* @returns {item is StoredRequest}
*/
exports.isStoredRequest = item => {
if (typeof item !== 'object') return false
if (item === null) return false
const obj = /** @type {StoredRequest} */ (item)
if (typeof obj.recipientPub !== 'string') return false
if (typeof obj.handshakeAddress !== 'string') return false
return true
}
/**
* @typedef {object} SimpleSentRequest
* @prop {string} id
* @prop {string|null} recipientAvatar
* @prop {boolean} recipientChangedRequestAddress True if the recipient changed
* the request node address and therefore can't no longer accept the request.
* @prop {string|null} recipientDisplayName
* @prop {string} recipientPublicKey Fallback for when user has no display name.
* @prop {number} timestamp
*/
/**
* @param {any} item
* @returns {item is SimpleSentRequest}
*/
exports.isSimpleSentRequest = item => {
if (typeof item !== 'object') {
return false
}
if (item === null) {
return false
}
const obj = /** @type {SimpleSentRequest} */ (item)
if (typeof obj.id !== 'string') {
return false
}
if (typeof obj.recipientAvatar !== 'string' && obj.recipientAvatar !== null) {
return false
}
if (typeof obj.recipientChangedRequestAddress !== 'boolean') {
return false
}
if (
typeof obj.recipientDisplayName !== 'string' &&
obj.recipientDisplayName !== null
) {
return false
}
if (typeof obj.recipientPublicKey !== 'string') {
return false
}
if (typeof obj.timestamp !== 'number') {
return false
}
return true
}
/**
* @typedef {object} SimpleReceivedRequest
* @prop {string} id
* @prop {string|null} requestorAvatar
* @prop {string|null} requestorDisplayName
* @prop {string} requestorPK
* @prop {string} response
* @prop {number} timestamp
*/
/**
* @param {any} item
* @returns {item is SimpleReceivedRequest}
*/
exports.isSimpleReceivedRequest = item => {
if (typeof item !== 'object') {
return false
}
if (item === null) {
return false
}
const obj = /** @type {SimpleReceivedRequest} */ (item)
if (typeof obj.id !== 'string') {
return false
}
if (typeof obj.requestorAvatar !== 'string' && obj.requestorAvatar !== null) {
return false
}
if (
typeof obj.requestorDisplayName !== 'string' &&
obj.requestorDisplayName !== null
) {
return false
}
if (typeof obj.requestorPK !== 'string') {
return false
}
if (typeof obj.response !== 'string') {
return false
}
if (typeof obj.timestamp !== 'number') {
return false
}
return true
}
/**
* @param {any} item
* @returns {item is HandshakeRequest}
*/
exports.isHandshakeRequest = item => {
if (typeof item !== 'object') {
return false
}
if (item === null) {
return false
}
const obj = /** @type {HandshakeRequest} */ (item)
if (typeof obj.from !== 'string') {
return false
}
if (typeof obj.response !== 'string') {
return false
}
if (typeof obj.timestamp !== 'number') {
return false
}
return true
}
/**
* @param {any} item
* @returns {item is Message}
*/
exports.isMessage = item => {
if (typeof item !== 'object') {
return false
}
if (item === null) {
return false
}
const obj = /** @type {Message} */ (item)
return typeof obj.body === 'string' && typeof obj.timestamp === 'number'
}
/**
* @param {any} item
* @returns {item is PartialOutgoing}
*/
exports.isPartialOutgoing = item => {
if (typeof item !== 'object') {
return false
}
if (item === null) {
return false
}
const obj = /** @type {PartialOutgoing} */ (item)
return typeof obj.with === 'string'
}
/**
*
* @param {any} item
* @returns {item is Outgoing}
*/
exports.isOutgoing = item => {
if (typeof item !== 'object') {
return false
}
if (item === null) {
return false
}
const obj = /** @type {Outgoing} */ (item)
const messagesAreMessages = Object.values(obj.messages).every(msg =>
exports.isMessage(msg)
)
return typeof obj.with === 'string' && messagesAreMessages
}

View file

@ -0,0 +1,295 @@
/**
* @format
*/
const ErrorCode = require('./errorCode')
const Key = require('./key')
/**
* @typedef {import('./SimpleGUN').GUNNode} GUNNode
* @typedef {import('./SimpleGUN').ISEA} ISEA
* @typedef {import('./SimpleGUN').UserGUNNode} UserGUNNode
*/
/**
* @param {number} ms
* @returns {Promise<void>}
*/
const delay = ms => new Promise(res => setTimeout(res, ms))
/**
* @template T
* @param {Promise<T>} promise
* @returns {Promise<T>}
*/
const timeout10 = promise => {
return Promise.race([
promise,
new Promise((_, rej) => {
setTimeout(() => {
rej(new Error(ErrorCode.TIMEOUT_ERR))
}, 10000)
})
])
}
/**
* @template T
* @param {(gun: GUNNode, user: UserGUNNode) => Promise<T>} promGen The function
* receives the most recent gun and user instances.
* @returns {Promise<T>}
*/
const tryAndWait = promGen =>
timeout10(
promGen(
require('../Mediator/index').getGun(),
require('../Mediator/index').getUser()
)
)
/**
* @param {string} pub
* @returns {Promise<string>}
*/
const pubToEpub = async pub => {
try {
const epub = await tryAndWait(async gun => {
const _epub = await gun
.user(pub)
.get('epub')
.then()
if (typeof _epub !== 'string') {
throw new TypeError(
`Expected gun.user(pub).get(epub) to be an string. Instead got: ${typeof _epub}`
)
}
return _epub
})
return epub
} catch (err) {
console.log(err)
throw new Error(`pubToEpub() -> ${err.message}`)
}
}
/**
* @param {string} reqID
* @param {ISEA} SEA
* @param {string} mySecret
* @returns {Promise<string>}
*/
const reqToRecipientPub = async (reqID, SEA, mySecret) => {
const maybeEncryptedForMeRecipientPub = await tryAndWait(async (_, user) => {
const reqToUser = user.get(Key.REQUEST_TO_USER)
const data = await reqToUser.get(reqID).then()
if (typeof data !== 'string') {
throw new TypeError("typeof maybeEncryptedForMeRecipientPub !== 'string'")
}
return data
})
const encryptedForMeRecipientPub = maybeEncryptedForMeRecipientPub
const recipientPub = await SEA.decrypt(encryptedForMeRecipientPub, mySecret)
if (typeof recipientPub !== 'string') {
throw new TypeError("typeof recipientPub !== 'string'")
}
return recipientPub
}
/**
* Should only be called with a recipient pub that has already been contacted.
* @param {string} recipientPub
* @returns {Promise<string>}
*/
const recipientPubToLastReqSentID = async recipientPub => {
const lastReqSentID = await tryAndWait(async (_, user) => {
const userToLastReqSent = user.get(Key.USER_TO_LAST_REQUEST_SENT)
const data = await userToLastReqSent.get(recipientPub).then()
if (typeof data !== 'string') {
throw new TypeError("typeof latestReqSentID !== 'string'")
}
return data
})
return lastReqSentID
}
/**
* @param {string} recipientPub
* @returns {Promise<boolean>}
*/
const successfulHandshakeAlreadyExists = async recipientPub => {
const maybeIncomingID = await tryAndWait((_, user) => {
const userToIncoming = user.get(Key.USER_TO_INCOMING)
return userToIncoming.get(recipientPub).then()
})
return typeof maybeIncomingID === 'string'
}
/**
* @param {string} recipientPub
* @param {UserGUNNode} user
* @param {ISEA} SEA
* @returns {Promise<string|null>}
*/
const recipientToOutgoingID = async (recipientPub, user, SEA) => {
const mySecret = await SEA.secret(user._.sea.epub, user._.sea)
if (typeof mySecret !== 'string') {
throw new TypeError('could not get mySecret')
}
const maybeEncryptedOutgoingID = await tryAndWait((_, user) =>
user
.get(Key.RECIPIENT_TO_OUTGOING)
.get(recipientPub)
.then()
)
if (typeof maybeEncryptedOutgoingID === 'string') {
const outgoingID = await SEA.decrypt(maybeEncryptedOutgoingID, mySecret)
return outgoingID || null
}
return null
}
/**
* @param {string} reqResponse
* @param {string} recipientPub
* @param {UserGUNNode} user
* @param {ISEA} SEA
* @returns {Promise<boolean>}
*/
const reqWasAccepted = async (reqResponse, recipientPub, user, SEA) => {
try {
const recipientEpub = await pubToEpub(recipientPub)
const ourSecret = await SEA.secret(recipientEpub, user._.sea)
if (typeof ourSecret !== 'string') {
throw new TypeError('typeof ourSecret !== "string"')
}
const decryptedResponse = await SEA.decrypt(reqResponse, ourSecret)
if (typeof decryptedResponse !== 'string') {
throw new TypeError('typeof decryptedResponse !== "string"')
}
const myFeedID = await recipientToOutgoingID(recipientPub, user, SEA)
if (typeof myFeedID === 'string' && decryptedResponse === myFeedID) {
return false
}
const recipientFeedID = decryptedResponse
const maybeFeed = await tryAndWait(gun =>
gun
.user(recipientPub)
.get(Key.OUTGOINGS)
.get(recipientFeedID)
.then()
)
const feedExistsOnRecipient =
typeof maybeFeed === 'object' && maybeFeed !== null
return feedExistsOnRecipient
} catch (err) {
throw new Error(`reqWasAccepted() -> ${err.message}`)
}
}
/**
*
* @param {string} userPub
* @returns {Promise<string|null>}
*/
const currHandshakeAddress = async userPub => {
const maybeAddr = await tryAndWait(gun =>
gun
.user(userPub)
.get(Key.CURRENT_HANDSHAKE_ADDRESS)
.then()
)
return typeof maybeAddr === 'string' ? maybeAddr : null
}
/**
* @template T
* @template U
* @param {T[]} arr
* @param {(item: T) => Promise<U>} cb
* @returns {Promise<U[]>}
*/
const asyncMap = (arr, cb) => {
if (arr.length === 0) {
return Promise.resolve([])
}
const promises = arr.map(item => cb(item))
return Promise.all(promises)
}
/**
* @template T
* @param {T[]} arr
* @param {(item: T) => Promise<boolean>} cb
* @returns {Promise<T[]>}
*/
const asyncFilter = async (arr, cb) => {
if (arr.length === 0) {
return []
}
/** @type {Promise<boolean>[]} */
const promises = arr.map(item => cb(item))
/** @type {boolean[]} */
const results = await Promise.all(promises)
return arr.filter((_, idx) => results[idx])
}
/**
* @param {import('./SimpleGUN').ListenerData} listenerData
* @returns {listenerData is import('./SimpleGUN').ListenerObj}
*/
const dataHasSoul = listenerData =>
typeof listenerData === 'object' && listenerData !== null
/**
* @param {string} pub
* @returns {string}
*/
const defaultName = pub => 'anon' + pub.slice(0, 8)
module.exports = {
asyncMap,
asyncFilter,
dataHasSoul,
defaultName,
delay,
pubToEpub,
reqToRecipientPub,
recipientPubToLastReqSentID,
successfulHandshakeAlreadyExists,
recipientToOutgoingID,
reqWasAccepted,
currHandshakeAddress,
tryAndWait
}

View file

@ -0,0 +1,11 @@
const Events = {
ON_AVATAR: "ON_AVATAR",
ON_BLACKLIST: "ON_BLACKLIST",
ON_CHATS: "ON_CHATS",
ON_DISPLAY_NAME: "ON_DISPLAY_NAME",
ON_HANDSHAKE_ADDRESS: "ON_HANDSHAKE_ADDRESS",
ON_RECEIVED_REQUESTS: "ON_RECEIVED_REQUESTS",
ON_SENT_REQUESTS: "ON_SENT_REQUESTS"
};
module.exports = Events;

84
services/lnd/lightning.js Normal file
View file

@ -0,0 +1,84 @@
const grpc = require("grpc");
const protoLoader = require("@grpc/proto-loader");
const fs = require("../../utils/fs");
const logger = require("winston");
const errorConstants = require("../../constants/errors");
// expose the routes to our app with module.exports
module.exports = async (protoPath, lndHost, lndCertPath, macaroonPath) => {
try {
process.env.GRPC_SSL_CIPHER_SUITES = "HIGH+ECDSA";
const packageDefinition = await protoLoader.load(protoPath, {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true,
includeDirs: ["node_modules/google-proto-files", "proto"]
});
const { lnrpc } = grpc.loadPackageDefinition(packageDefinition);
const getCredentials = async () => {
const lndCert = await fs.readFile(lndCertPath);
const sslCreds = grpc.credentials.createSsl(lndCert);
if (macaroonPath) {
const macaroonExists = await fs.access(macaroonPath);
if (macaroonExists) {
const macaroonCreds = grpc.credentials.createFromMetadataGenerator(
async (args, callback) => {
const adminMacaroon = await fs.readFile(macaroonPath);
const metadata = new grpc.Metadata();
metadata.add("macaroon", adminMacaroon.toString("hex"));
callback(null, metadata);
}
);
return grpc.credentials.combineChannelCredentials(
sslCreds,
macaroonCreds
);
}
const error = errorConstants.MACAROON_PATH(macaroonPath);
logger.error(error);
throw error;
} else {
return sslCreds;
}
};
if (lndCertPath) {
const certExists = await fs.access(lndCertPath);
if (certExists) {
const credentials = await getCredentials();
const lightning = new lnrpc.Lightning(lndHost, credentials);
const walletUnlocker = new lnrpc.WalletUnlocker(lndHost, credentials);
return {
lightning,
walletUnlocker
};
}
const error = errorConstants.CERT_PATH(lndCertPath);
logger.error(error);
throw error;
} else {
const error = errorConstants.MACAROON_PATH(macaroonPath);
logger.error(error);
throw error;
}
} catch (err) {
if (err.code === 14) {
throw {
field: "unknown",
message:
"Failed to connect to LND server, make sure it's up and running."
};
}
}
};

81
services/lnd/lnd.js Normal file
View file

@ -0,0 +1,81 @@
// app/lnd.js
const debug = require("debug")("lncliweb:lnd");
const logger = require("winston");
// TODO
module.exports = function(lightning) {
const module = {};
const invoiceListeners = [];
let lndInvoicesStream = null;
const openLndInvoicesStream = function() {
if (lndInvoicesStream) {
logger.debug("Lnd invoices subscription stream already opened.");
} else {
logger.debug("Opening lnd invoices subscription stream...");
lndInvoicesStream = lightning.subscribeInvoices({});
logger.debug("Lnd invoices subscription stream opened.");
lndInvoicesStream.on("data", function(data) {
logger.debug("SubscribeInvoices Data", data);
for (let i = 0; i < invoiceListeners.length; i++) {
try {
invoiceListeners[i].dataReceived(data);
} catch (err) {
logger.warn(err);
}
}
});
lndInvoicesStream.on("end", function() {
logger.debug("SubscribeInvoices End");
lndInvoicesStream = null;
openLndInvoicesStream(); // try opening stream again
});
lndInvoicesStream.on("error", function(err) {
logger.debug("SubscribeInvoices Error", err);
});
lndInvoicesStream.on("status", function(status) {
logger.debug("SubscribeInvoices Status", status);
if (status.code == 14) {
// Unavailable
lndInvoicesStream = null;
openLndInvoicesStream(); // try opening stream again
}
});
}
};
// register invoice listener
module.registerInvoiceListener = function(listener) {
invoiceListeners.push(listener);
logger.debug(
"New lnd invoice listener registered, " +
invoiceListeners.length +
" listening now"
);
};
// unregister invoice listener
module.unregisterInvoiceListener = function(listener) {
invoiceListeners.splice(invoiceListeners.indexOf(listener), 1);
logger.debug(
"Lnd invoice listener unregistered, " +
invoiceListeners.length +
" still listening"
);
};
// open lnd invoices stream on start
openLndInvoicesStream();
// check every minute that lnd invoices stream is still opened
setInterval(function() {
if (!lndInvoicesStream) {
openLndInvoicesStream();
}
}, 60 * 1000);
return module;
};

18
src/cors.js Normal file
View file

@ -0,0 +1,18 @@
const setAccessControlHeaders = (req, res) => {
res.header("Access-Control-Allow-Origin", "*");
res.header(
"Access-Control-Allow-Headers",
"Origin, X-Requested-With, Content-Type, Accept, Authorization"
);
};
module.exports = (req, res, next) => {
if (req.method === "OPTIONS") {
setAccessControlHeaders(req, res);
res.sendStatus(204);
return;
}
setAccessControlHeaders(req, res);
next();
};

1355
src/routes.js Normal file

File diff suppressed because it is too large Load diff

254
src/server.js Normal file
View file

@ -0,0 +1,254 @@
"use strict";
/**
* Module dependencies.
*/
const server = program => {
const Https = require("https");
const Http = require("http");
const Express = require("express");
// const Cluster = require("cluster");
// const OS = require("os");
const app = Express();
const FS = require("../utils/fs");
const bodyParser = require("body-parser");
const session = require("express-session");
const methodOverride = require("method-override");
// load app default configuration data
const defaults = require("../config/defaults")(program.mainnet);
// define useful global variables ======================================
module.useTLS = program.usetls;
module.serverPort = program.serverport || defaults.serverPort;
module.httpsPort = module.serverPort;
module.serverHost = program.serverhost || defaults.serverHost;
// setup winston logging ==========
const logger = require("../config/log")(
program.logfile || defaults.logfile,
program.loglevel || defaults.loglevel
);
// utilities functions =================
require("../utils/server-utils")(module);
// setup lightning client =================
const lnrpc = require("../services/lnd/lightning");
const lndHost = program.lndhost || defaults.lndHost;
const lndCertPath = program.lndCertPath || defaults.lndCertPath;
const macaroonPath = program.macaroonPath || defaults.macaroonPath;
logger.info("Mainnet Mode:", !!program.mainnet);
const wait = seconds =>
new Promise(resolve => {
const timer = setTimeout(() => resolve(timer), seconds * 1000);
});
// eslint-disable-next-line consistent-return
const startServer = async () => {
try {
const macaroonExists = await FS.access(macaroonPath);
const lnServices = await lnrpc(
defaults.lndProto,
lndHost,
lndCertPath,
macaroonExists ? macaroonPath : null
);
const { lightning } = lnServices;
const { walletUnlocker } = lnServices;
const lnServicesData = {
lndProto: defaults.lndProto,
lndHost,
lndCertPath,
macaroonPath
};
// init lnd module =================
const lnd = require("../services/lnd/lnd")(lightning);
const unprotectedRoutes = {
GET: {
"/healthz": true,
"/ping": true,
"/api/lnd/connect": true,
"/api/lnd/wallet/status": true,
"/api/lnd/auth": true
},
POST: {
"/api/lnd/connect": true,
"/api/lnd/wallet": true,
"/api/lnd/wallet/existing": true,
"/api/lnd/auth": true
},
PUT: {},
DELETE: {}
};
const auth = require("../services/auth/auth");
app.use(async (req, res, next) => {
if (unprotectedRoutes[req.method][req.path]) {
next();
} else {
try {
const response = await auth.validateToken(
req.headers.authorization.replace("Bearer ", "")
);
if (response.valid) {
next();
} else {
res.status(401).json({ message: "Please log in" });
}
} catch (err) {
logger.error(err);
res.status(401).json({ message: "Please log in" });
}
}
});
const sensitiveRoutes = {
GET: {},
POST: {
"/api/lnd/connect": true,
"/api/lnd/wallet": true
},
PUT: {},
DELETE: {}
};
app.use((req, res, next) => {
if (sensitiveRoutes[req.method][req.path]) {
console.log(
JSON.stringify({
time: new Date(),
ip: req.ip,
method: req.method,
path: req.path,
sessionId: req.sessionId
})
);
} else {
console.log(
JSON.stringify({
time: new Date(),
ip: req.ip,
method: req.method,
path: req.path,
body: req.body,
query: req.query,
sessionId: req.sessionId
})
);
}
next();
});
app.use(
session({
secret: defaults.sessionSecret,
cookie: { maxAge: defaults.sessionMaxAge },
resave: true,
rolling: true,
saveUninitialized: true
})
);
app.use(bodyParser.urlencoded({ extended: "true" }));
app.use(bodyParser.json());
app.use(bodyParser.json({ type: "application/vnd.api+json" }));
app.use(methodOverride());
// WARNING
// error handler middleware, KEEP 4 parameters as express detects the
// arity of the function to treat it as a err handling middleware
// eslint-disable-next-line no-unused-vars
app.use((err, _, res, __) => {
// Do logging and user-friendly error message display
logger.error(err);
res
.status(500)
.send({ status: 500, message: "internal error", type: "internal" });
});
const createServer = async () => {
try {
if (program.usetls) {
const [key, cert] = await Promise.all([
FS.readFile(program.usetls + "/key.pem"),
FS.readFile(program.usetls + "/cert.pem")
]);
const httpsServer = Https.createServer({ key, cert }, app);
return httpsServer;
}
const httpServer = Http.Server(app);
return httpServer;
} catch (err) {
logger.error(err.message);
throw err;
}
};
const serverInstance = await createServer();
const io = require("socket.io")(serverInstance);
// setup sockets =================
const lndLogfile = program.lndlogfile || defaults.lndLogFile;
const Sockets = require("./sockets")(
io,
lightning,
lnd,
program.user,
program.pwd,
program.limituser,
program.limitpwd,
lndLogfile,
lnServicesData
);
require("./routes")(
app,
lightning,
defaults,
walletUnlocker,
lnServicesData,
Sockets,
{
serverHost: module.serverHost,
serverPort: module.serverPort
}
);
// enable CORS headers
app.use(require("./cors"));
// app.use(bodyParser.json({limit: '100000mb'}));
app.use(bodyParser.json({ limit: "50mb" }));
app.use(bodyParser.urlencoded({ limit: "50mb", extended: true }));
serverInstance.listen(module.serverPort, module.serverhost);
logger.info(
"App listening on " + module.serverHost + " port " + module.serverPort
);
module.server = serverInstance;
// const localtunnel = require('localtunnel');
//
// const tunnel = localtunnel(port, (err, t) => {
// console.log('err', err);
// console.log('t', t.url);
// });
} catch (err) {
console.error(err);
logger.info("Restarting server in 30 seconds...");
await wait(30);
startServer();
return false;
}
};
startServer();
};
module.exports = server;

147
src/sockets.js Normal file
View file

@ -0,0 +1,147 @@
// app/sockets.js
const logger = require("winston");
const FS = require("../utils/fs");
module.exports = (
io,
lightning,
lnd,
login,
pass,
limitlogin,
limitpass,
lndLogfile,
lnServicesData
) => {
const Mediator = require("../services/gunDB/Mediator/index.js");
const EventEmitter = require("events");
class MySocketsEvents extends EventEmitter {}
const mySocketsEvents = new MySocketsEvents();
const clients = [];
const authEnabled = (login && pass) || (limitlogin && limitpass);
let userToken = null;
let limitUserToken = null;
if (login && pass) {
userToken = Buffer.from(login + ":" + pass).toString("base64");
}
if (limitlogin && limitpass) {
limitUserToken = Buffer.from(limitlogin + ":" + limitpass).toString(
"base64"
);
}
// register the lnd invoices listener
const registerLndInvoiceListener = socket => {
socket._invoiceListener = {
dataReceived(data) {
socket.emit("invoice", data);
}
};
lnd.registerInvoiceListener(socket._invoiceListener);
};
// unregister the lnd invoices listener
const unregisterLndInvoiceListener = socket => {
lnd.unregisterInvoiceListener(socket._invoiceListener);
};
// register the socket listeners
const registerSocketListeners = socket => {
registerLndInvoiceListener(socket);
};
// unregister the socket listeners
const unregisterSocketListeners = socket => {
unregisterLndInvoiceListener(socket);
};
const getSocketAuthToken = socket => {
if (socket.handshake.query.auth) {
return socket.handshake.query.auth;
} else if (socket.handshake.headers.authorization) {
return socket.handshake.headers.authorization.substr(6);
}
socket.disconnect("unauthorized");
return null;
};
io.on("connection", async socket => {
// this is where we create the websocket connection
// with the GunDB service.
Mediator.createMediator(socket);
const macaroonExists = await FS.access(lnServicesData.macaroonPath);
const lnServices = await require("../services/lnd/lightning")(
lnServicesData.lndProto,
lnServicesData.lndHost,
lnServicesData.lndCertPath,
macaroonExists ? lnServicesData.macaroonPath : null
);
// eslint-disable-next-line prefer-destructuring, no-param-reassign
lightning = lnServices.lightning;
mySocketsEvents.addListener("updateLightning", async () => {
const newMacaroonExists = await FS.access(lnServicesData.macaroonPath);
const newLNServices = await require("../services/lnd/lightning")(
lnServicesData.lndProto,
lnServicesData.lndHost,
lnServicesData.lndCertPath,
newMacaroonExists ? lnServicesData.macaroonPath : null
);
// eslint-disable-next-line prefer-destructuring, no-param-reassign
lightning = newLNServices.lightning;
});
logger.debug("socket.handshake", socket.handshake);
if (authEnabled) {
try {
const authorizationHeaderToken = getSocketAuthToken(socket);
if (authorizationHeaderToken === userToken) {
socket._limituser = false;
} else if (authorizationHeaderToken === limitUserToken) {
socket._limituser = true;
} else {
socket.disconnect("unauthorized");
return;
}
} catch (err) {
// probably because of missing authorization header
logger.debug(err);
socket.disconnect("unauthorized");
return;
}
} else {
socket._limituser = false;
}
/** printing out the client who joined */
logger.debug("New socket client connected (id=" + socket.id + ").");
socket.emit("hello", { limitUser: socket._limituser });
socket.broadcast.emit("hello", { remoteAddress: socket.handshake.address });
/** pushing new client to client array*/
clients.push(socket);
registerSocketListeners(socket);
/** listening if client has disconnected */
socket.on("disconnect", () => {
clients.splice(clients.indexOf(socket), 1);
unregisterSocketListeners(socket);
logger.debug("client disconnected (id=" + socket.id + ").");
});
});
return mySocketsEvents;
};

64
tsconfig.json Normal file
View file

@ -0,0 +1,64 @@
{
"include": ["./services/gunDB/**/*.*"],
"compilerOptions": {
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */,
"module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
// "lib": [], /* Specify library files to be included in the compilation. */
"allowJs": true /* Allow javascript files to be compiled. */,
"checkJs": true /* Report errors in .js files. */,
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
// "outDir": "./", /* Redirect output structure to the directory. */
// "rootDir": "./" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */,
// "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
// "removeComments": true, /* Do not emit comments to output. */
"noEmit": true /* Do not emit outputs. */,
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true /* Enable all strict type-checking options. */,
"noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */,
"strictNullChecks": true /* Enable strict null checks. */,
"strictFunctionTypes": true /* Enable strict checking of function types. */,
"strictBindCallApply": true /* Enable strict 'bind', 'call', and 'apply' methods on functions. */,
"strictPropertyInitialization": true /* Enable strict checking of property initialization in classes. */,
"noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */,
"alwaysStrict": true /* Parse in strict mode and emit "use strict" for each source file. */,
/* Additional Checks */
"noUnusedLocals": true /* Report errors on unused locals. */,
"noUnusedParameters": true /* Report errors on unused parameters. */,
"noImplicitReturns": true /* Report error when not all code paths in function return a value. */,
"noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */,
/* Module Resolution Options */
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
}
}

16
utils/colors.js Normal file
View file

@ -0,0 +1,16 @@
const colors = require("colors/safe");
colors.setTheme({
silly: "rainbow",
input: "grey",
verbose: "cyan",
prompt: "grey",
info: "green",
data: "grey",
help: "cyan",
warn: "yellow",
debug: "blue",
error: "red"
});
module.exports = colors;

16
utils/fs.js Normal file
View file

@ -0,0 +1,16 @@
const { promisify } = require("util");
const FS = require("fs");
module.exports = {
access: path =>
new Promise(resolve => {
FS.access(path, FS.constants.F_OK, err => {
resolve(!err);
});
}),
exists: promisify(FS.exists),
readFile: promisify(FS.readFile),
writeFile: promisify(FS.writeFile),
readdir: promisify(FS.readdir),
unlink: promisify(FS.unlink)
};

14
utils/paginate.js Normal file
View file

@ -0,0 +1,14 @@
const getListPage = ({ entries = [], itemsPerPage = 20, page = 1 }) => {
const totalPages = Math.ceil(entries.length / itemsPerPage);
const offset = (page - 1) * itemsPerPage;
const limit = page * itemsPerPage;
const paginatedContent = entries.slice(offset, limit);
return {
content: paginatedContent,
page,
totalPages,
totalItems: entries.length
};
};
module.exports = getListPage;

12
utils/server-utils.js Normal file
View file

@ -0,0 +1,12 @@
module.exports = server => {
const module = {};
server.getURL = function getURL() {
const HTTPSPort = this.httpsPort === "443" ? "" : ":" + this.httpsPort;
const HTTPPort = this.serverPort === "80" ? "" : ":" + this.serverPort;
const URLPort = this.useTLS ? HTTPSPort : HTTPPort;
return `http${this.useTLS ? "s" : ""}://${this.serverHost}${URLPort}`;
};
return module;
};

6494
yarn.lock Normal file

File diff suppressed because it is too large Load diff