Merge pull request #484 from shocknet/gunsmith-rebased
Gunsmith rebased
This commit is contained in:
commit
951055ea0c
31 changed files with 2656 additions and 2503 deletions
|
|
@ -1,10 +1,14 @@
|
|||
{
|
||||
"extends": ["eslint:all", "prettier", "plugin:jest/all"],
|
||||
"plugins": ["prettier", "jest", "babel"],
|
||||
"extends": ["eslint:all", "prettier", "plugin:mocha/recommended"],
|
||||
"plugins": ["prettier", "mocha", "babel"],
|
||||
"rules": {
|
||||
"prettier/prettier": "error",
|
||||
"strict": "off",
|
||||
|
||||
"mocha/no-mocha-arrows": "off",
|
||||
|
||||
"max-statements-per-line": "off",
|
||||
|
||||
"no-empty-function": "off",
|
||||
|
||||
"no-console": "off",
|
||||
|
|
|
|||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -6,7 +6,9 @@ services/auth/secrets.json
|
|||
# New logger date format
|
||||
*.log.*
|
||||
.directory
|
||||
.DS_Store
|
||||
|
||||
test-radata/
|
||||
radata/
|
||||
radata-*.tmp
|
||||
*.cert
|
||||
|
|
|
|||
1
.nvmrc
Normal file
1
.nvmrc
Normal file
|
|
@ -0,0 +1 @@
|
|||
v14.17.6
|
||||
6
.vscode/settings.json
vendored
6
.vscode/settings.json
vendored
|
|
@ -5,12 +5,18 @@
|
|||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"cSpell.words": [
|
||||
"acked",
|
||||
"Authing",
|
||||
"endregion",
|
||||
"epriv",
|
||||
"Epub",
|
||||
"falsey",
|
||||
"GUNRPC",
|
||||
"ISEA",
|
||||
"PUBKEY",
|
||||
"radata",
|
||||
"Reqs",
|
||||
"thenables",
|
||||
"uuidv"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
3
main.js
3
main.js
|
|
@ -18,6 +18,9 @@ program
|
|||
.option("-c, --mainnet", "run server on mainnet mode")
|
||||
.option("-t, --tunnel","create a localtunnel to listen behind a firewall")
|
||||
.option('-r, --lndaddress', 'Lnd address, defaults to 127.0.0.1:9735')
|
||||
.option('-a, --use-TLS', 'use TLS')
|
||||
.option('-i, --https-cert [path]', 'HTTPS certificate path')
|
||||
.option('-y, --https-cert-key [path]', 'HTTPS certificate key path')
|
||||
.parse(process.argv);
|
||||
|
||||
// load server
|
||||
|
|
|
|||
14
package.json
14
package.json
|
|
@ -7,8 +7,7 @@
|
|||
"start": "node main.js -h 0.0.0.0 -c",
|
||||
"dev": "node --trace-warnings --max-old-space-size=4096 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",
|
||||
"test": "mocha ./utils -b -t 50000 --recursive",
|
||||
"typecheck": "tsc",
|
||||
"lint": "eslint \"services/gunDB/**/*.js\"",
|
||||
"format": "prettier --write \"./**/*.js\"",
|
||||
|
|
@ -31,7 +30,7 @@
|
|||
"command-exists": "^1.2.6",
|
||||
"commander": "^2.9.0",
|
||||
"compression": "^1.7.4",
|
||||
"cors": "^2.8.4",
|
||||
"cors": "^2.8.5",
|
||||
"debug": "^3.1.0",
|
||||
"dotenv": "^8.1.0",
|
||||
"eccrypto": "^1.1.6",
|
||||
|
|
@ -40,7 +39,7 @@
|
|||
"google-proto-files": "^1.0.3",
|
||||
"graphviz": "0.0.8",
|
||||
"grpc": "1.24.4",
|
||||
"gun": "git://github.com/amark/gun#97aa976c97e6219a9f93095d32c220dcd371ca62",
|
||||
"gun": "amark/gun#50af2d52ad6677fd5b95e5ed64fca7491c7877b5",
|
||||
"husky": "^4.2.5",
|
||||
"hybrid-relay-client": "git://github.com/shocknet/hybridRelayClient#a99e57794cf7a62f0f5b6aef53a35d6b77d0a889",
|
||||
"jsonfile": "^4.0.0",
|
||||
|
|
@ -73,9 +72,9 @@
|
|||
"@types/eccrypto": "^1.1.2",
|
||||
"@types/express": "^4.17.1",
|
||||
"@types/gun": "^0.9.2",
|
||||
"@types/jest": "^24.0.18",
|
||||
"@types/jsonwebtoken": "^8.3.7",
|
||||
"@types/lodash": "^4.14.168",
|
||||
"@types/mocha": "^9.0.0",
|
||||
"@types/node-fetch": "^2.5.8",
|
||||
"@types/node-persist": "^3.1.1",
|
||||
"@types/ramda": "types/npm-ramda#dist",
|
||||
|
|
@ -86,10 +85,11 @@
|
|||
"eslint": "^6.6.0",
|
||||
"eslint-config-prettier": "^6.5.0",
|
||||
"eslint-plugin-babel": "^5.3.1",
|
||||
"eslint-plugin-jest": "^22.20.1",
|
||||
"eslint-plugin-mocha": "^9.0.0",
|
||||
"eslint-plugin-prettier": "^3.1.4",
|
||||
"jest": "^24.9.0",
|
||||
"expect": "^27.2.1",
|
||||
"lint-staged": "^10.2.2",
|
||||
"mocha": "^9.1.1",
|
||||
"nodemon": "^2.0.7",
|
||||
"prettier": "^1.18.2",
|
||||
"random-words": "^1.1.1",
|
||||
|
|
|
|||
|
|
@ -2,18 +2,20 @@
|
|||
* @format
|
||||
*/
|
||||
const Common = require('shock-common')
|
||||
const Gun = require('gun')
|
||||
const Gun = require('../../../utils/GunSmith')
|
||||
// @ts-ignore
|
||||
require('gun/nts')
|
||||
const logger = require('../../../config/log')
|
||||
// @ts-ignore
|
||||
Gun.log = () => {}
|
||||
// Gun.log = () => {}
|
||||
// @ts-ignore
|
||||
require('gun/lib/open')
|
||||
// @ts-ignore
|
||||
require('gun/lib/load')
|
||||
//@ts-ignore
|
||||
const { encryptedEmit, encryptedOn } = require('../../../utils/ECC/socket')
|
||||
const Key = require('../contact-api/key')
|
||||
const Config = require('../config')
|
||||
|
||||
/** @type {import('../contact-api/SimpleGUN').ISEA} */
|
||||
// @ts-ignore
|
||||
|
|
@ -256,19 +258,16 @@ const API = require('../contact-api/index')
|
|||
|
||||
/* eslint-disable init-declarations */
|
||||
|
||||
/** @type {GUNNode} */
|
||||
// @ts-ignore
|
||||
let gun
|
||||
const gun = Gun({
|
||||
axe: false,
|
||||
multicast: false,
|
||||
peers: Config.PEERS
|
||||
})
|
||||
|
||||
/** @type {UserGUNNode} */
|
||||
let user
|
||||
const user = gun.user()
|
||||
|
||||
/* eslint-enable init-declarations */
|
||||
|
||||
/** @type {string|null} */
|
||||
let _currentAlias = null
|
||||
/** @type {string|null} */
|
||||
|
||||
/** @type {string|null} */
|
||||
let mySec = null
|
||||
|
||||
|
|
@ -298,10 +297,9 @@ const getUser = () => {
|
|||
* Returns a promise containing the public key of the newly created user.
|
||||
* @param {string} alias
|
||||
* @param {string} pass
|
||||
* @param {UserGUNNode=} __user
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
const authenticate = async (alias, pass, __user) => {
|
||||
const authenticate = async (alias, pass) => {
|
||||
if (!Common.isPopulatedString(alias)) {
|
||||
throw new TypeError(
|
||||
`Expected alias to be a populated string, instead got: ${alias}`
|
||||
|
|
@ -312,45 +310,6 @@ const authenticate = async (alias, pass, __user) => {
|
|||
`Expected pass to be a populated string, instead got: ${pass}`
|
||||
)
|
||||
}
|
||||
const _user = __user || user
|
||||
const isFreshGun = _user !== user
|
||||
if (isFreshGun) {
|
||||
const ack = await new Promise(res => {
|
||||
_user.auth(alias, pass, _ack => {
|
||||
res(_ack)
|
||||
})
|
||||
})
|
||||
|
||||
if (typeof ack.err === 'string') {
|
||||
throw new Error(ack.err)
|
||||
} else if (typeof ack.sea === 'object') {
|
||||
// clock skew
|
||||
await new Promise(res => setTimeout(res, 2000))
|
||||
|
||||
return ack.sea.pub
|
||||
} else {
|
||||
throw new Error('Unknown error.')
|
||||
}
|
||||
}
|
||||
|
||||
if (isAuthenticated()) {
|
||||
if (alias !== _currentAlias) {
|
||||
throw new Error(
|
||||
`Tried to re-authenticate with an alias different to that of stored one, tried: ${alias} - stored: ${_currentAlias}, logoff first if need to change aliases.`
|
||||
)
|
||||
}
|
||||
|
||||
// clock skew
|
||||
await new Promise(res => setTimeout(res, 2000))
|
||||
|
||||
// move this to a subscription; implement off() ? todo
|
||||
API.Jobs.onOrders(_user, gun, mySEA)
|
||||
API.Jobs.lastSeenNode(_user)
|
||||
|
||||
API.Events.onSeedBackup(() => {}, user, mySEA)
|
||||
|
||||
return _user._.sea.pub
|
||||
}
|
||||
|
||||
if (isAuthenticating()) {
|
||||
throw new Error(
|
||||
|
|
@ -361,7 +320,7 @@ const authenticate = async (alias, pass, __user) => {
|
|||
_isAuthenticating = true
|
||||
|
||||
const ack = await new Promise(res => {
|
||||
_user.auth(alias, pass, _ack => {
|
||||
user.auth(alias, pass, _ack => {
|
||||
res(_ack)
|
||||
})
|
||||
})
|
||||
|
|
@ -371,15 +330,36 @@ const authenticate = async (alias, pass, __user) => {
|
|||
if (typeof ack.err === 'string') {
|
||||
throw new Error(ack.err)
|
||||
} else if (typeof ack.sea === 'object') {
|
||||
mySec = await mySEA.secret(_user._.sea.epub, _user._.sea)
|
||||
mySec = await mySEA.secret(user._.sea.epub, user._.sea)
|
||||
// clock skew
|
||||
await new Promise(res => setTimeout(res, 2000))
|
||||
|
||||
_currentAlias = alias
|
||||
|
||||
await new Promise(res => setTimeout(res, 5000))
|
||||
|
||||
API.Jobs.onOrders(_user, gun, mySEA)
|
||||
API.Jobs.lastSeenNode(_user)
|
||||
await /** @type {Promise<void>} */ (new Promise((res, rej) => {
|
||||
user.get(Key.FOLLOWS).put(
|
||||
{
|
||||
unused: null
|
||||
},
|
||||
ack => {
|
||||
if (ack.err && typeof ack.err !== 'number') {
|
||||
rej(
|
||||
new Error(
|
||||
`Error initializing follows: ${JSON.stringify(
|
||||
ack.err,
|
||||
null,
|
||||
4
|
||||
)}`
|
||||
)
|
||||
)
|
||||
} else {
|
||||
res()
|
||||
}
|
||||
}
|
||||
)
|
||||
}))
|
||||
|
||||
// move this to a subscription; implement off() ? todo
|
||||
API.Jobs.onOrders(user, gun, mySEA)
|
||||
API.Jobs.lastSeenNode(user)
|
||||
API.Events.onSeedBackup(() => {}, user, mySEA)
|
||||
|
||||
return ack.sea.pub
|
||||
|
|
@ -392,37 +372,8 @@ const authenticate = async (alias, pass, __user) => {
|
|||
}
|
||||
}
|
||||
|
||||
const instantiateGun = () => {
|
||||
const Config = require('../config')
|
||||
// if (user) {
|
||||
// user.leave()
|
||||
// }
|
||||
// @ts-ignore
|
||||
user = null
|
||||
if (gun) {
|
||||
gun.off()
|
||||
}
|
||||
// @ts-ignore
|
||||
gun = null
|
||||
|
||||
const _gun = /** @type {unknown} */ (new Gun({
|
||||
axe: false,
|
||||
multicast: false,
|
||||
peers: Config.PEERS
|
||||
}))
|
||||
|
||||
gun = /** @type {GUNNode} */ (_gun)
|
||||
|
||||
user = gun.user()
|
||||
}
|
||||
|
||||
instantiateGun()
|
||||
|
||||
const freshGun = () => {
|
||||
return {
|
||||
gun,
|
||||
user
|
||||
}
|
||||
const logoff = () => {
|
||||
user.leave()
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -512,22 +463,24 @@ const register = async (alias, pass) => {
|
|||
// 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
|
||||
instantiateGun()
|
||||
// instantiateGun()
|
||||
|
||||
logoff()
|
||||
|
||||
return authenticate(alias, pass)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
authenticate,
|
||||
instantiateGun,
|
||||
isAuthenticated,
|
||||
isAuthenticating,
|
||||
isRegistering,
|
||||
gun,
|
||||
user,
|
||||
register,
|
||||
getGun,
|
||||
getUser,
|
||||
mySEA,
|
||||
getMySecret,
|
||||
freshGun,
|
||||
$$__SHOCKWALLET__ENCRYPTED__
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@
|
|||
*/
|
||||
const uuidv1 = require('uuid/v1')
|
||||
const logger = require('../../../config/log')
|
||||
const throttle = require('lodash/throttle')
|
||||
const Common = require('shock-common')
|
||||
const { Constants, Schema } = Common
|
||||
const Gun = require('gun')
|
||||
|
||||
const { ErrorCode } = Constants
|
||||
|
||||
|
|
@ -25,9 +25,11 @@ const SchemaManager = require('../../schema')
|
|||
const LNDHealthMananger = require('../../../utils/lightningServices/errors')
|
||||
const { enrollContentTokens, selfContentToken } = require('../../seed')
|
||||
|
||||
/// <reference path="../../../utils/GunSmith/Smith.ts" />
|
||||
|
||||
/**
|
||||
* @typedef {import('./SimpleGUN').ISEA} ISEA
|
||||
* @typedef {import('./SimpleGUN').UserGUNNode} UserGUNNode
|
||||
* @typedef {Smith.UserSmithNode} UserGUNNode
|
||||
*/
|
||||
|
||||
/**
|
||||
|
|
@ -759,42 +761,14 @@ const saveChannelsBackup = async (backups, user, SEA) => {
|
|||
/**
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const setLastSeenApp = () =>
|
||||
/** @type {Promise<void>} */ (new Promise((res, rej) => {
|
||||
require('../Mediator')
|
||||
.getUser()
|
||||
.get(Key.LAST_SEEN_APP)
|
||||
.put(Date.now(), ack => {
|
||||
if (
|
||||
ack.err &&
|
||||
typeof ack.err !== 'number' &&
|
||||
typeof ack.err !== 'object'
|
||||
) {
|
||||
rej(new Error(ack.err))
|
||||
} else {
|
||||
res()
|
||||
}
|
||||
})
|
||||
})).then(
|
||||
() =>
|
||||
new Promise((res, rej) => {
|
||||
require('../Mediator')
|
||||
.getUser()
|
||||
.get(Key.PROFILE)
|
||||
.get(Key.LAST_SEEN_APP)
|
||||
.put(Date.now(), ack => {
|
||||
if (
|
||||
ack.err &&
|
||||
typeof ack.err !== 'number' &&
|
||||
typeof ack.err !== 'object'
|
||||
) {
|
||||
rej(new Error(ack.err))
|
||||
} else {
|
||||
res()
|
||||
}
|
||||
})
|
||||
})
|
||||
)
|
||||
const setLastSeenApp = throttle(() => {
|
||||
const user = require('../Mediator').getUser()
|
||||
|
||||
return user
|
||||
.get(Key.PROFILE)
|
||||
.get(Key.LAST_SEEN_APP)
|
||||
.pPut(Date.now())
|
||||
}, 10000)
|
||||
|
||||
/**
|
||||
* @param {string[]} tags
|
||||
|
|
@ -816,8 +790,7 @@ const createPostNew = async (tags, title, content) => {
|
|||
const mySecret = require('../Mediator').getMySecret()
|
||||
|
||||
await Common.Utils.asyncForEach(content, async c => {
|
||||
// @ts-expect-error
|
||||
const uuid = Gun.text.random()
|
||||
const uuid = Utils.gunID()
|
||||
newPost.contentItems[uuid] = c
|
||||
if (
|
||||
(c.type === 'image/embedded' || c.type === 'video/embedded') &&
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ const {
|
|||
} = require('shock-common')
|
||||
|
||||
const Key = require('../key')
|
||||
/// <reference path="../../../utils/GunSmith/Smith.ts" />
|
||||
|
||||
const DEBOUNCE_WAIT_TIME = 500
|
||||
|
||||
|
|
@ -16,7 +17,7 @@ let currentSeedBackup = null
|
|||
|
||||
/**
|
||||
* @param {(seedBackup: string|null) => void} cb
|
||||
* @param {import('../SimpleGUN').UserGUNNode} user
|
||||
* @param {Smith.UserSmithNode} user
|
||||
* @param {import('../SimpleGUN').ISEA} SEA
|
||||
* @throws {Error} If user hasn't been auth.
|
||||
* @returns {void}
|
||||
|
|
|
|||
|
|
@ -3,19 +3,17 @@
|
|||
*/
|
||||
|
||||
const Key = require('../key')
|
||||
const Utils = require('../utils')
|
||||
|
||||
/**
|
||||
* @param {string} pub
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
exports.currentOrderAddress = async pub => {
|
||||
const currAddr = await Utils.tryAndWait(gun =>
|
||||
gun
|
||||
.user(pub)
|
||||
.get(Key.CURRENT_ORDER_ADDRESS)
|
||||
.then()
|
||||
)
|
||||
const currAddr = await require('../../Mediator')
|
||||
.getGun()
|
||||
.user(pub)
|
||||
.get(Key.CURRENT_ORDER_ADDRESS)
|
||||
.specialThen()
|
||||
|
||||
if (typeof currAddr !== 'string') {
|
||||
throw new TypeError('Expected user.currentOrderAddress to be an string')
|
||||
|
|
|
|||
|
|
@ -11,12 +11,13 @@ const {
|
|||
}
|
||||
} = require('shock-common')
|
||||
const Key = require('../key')
|
||||
/// <reference path="../../../utils/GunSmith/Smith.ts" />
|
||||
|
||||
/**
|
||||
* @typedef {import('../SimpleGUN').GUNNode} GUNNode
|
||||
* @typedef {import('../SimpleGUN').ListenerData} ListenerData
|
||||
* @typedef {Smith.GunSmithNode} GUNNode
|
||||
* @typedef {GunT.ListenerData} ListenerData
|
||||
* @typedef {import('../SimpleGUN').ISEA} ISEA
|
||||
* @typedef {import('../SimpleGUN').UserGUNNode} UserGUNNode
|
||||
* @typedef {Smith.UserSmithNode} UserGUNNode
|
||||
*/
|
||||
|
||||
/**
|
||||
|
|
@ -26,32 +27,12 @@ const Key = require('../key')
|
|||
*/
|
||||
const lastSeenNode = user => {
|
||||
if (!user.is) {
|
||||
logger.warn('onOrders() -> tried to sub without authing')
|
||||
logger.warn('lastSeenNode() -> tried to sub without authing')
|
||||
throw new Error(ErrorCode.NOT_AUTH)
|
||||
}
|
||||
let gotLatestUserAck = true
|
||||
|
||||
let gotLatestProfileAck = true
|
||||
|
||||
setInterval(() => {
|
||||
if (!user.is) {
|
||||
return
|
||||
}
|
||||
if (!gotLatestUserAck) {
|
||||
logger.error(`lastSeenNode user job: didnt get latest ack`)
|
||||
return
|
||||
}
|
||||
gotLatestUserAck = false
|
||||
user.get(Key.LAST_SEEN_NODE).put(Date.now(), ack => {
|
||||
if (
|
||||
ack.err &&
|
||||
typeof ack.err !== 'number' &&
|
||||
typeof ack.err !== 'object'
|
||||
) {
|
||||
logger.error(`Error inside lastSeenNode user job: ${ack.err}`)
|
||||
}
|
||||
gotLatestUserAck = true
|
||||
})
|
||||
}, LAST_SEEN_NODE_INTERVAL)
|
||||
setInterval(() => {
|
||||
if (!user.is) {
|
||||
return
|
||||
|
|
|
|||
|
|
@ -15,9 +15,8 @@ const SchemaManager = require('../../../schema')
|
|||
const LightningServices = require('../../../../utils/lightningServices')
|
||||
const Key = require('../key')
|
||||
const Utils = require('../utils')
|
||||
const Gun = require('gun')
|
||||
const { selfContentToken, enrollContentTokens } = require('../../../seed')
|
||||
|
||||
/// <reference path="../../../utils/GunSmith/Smith.ts" />
|
||||
const TipForwarder = require('../../../tipsCallback')
|
||||
|
||||
const getUser = () => require('../../Mediator').getUser()
|
||||
|
|
@ -28,10 +27,10 @@ const getUser = () => require('../../Mediator').getUser()
|
|||
const ordersProcessed = new Set()
|
||||
|
||||
/**
|
||||
* @typedef {import('../SimpleGUN').GUNNode} GUNNode
|
||||
* @typedef {import('../SimpleGUN').ListenerData} ListenerData
|
||||
* @typedef {Smith.GunSmithNode} GUNNode
|
||||
* @typedef {GunT.ListenerData} ListenerData
|
||||
* @typedef {import('../SimpleGUN').ISEA} ISEA
|
||||
* @typedef {import('../SimpleGUN').UserGUNNode} UserGUNNode
|
||||
* @typedef {Smith.UserSmithNode} UserGUNNode
|
||||
*/
|
||||
|
||||
/**
|
||||
|
|
@ -89,13 +88,22 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
|
|||
try {
|
||||
if (addr !== currentOrderAddr) {
|
||||
logger.info(
|
||||
orderID,
|
||||
`order address: ${addr} invalidated (current address: ${currentOrderAddr})`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Was recycled
|
||||
if (order === null) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!Schema.isOrder(order)) {
|
||||
logger.info(`Expected an order instead got: ${JSON.stringify(order)}`)
|
||||
logger.info(
|
||||
orderID,
|
||||
`Expected an order instead got: ${JSON.stringify(order)}`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -109,15 +117,25 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
|
|||
|
||||
ordersProcessed.add(orderID)
|
||||
|
||||
if (Date.now() - order.timestamp > 66000) {
|
||||
logger.info('Not processing old order', orderID)
|
||||
return
|
||||
}
|
||||
|
||||
logger.info('processing order ', orderID)
|
||||
|
||||
const alreadyAnswered = await getUser()
|
||||
.get(Key.ORDER_TO_RESPONSE)
|
||||
.get(orderID)
|
||||
.then()
|
||||
|
||||
if (alreadyAnswered) {
|
||||
logger.info(orderID, 'alreadyAnswered')
|
||||
return
|
||||
}
|
||||
|
||||
logger.info(orderID, ' was not answered, will now answer')
|
||||
|
||||
const senderEpub = await Utils.pubToEpub(order.from)
|
||||
const secret = await SEA.secret(senderEpub, getUser()._.sea)
|
||||
|
||||
|
|
@ -130,19 +148,19 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
|
|||
|
||||
if (!isNumber(amount)) {
|
||||
throw new TypeError(
|
||||
`Could not parse decrypted amount as a number, not a number?, decryptedAmount: ${decryptedAmount}`
|
||||
`${orderID} Could not parse decrypted amount as a number, not a number?, decryptedAmount: ${decryptedAmount}`
|
||||
)
|
||||
}
|
||||
|
||||
if (isNaN(amount)) {
|
||||
throw new TypeError(
|
||||
`Could not parse decrypted amount as a number, got NaN, decryptedAmount: ${decryptedAmount}`
|
||||
`${orderID} Could not parse decrypted amount as a number, got NaN, decryptedAmount: ${decryptedAmount}`
|
||||
)
|
||||
}
|
||||
|
||||
if (!isFinite(amount)) {
|
||||
throw new TypeError(
|
||||
`Amount was correctly decrypted, but got a non finite number, decryptedAmount: ${decryptedAmount}`
|
||||
`${orderID} Amount was correctly decrypted, but got a non finite number, decryptedAmount: ${decryptedAmount}`
|
||||
)
|
||||
}
|
||||
const mySecret = require('../../Mediator').getMySecret()
|
||||
|
|
@ -155,32 +173,34 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
|
|||
*/
|
||||
let serviceOrderContentSeedInfo = null //in case the service is of type 'torrentSeed' this is {seedUrl,seedToken}, can be omitted, in that case, it will be taken from env
|
||||
if (order.targetType === 'service') {
|
||||
logger.info('General Service')
|
||||
logger.info(orderID, 'General Service')
|
||||
const { ackInfo: serviceID } = order
|
||||
logger.info('ACK INFO')
|
||||
logger.info(serviceID)
|
||||
logger.info(orderID, 'ACK INFO')
|
||||
logger.info(orderID, serviceID)
|
||||
if (!Common.isPopulatedString(serviceID)) {
|
||||
throw new TypeError(`no serviceID provided to orderAck`)
|
||||
throw new TypeError(`${orderID} no serviceID provided to orderAck`)
|
||||
}
|
||||
const selectedService = await new Promise(res => {
|
||||
getUser()
|
||||
.get(Key.OFFERED_SERVICES)
|
||||
.get(serviceID)
|
||||
.load(res)
|
||||
})
|
||||
logger.info(selectedService)
|
||||
if (!selectedService) {
|
||||
throw new TypeError(`invalid serviceID provided to orderAck`)
|
||||
const selectedService = await getUser()
|
||||
.get(Key.OFFERED_SERVICES)
|
||||
.get(serviceID)
|
||||
.then()
|
||||
|
||||
logger.info(orderID, selectedService)
|
||||
if (!Common.isObj(selectedService)) {
|
||||
throw new TypeError(
|
||||
`${orderID} invalid serviceID provided to orderAck or service is not an object`
|
||||
)
|
||||
}
|
||||
|
||||
const {
|
||||
serviceType,
|
||||
servicePrice,
|
||||
serviceSeedUrl: encSeedUrl, //=
|
||||
serviceSeedToken: encSeedToken //=
|
||||
} = selectedService
|
||||
} = /** @type {Record<string, any>} */ (selectedService)
|
||||
if (Number(amount) !== Number(servicePrice)) {
|
||||
throw new TypeError(
|
||||
`service price mismatch ${amount} : ${servicePrice}`
|
||||
`${orderID} service price mismatch ${amount} : ${servicePrice}`
|
||||
)
|
||||
}
|
||||
if (serviceType === 'torrentSeed') {
|
||||
|
|
@ -204,16 +224,16 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
|
|||
const invoice = await _addInvoice(invoiceReq)
|
||||
|
||||
logger.info(
|
||||
'onOrders() -> Successfully created the invoice, will now encrypt it'
|
||||
`${orderID} onOrders() -> Successfully created the invoice, will now encrypt it`
|
||||
)
|
||||
|
||||
const encInvoice = await SEA.encrypt(invoice.payment_request, secret)
|
||||
|
||||
logger.info(
|
||||
`onOrders() -> Will now place the encrypted invoice in order to response usergraph: ${addr}`
|
||||
`${orderID} onOrders() -> Will now place the encrypted invoice in order to response usergraph: ${addr}`
|
||||
)
|
||||
//@ts-expect-error
|
||||
const ackNode = Gun.text.random()
|
||||
|
||||
const ackNode = Utils.gunID()
|
||||
|
||||
/** @type {import('shock-common').Schema.OrderResponse} */
|
||||
const orderResponse = {
|
||||
|
|
@ -235,7 +255,7 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
|
|||
) {
|
||||
rej(
|
||||
new Error(
|
||||
`Error saving encrypted invoice to order to response usergraph: ${ack}`
|
||||
`${orderID} Error saving encrypted invoice to order to response usergraph: ${ack}`
|
||||
)
|
||||
)
|
||||
} else {
|
||||
|
|
@ -250,7 +270,15 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
|
|||
* @param {Common.Schema.InvoiceWhenListed & {r_hash:Buffer,payment_addr:Buffer}} paidInvoice
|
||||
*/
|
||||
const invoicePaidCb = async paidInvoice => {
|
||||
logger.info('INVOICE PAID')
|
||||
logger.info(orderID, 'INVOICE PAID')
|
||||
// Recycle
|
||||
require('../../Mediator')
|
||||
.getGun()
|
||||
.get('orderNodes')
|
||||
.get(addr)
|
||||
.get(orderID)
|
||||
.put(null)
|
||||
|
||||
let breakError = null
|
||||
let orderMetadata //eslint-disable-line init-declarations
|
||||
const hashString = paidInvoice.r_hash.toString('hex')
|
||||
|
|
@ -266,7 +294,7 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
|
|||
case 'tip': {
|
||||
const postID = ackInfo
|
||||
if (!Common.isPopulatedString(postID)) {
|
||||
breakError = 'invalid ackInfo provided for postID'
|
||||
breakError = orderID + ' invalid ackInfo provided for postID'
|
||||
break //create the coordinate, but stop because of the invalid id
|
||||
}
|
||||
getUser()
|
||||
|
|
@ -300,7 +328,7 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
|
|||
) {
|
||||
rej(
|
||||
new Error(
|
||||
`Error saving encrypted orderAck to order to response usergraph: ${ack}`
|
||||
`${orderID} Error saving encrypted orderAck to order to response usergraph: ${ack}`
|
||||
)
|
||||
)
|
||||
} else {
|
||||
|
|
@ -316,23 +344,27 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
|
|||
break
|
||||
}
|
||||
case 'contentReveal': {
|
||||
logger.info('cONTENT REVEAL')
|
||||
logger.info(orderID, 'CONTENT REVEAL')
|
||||
//assuming digital product that only requires to be unlocked
|
||||
const postID = ackInfo
|
||||
logger.info('ACK INFO')
|
||||
logger.info(orderID, 'ACK INFO')
|
||||
logger.info(ackInfo)
|
||||
if (!Common.isPopulatedString(postID)) {
|
||||
breakError = 'invalid ackInfo provided for postID'
|
||||
break //create the coordinate, but stop because of the invalid id
|
||||
}
|
||||
logger.info('IS STRING')
|
||||
const selectedPost = await new Promise(res => {
|
||||
getUser()
|
||||
.get(Key.POSTS_NEW)
|
||||
.get(postID)
|
||||
.load(res)
|
||||
})
|
||||
logger.info('LOAD ok')
|
||||
logger.info(orderID, 'IS STRING')
|
||||
const selectedPost = /** @type {Record<string, any>} */ (await getUser()
|
||||
.get(Key.POSTS_NEW)
|
||||
.get(postID)
|
||||
.then())
|
||||
const selectedPostContent = /** @type {Record<string, any>} */ (await getUser()
|
||||
.get(Key.POSTS_NEW)
|
||||
.get(postID)
|
||||
.get(Key.CONTENT_ITEMS)
|
||||
.then())
|
||||
|
||||
logger.info(orderID, 'LOAD ok')
|
||||
logger.info(selectedPost)
|
||||
if (
|
||||
!selectedPost ||
|
||||
|
|
@ -342,15 +374,15 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
|
|||
breakError = 'ackInfo provided does not correspond to a valid post'
|
||||
break //create the coordinate, but stop because of the invalid post
|
||||
}
|
||||
logger.info('IS POST')
|
||||
logger.info(orderID, 'IS POST')
|
||||
/**
|
||||
* @type {Record<string,string>} <contentID,decryptedRef>
|
||||
*/
|
||||
const contentsToSend = {}
|
||||
logger.info('SECRET OK')
|
||||
logger.info(orderID, 'SECRET OK')
|
||||
let privateFound = false
|
||||
await Common.Utils.asyncForEach(
|
||||
Object.entries(selectedPost.contentItems),
|
||||
Object.entries(selectedPostContent),
|
||||
async ([contentID, item]) => {
|
||||
if (
|
||||
item.type !== 'image/embedded' &&
|
||||
|
|
@ -378,7 +410,7 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
|
|||
type: 'orderAck',
|
||||
response: encrypted
|
||||
}
|
||||
logger.info('RES READY')
|
||||
logger.info(orderID, 'RES READY')
|
||||
|
||||
await new Promise((res, rej) => {
|
||||
getUser()
|
||||
|
|
@ -400,12 +432,12 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
|
|||
}
|
||||
})
|
||||
})
|
||||
logger.info('RES SENT CONTENT')
|
||||
logger.info(orderID, 'RES SENT CONTENT')
|
||||
orderMetadata = JSON.stringify(ackData)
|
||||
break
|
||||
}
|
||||
case 'torrentSeed': {
|
||||
logger.info('TORRENT')
|
||||
logger.info(orderID, 'TORRENT')
|
||||
const numberOfTokens = Number(ackInfo) || 1
|
||||
const seedInfo = selfContentToken()
|
||||
if (!seedInfo && !serviceOrderContentSeedInfo) {
|
||||
|
|
@ -422,7 +454,7 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
|
|||
numberOfTokens,
|
||||
seedInfoReady
|
||||
)
|
||||
logger.info('RES SEED OK')
|
||||
logger.info(orderID, 'RES SEED OK')
|
||||
const ackData = { seedUrl, tokens, ackInfo }
|
||||
const toSend = JSON.stringify(ackData)
|
||||
const encrypted = await SEA.encrypt(toSend, secret)
|
||||
|
|
@ -430,7 +462,7 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
|
|||
type: 'orderAck',
|
||||
response: encrypted
|
||||
}
|
||||
logger.info('RES SEED SENT')
|
||||
logger.info(orderID, 'RES SEED SENT')
|
||||
await new Promise((res, rej) => {
|
||||
getUser()
|
||||
.get(Key.ORDER_TO_RESPONSE)
|
||||
|
|
@ -451,7 +483,7 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
|
|||
}
|
||||
})
|
||||
})
|
||||
logger.info('RES SENT SEED')
|
||||
logger.info(orderID, 'RES SENT SEED')
|
||||
orderMetadata = JSON.stringify(ackData)
|
||||
break
|
||||
}
|
||||
|
|
@ -481,17 +513,18 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
|
|||
throw new Error(breakError)
|
||||
}
|
||||
}
|
||||
logger.info('WAITING INVOICE TO BE PAID')
|
||||
logger.info(orderID, 'Waiting for invoice to be paid for order ' + orderID)
|
||||
new Promise(res => SchemaManager.addListenInvoice(invoice.r_hash, res))
|
||||
.then(invoicePaidCb)
|
||||
.catch(err => {
|
||||
logger.error(
|
||||
orderID,
|
||||
`error inside onOrders, orderAddr: ${addr}, orderID: ${orderID}, order: ${JSON.stringify(
|
||||
order
|
||||
)}`
|
||||
)
|
||||
logger.error(err)
|
||||
logger.info(err)
|
||||
logger.error(orderID, err)
|
||||
logger.info(orderID, err)
|
||||
|
||||
/** @type {import('shock-common').Schema.OrderResponse} */
|
||||
const orderResponse = {
|
||||
|
|
@ -510,19 +543,21 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
|
|||
typeof ack.err !== 'object'
|
||||
) {
|
||||
logger.error(
|
||||
orderID,
|
||||
`Error saving encrypted invoice to order to response usergraph: ${ack}`
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
} catch (err) {
|
||||
} catch (/** @type {any} */ err) {
|
||||
logger.error(
|
||||
orderID,
|
||||
`error inside onOrders, orderAddr: ${addr}, orderID: ${orderID}, order: ${JSON.stringify(
|
||||
order
|
||||
)}`
|
||||
)
|
||||
logger.error(err)
|
||||
logger.info(err)
|
||||
logger.error(orderID, err)
|
||||
logger.info(orderID, err)
|
||||
|
||||
/** @type {import('shock-common').Schema.OrderResponse} */
|
||||
const orderResponse = {
|
||||
|
|
@ -541,6 +576,7 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
|
|||
typeof ack.err !== 'object'
|
||||
) {
|
||||
logger.error(
|
||||
orderID,
|
||||
`Error saving encrypted invoice to order to response usergraph: ${ack}`
|
||||
)
|
||||
}
|
||||
|
|
@ -568,6 +604,11 @@ const onOrders = (user, gun, SEA) => {
|
|||
return
|
||||
}
|
||||
|
||||
if (currentOrderAddr === addr) {
|
||||
// Already subscribed
|
||||
return
|
||||
}
|
||||
|
||||
currentOrderAddr = addr
|
||||
logger.info(`listening to address: ${addr}`)
|
||||
|
||||
|
|
|
|||
|
|
@ -6,11 +6,12 @@ const logger = require('../../../../config/log')
|
|||
const { Constants, Utils: CommonUtils } = require('shock-common')
|
||||
|
||||
const Key = require('../key')
|
||||
/// <reference path="../../../../utils/GunSmith/Smith.ts" />
|
||||
|
||||
/**
|
||||
* @typedef {import('../SimpleGUN').GUNNode} GUNNode
|
||||
* @typedef {Smith.GunSmithNode} GUNNode
|
||||
* @typedef {import('../SimpleGUN').ISEA} ISEA
|
||||
* @typedef {import('../SimpleGUN').UserGUNNode} UserGUNNode
|
||||
* @typedef {Smith.UserSmithNode} UserGUNNode
|
||||
*/
|
||||
|
||||
/**
|
||||
|
|
@ -161,32 +162,13 @@ const tryAndWait = async (promGen, shouldRetry = () => false) => {
|
|||
*/
|
||||
const pubToEpub = async pub => {
|
||||
try {
|
||||
const TIMEOUT_PTR = {}
|
||||
const epub = await require('../../Mediator/index')
|
||||
.getGun()
|
||||
.user(pub)
|
||||
.get('epub')
|
||||
.specialThen()
|
||||
|
||||
const epubOrTimeout = await Promise.race([
|
||||
CommonUtils.makePromise(res => {
|
||||
require('../../Mediator/index')
|
||||
.getGun()
|
||||
.user(pub)
|
||||
.get('epub')
|
||||
.on(data => {
|
||||
if (typeof data === 'string') {
|
||||
res(data)
|
||||
}
|
||||
})
|
||||
}),
|
||||
CommonUtils.makePromise(res => {
|
||||
setTimeout(() => {
|
||||
res(TIMEOUT_PTR)
|
||||
}, 10000)
|
||||
})
|
||||
])
|
||||
|
||||
if (epubOrTimeout === TIMEOUT_PTR) {
|
||||
throw new Error(`Timeout inside pubToEpub()`)
|
||||
}
|
||||
|
||||
return epubOrTimeout
|
||||
return /** @type {string} */ (epub)
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
`Error inside pubToEpub for pub ${pub.slice(0, 8)}...${pub.slice(-8)}:`
|
||||
|
|
@ -232,6 +214,21 @@ const isNodeOnline = async pub => {
|
|||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
const gunID = () => {
|
||||
// Copied from gun internals
|
||||
let s = ''
|
||||
let l = 24 // you are not going to make a 0 length random number, so no need to check type
|
||||
const c = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXZabcdefghijklmnopqrstuvwxyz'
|
||||
while (l > 0) {
|
||||
s += c.charAt(Math.floor(Math.random() * c.length))
|
||||
l--
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
dataHasSoul,
|
||||
delay,
|
||||
|
|
@ -241,5 +238,6 @@ module.exports = {
|
|||
promisifyGunNode: require('./promisifygun'),
|
||||
timeout5,
|
||||
timeout2,
|
||||
isNodeOnline
|
||||
isNodeOnline,
|
||||
gunID
|
||||
}
|
||||
|
|
|
|||
14
services/gunDB/contact-api/utils/index.spec.js
Normal file
14
services/gunDB/contact-api/utils/index.spec.js
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
/**
|
||||
* @format
|
||||
*/
|
||||
const expect = require('expect')
|
||||
|
||||
const { gunID } = require('./index')
|
||||
|
||||
describe('gunID()', () => {
|
||||
it('generates 24-chars-long unique IDs', () => {
|
||||
const id = gunID()
|
||||
expect(id).toBeTruthy()
|
||||
expect(id.length).toBe(24)
|
||||
})
|
||||
})
|
||||
|
|
@ -6,7 +6,6 @@
|
|||
const { makePromise, Constants, Schema } = require('shock-common')
|
||||
const mapValues = require('lodash/mapValues')
|
||||
const Bluebird = require('bluebird')
|
||||
const Gun = require('gun')
|
||||
|
||||
const { pubToEpub } = require('../contact-api/utils')
|
||||
const {
|
||||
|
|
@ -17,6 +16,7 @@ const {
|
|||
$$__SHOCKWALLET__ENCRYPTED__
|
||||
} = require('../Mediator')
|
||||
const logger = require('../../../config/log')
|
||||
const Utils = require('../contact-api/utils')
|
||||
/**
|
||||
* @typedef {import('../contact-api/SimpleGUN').ValidDataValue} ValidDataValue
|
||||
* @typedef {import('./types').ValidRPCDataValue} ValidRPCDataValue
|
||||
|
|
@ -266,8 +266,7 @@ async function set(rawPath, value) {
|
|||
if (Array.isArray(theValue)) {
|
||||
// we'll create a set of sets
|
||||
|
||||
// @ts-expect-error
|
||||
const uuid = Gun.text.random()
|
||||
const uuid = Utils.gunID()
|
||||
|
||||
// here we are simulating the top-most set()
|
||||
const subPath = rawPath + PATH_SEPARATOR + uuid
|
||||
|
|
@ -278,8 +277,7 @@ async function set(rawPath, value) {
|
|||
|
||||
return uuid
|
||||
} else if (Schema.isObj(theValue)) {
|
||||
// @ts-expect-error
|
||||
const uuid = Gun.text.random() // we'll handle UUID ourselves
|
||||
const uuid = Utils.gunID() // we'll handle UUID ourselves
|
||||
|
||||
// so we can use our own put()
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ const {
|
|||
encryptedOn,
|
||||
encryptedCallback
|
||||
} = require('../../../utils/ECC/socket')
|
||||
/// <reference path="../../../utils/GunSmith/Smith.ts" />
|
||||
|
||||
const ALLOWED_GUN_METHODS = [
|
||||
'map',
|
||||
|
|
@ -51,7 +52,7 @@ const getNode = root => {
|
|||
}
|
||||
|
||||
/**
|
||||
* @param {import("../contact-api/SimpleGUN").GUNNode} node
|
||||
* @param {Smith.GunSmithNode} node
|
||||
* @param {string} path
|
||||
*/
|
||||
const getGunQuery = (node, path) => {
|
||||
|
|
|
|||
149
src/routes.js
149
src/routes.js
|
|
@ -16,6 +16,7 @@ const Big = require('big.js')
|
|||
const size = require('lodash/size')
|
||||
const { range, flatten, evolve } = require('ramda')
|
||||
const path = require('path')
|
||||
const cors = require('cors')
|
||||
|
||||
const getListPage = require('../utils/paginate')
|
||||
const auth = require('../services/auth/auth')
|
||||
|
|
@ -47,13 +48,18 @@ module.exports = async (
|
|||
app,
|
||||
config,
|
||||
mySocketsEvents,
|
||||
{ serverPort, CA, CA_KEY, usetls }
|
||||
{ serverPort, CA, CA_KEY, useTLS }
|
||||
) => {
|
||||
try {
|
||||
const Http = Axios.create({
|
||||
httpsAgent: new httpsAgent.Agent({
|
||||
ca: await FS.readFile(CA)
|
||||
})
|
||||
httpsAgent: new httpsAgent.Agent(
|
||||
CA && CA_KEY
|
||||
? {
|
||||
ca: await FS.readFile(CA),
|
||||
key: await FS.readFile(CA_KEY)
|
||||
}
|
||||
: {}
|
||||
)
|
||||
})
|
||||
|
||||
const sanitizeLNDError = (message = '') => {
|
||||
|
|
@ -85,7 +91,7 @@ module.exports = async (
|
|||
|
||||
try {
|
||||
const APIHealth = await Http.get(
|
||||
`${usetls ? 'https' : 'http'}://localhost:${serverPort}/ping`
|
||||
`${useTLS ? 'https' : 'http'}://localhost:${serverPort}/ping`
|
||||
)
|
||||
const APIStatus = {
|
||||
message: APIHealth.data,
|
||||
|
|
@ -100,8 +106,8 @@ module.exports = async (
|
|||
} catch (err) {
|
||||
logger.error(err)
|
||||
const APIStatus = {
|
||||
message: err.response.data,
|
||||
responseTime: err.response.headers['x-response-time'],
|
||||
message: err?.response?.data,
|
||||
responseTime: err?.response?.headers['x-response-time'],
|
||||
success: false
|
||||
}
|
||||
logger.warn('Failed to retrieve API status', APIStatus)
|
||||
|
|
@ -209,6 +215,13 @@ module.exports = async (
|
|||
}
|
||||
}
|
||||
|
||||
app.use(
|
||||
cors({
|
||||
credentials: true,
|
||||
origin: '*'
|
||||
})
|
||||
)
|
||||
|
||||
app.use((req, res, next) => {
|
||||
res.setHeader('x-session-id', SESSION_ID)
|
||||
next()
|
||||
|
|
@ -2268,7 +2281,7 @@ module.exports = async (
|
|||
|
||||
/**
|
||||
* @typedef {object} HandleGunFetchParams
|
||||
* @prop {'once'|'load'} type
|
||||
* @prop {'once'|'load'|'specialOnce'} type
|
||||
* @prop {boolean} startFromUserGraph
|
||||
* @prop {string} path
|
||||
* @prop {string=} publicKey
|
||||
|
|
@ -2288,48 +2301,36 @@ module.exports = async (
|
|||
epubForDecryption
|
||||
}) => {
|
||||
const keys = path.split('>')
|
||||
const { tryAndWait } = require('../services/gunDB/contact-api/utils')
|
||||
return tryAndWait((gun, user) => {
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
let node = startFromUserGraph
|
||||
? user
|
||||
: publicKey
|
||||
? gun.user(publicKey)
|
||||
: gun
|
||||
logger.info(`fetching: ${keys}`)
|
||||
keys.forEach(key => (node = node.get(key)))
|
||||
const { gun, user } = require('../services/gunDB/Mediator')
|
||||
|
||||
if (!publicKeyForDecryption || !epubForDecryption) {
|
||||
logger.warn('[GUN] Missing public key for decryption!', {
|
||||
publicKeyForDecryption,
|
||||
epubForDecryption
|
||||
})
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
let node = startFromUserGraph
|
||||
? user
|
||||
: publicKey
|
||||
? gun.user(publicKey)
|
||||
: gun
|
||||
keys.forEach(key => (node = node.get(key)))
|
||||
logger.info(`fetching: ${keys}`)
|
||||
return new Promise((res, rej) => {
|
||||
const listener = data => {
|
||||
logger.info(`got res for: ${keys}`)
|
||||
logger.info(data || 'falsey data (does not get logged)')
|
||||
if (publicKeyForDecryption) {
|
||||
GunWriteRPC.deepDecryptIfNeeded(
|
||||
data,
|
||||
publicKeyForDecryption,
|
||||
epubForDecryption
|
||||
)
|
||||
.then(res)
|
||||
.catch(rej)
|
||||
} else {
|
||||
res(data)
|
||||
}
|
||||
}
|
||||
|
||||
return new Promise((res, rej) => {
|
||||
try {
|
||||
const listener = data => {
|
||||
logger.info(`got res for: ${keys}`)
|
||||
logger.info(data || 'falsey data (does not get logged)')
|
||||
if (publicKeyForDecryption) {
|
||||
GunWriteRPC.deepDecryptIfNeeded(
|
||||
data,
|
||||
publicKeyForDecryption,
|
||||
epubForDecryption
|
||||
)
|
||||
.then(res)
|
||||
.catch(rej)
|
||||
} else {
|
||||
res(data)
|
||||
}
|
||||
}
|
||||
|
||||
if (type === 'once') node.once(listener)
|
||||
if (type === 'load') node.load(listener)
|
||||
} catch (err) {
|
||||
logger.error('Gun Fetch Error:', err)
|
||||
}
|
||||
})
|
||||
if (type === 'once') node.once(listener)
|
||||
if (type === 'load') node.load(listener)
|
||||
if (type === 'specialOnce') node.specialOnce(listener)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -2366,6 +2367,30 @@ module.exports = async (
|
|||
}
|
||||
})
|
||||
|
||||
ap.get('/api/gun/specialOnce/:path', async (req, res) => {
|
||||
try {
|
||||
const publicKeyForDecryption = req.header(PUBKEY_FOR_DECRYPT_HEADER)
|
||||
const epubForDecryption = req.header(EPUB_FOR_DECRYPT_HEADER)
|
||||
const { path } = req.params
|
||||
logger.info(`Gun special once: ${path}`)
|
||||
const data = await handleGunFetch({
|
||||
path,
|
||||
startFromUserGraph: false,
|
||||
type: 'specialOnce',
|
||||
publicKeyForDecryption,
|
||||
epubForDecryption
|
||||
})
|
||||
res.status(200).json({
|
||||
data
|
||||
})
|
||||
} catch (e) {
|
||||
logger.error(e)
|
||||
res.status(500).json({
|
||||
errorMessage: e.message
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
ap.get('/api/gun/load/:path', async (req, res) => {
|
||||
try {
|
||||
const publicKeyForDecryption = req.header(PUBKEY_FOR_DECRYPT_HEADER)
|
||||
|
|
@ -2414,6 +2439,30 @@ module.exports = async (
|
|||
}
|
||||
})
|
||||
|
||||
ap.get('/api/gun/user/specialOnce/:path', async (req, res) => {
|
||||
try {
|
||||
const publicKeyForDecryption = req.header(PUBKEY_FOR_DECRYPT_HEADER)
|
||||
const epubForDecryption = req.header(EPUB_FOR_DECRYPT_HEADER)
|
||||
const { path } = req.params
|
||||
logger.info(`Gun user special once: ${path}`)
|
||||
const data = await handleGunFetch({
|
||||
path,
|
||||
startFromUserGraph: true,
|
||||
type: 'specialOnce',
|
||||
publicKeyForDecryption,
|
||||
epubForDecryption
|
||||
})
|
||||
res.status(200).json({
|
||||
data
|
||||
})
|
||||
} catch (e) {
|
||||
logger.error(e)
|
||||
res.status(500).json({
|
||||
errorMessage: e.message
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
ap.get('/api/gun/user/load/:path', async (req, res) => {
|
||||
try {
|
||||
const publicKeyForDecryption = req.header(PUBKEY_FOR_DECRYPT_HEADER)
|
||||
|
|
@ -2440,11 +2489,11 @@ module.exports = async (
|
|||
|
||||
ap.get('/api/gun/otheruser/:publicKey/:type/:path', async (req, res) => {
|
||||
try {
|
||||
const allowedTypes = ['once', 'load', 'open']
|
||||
const allowedTypes = ['once', 'load', 'open', 'specialOnce']
|
||||
const publicKeyForDecryption = req.header(PUBKEY_FOR_DECRYPT_HEADER)
|
||||
const epubForDecryption = req.header(EPUB_FOR_DECRYPT_HEADER)
|
||||
const { path /*:rawPath*/, publicKey, type } = req.params
|
||||
logger.info(`gun otheruser ${type}: ${path}`)
|
||||
logger.info(`Gun other user ${type}: ${path}`)
|
||||
// const path = decodeURI(rawPath)
|
||||
if (!publicKey || publicKey === 'undefined') {
|
||||
res.status(400).json({
|
||||
|
|
@ -2543,7 +2592,7 @@ module.exports = async (
|
|||
ap.post('/api/gun/set', async (req, res) => {
|
||||
try {
|
||||
const { path, value } = req.body
|
||||
logger.info(`gun PUT: ${path}`)
|
||||
logger.info(`gun SET: ${path}`)
|
||||
const id = await GunWriteRPC.set(path, value)
|
||||
|
||||
res.status(200).json({
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ process.on('uncaughtException', e => {
|
|||
*/
|
||||
const server = program => {
|
||||
const Http = require('http')
|
||||
const Https = require('https')
|
||||
const FS = require('fs')
|
||||
const Express = require('express')
|
||||
const Crypto = require('crypto')
|
||||
const Dotenv = require('dotenv')
|
||||
|
|
@ -162,6 +164,16 @@ const server = program => {
|
|||
}
|
||||
|
||||
if (!authorized || process.env.SHOCK_ENCRYPTION_ECC === 'false') {
|
||||
if (!authorized) {
|
||||
logger.warn(
|
||||
`An unauthorized Device ID is contacting the API: ${deviceId}`
|
||||
)
|
||||
logger.warn(
|
||||
`Authorized Device IDs: ${[...ECC.devicePublicKeys.keys()].join(
|
||||
', '
|
||||
)}`
|
||||
)
|
||||
}
|
||||
args[0] = JSON.stringify(args[0])
|
||||
oldSend.apply(res, args)
|
||||
}
|
||||
|
|
@ -294,20 +306,19 @@ const server = program => {
|
|||
res.status(500).send({ status: 500, errorMessage: 'internal error' })
|
||||
})
|
||||
|
||||
const CA = LightningServices.servicesConfig.lndCertPath
|
||||
const CA_KEY = CA.replace('cert', 'key')
|
||||
const CA = program.httpsCert
|
||||
const CA_KEY = program.httpsCertKey
|
||||
|
||||
const createServer = () => {
|
||||
try {
|
||||
// if (LightningServices.servicesConfig.lndCertPath && program.usetls) {
|
||||
// const [key, cert] = await Promise.all([
|
||||
// FS.readFile(CA_KEY),
|
||||
// FS.readFile(CA)
|
||||
// ])
|
||||
// const httpsServer = Https.createServer({ key, cert }, app)
|
||||
if (program.useTLS) {
|
||||
const key = FS.readFileSync(CA_KEY, 'utf-8')
|
||||
const cert = FS.readFileSync(CA, 'utf-8')
|
||||
|
||||
// return httpsServer
|
||||
// }
|
||||
const httpsServer = Https.createServer({ key, cert }, app)
|
||||
|
||||
return httpsServer
|
||||
}
|
||||
|
||||
const httpServer = Http.Server(app)
|
||||
return httpServer
|
||||
|
|
@ -357,7 +368,7 @@ const server = program => {
|
|||
{
|
||||
serverHost,
|
||||
serverPort,
|
||||
usetls: program.usetls,
|
||||
useTLS: program.useTLS,
|
||||
CA,
|
||||
CA_KEY
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
// "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'. */
|
||||
"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 */
|
||||
|
|
|
|||
|
|
@ -188,5 +188,7 @@ module.exports = {
|
|||
encryptMessage,
|
||||
decryptMessage,
|
||||
authorizeDevice,
|
||||
generateRandomString
|
||||
generateRandomString,
|
||||
nodeKeyPairs,
|
||||
devicePublicKeys
|
||||
}
|
||||
|
|
|
|||
760
utils/GunSmith/GunSmith.js
Normal file
760
utils/GunSmith/GunSmith.js
Normal file
|
|
@ -0,0 +1,760 @@
|
|||
/**
|
||||
* @format
|
||||
*/
|
||||
/* eslint-disable no-use-before-define */
|
||||
/* eslint-disable func-style */
|
||||
// @ts-check
|
||||
/// <reference path="Smith.ts" />
|
||||
/// <reference path="GunT.ts" />
|
||||
const uuid = require('uuid/v1')
|
||||
const mapValues = require('lodash/mapValues')
|
||||
const { fork } = require('child_process')
|
||||
|
||||
const logger = require('../../config/log')
|
||||
|
||||
const { mergePuts, isPopulated } = require('./misc')
|
||||
|
||||
const gunUUID = () => {
|
||||
// Copied from gun internals
|
||||
let s = ''
|
||||
let l = 24 // you are not going to make a 0 length random number, so no need to check type
|
||||
const c = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXZabcdefghijklmnopqrstuvwxyz'
|
||||
while (l > 0) {
|
||||
s += c.charAt(Math.floor(Math.random() * c.length))
|
||||
l--
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a path to `on()` listeners
|
||||
* @type {Record<string, Set<GunT.Listener>|undefined>}
|
||||
*/
|
||||
const pathToListeners = {}
|
||||
|
||||
/**
|
||||
* Maps a path to `map().on()` listeners
|
||||
* @type {Record<string, Set<GunT.Listener>|undefined>}
|
||||
*/
|
||||
const pathToMapListeners = {}
|
||||
|
||||
/** @type {Record<string, GunT.LoadListener>} */
|
||||
const idToLoadListener = {}
|
||||
|
||||
/**
|
||||
* Path to pending puts. Oldest to newest
|
||||
* @type {Record<string, Smith.PendingPut[]>}
|
||||
*/
|
||||
const pendingPuts = {}
|
||||
|
||||
/**
|
||||
* @param {Smith.GunMsg} msg
|
||||
*/
|
||||
const handleMsg = msg => {
|
||||
if (msg.type === 'load') {
|
||||
const { data, id, key } = msg
|
||||
|
||||
const listener = idToLoadListener[id]
|
||||
|
||||
if (listener) {
|
||||
listener(data, key)
|
||||
delete idToLoadListener[id]
|
||||
}
|
||||
}
|
||||
if (msg.type === 'on') {
|
||||
const { data, path } = msg
|
||||
|
||||
// eslint-disable-next-line no-multi-assign
|
||||
const listeners =
|
||||
pathToListeners[path] || (pathToListeners[path] = new Set())
|
||||
|
||||
for (const l of listeners) {
|
||||
l(data, path.split('>')[path.split('>').length - 1])
|
||||
}
|
||||
}
|
||||
if (msg.type === 'map.on') {
|
||||
const { data, key, path } = msg
|
||||
|
||||
// eslint-disable-next-line no-multi-assign
|
||||
const listeners =
|
||||
pathToMapListeners[path] || (pathToMapListeners[path] = new Set())
|
||||
|
||||
for (const l of listeners) {
|
||||
l(data, key)
|
||||
}
|
||||
}
|
||||
if (msg.type === 'put') {
|
||||
const { ack, id, path } = msg
|
||||
|
||||
const pendingPutsForPath = pendingPuts[path] || (pendingPuts[path] = [])
|
||||
|
||||
const pendingPut = pendingPutsForPath.find(pp => pp.id === id)
|
||||
const idx = pendingPutsForPath.findIndex(pp => pp.id === id)
|
||||
|
||||
if (pendingPut) {
|
||||
pendingPutsForPath.splice(idx, 1)
|
||||
|
||||
if (pendingPut.cb) {
|
||||
pendingPut.cb(ack)
|
||||
}
|
||||
} else {
|
||||
logger.error(
|
||||
`Could not find request for put message from gun subprocess. Data will be logged below.`
|
||||
)
|
||||
console.log({
|
||||
msg,
|
||||
pendingPut: pendingPut || 'No pending put found',
|
||||
allPendingPuts: pendingPuts
|
||||
})
|
||||
}
|
||||
}
|
||||
if (msg.type === 'multiPut') {
|
||||
const { ack, ids, path } = msg
|
||||
|
||||
const pendingPutsForPath = pendingPuts[path] || (pendingPuts[path] = [])
|
||||
|
||||
const ackedPuts = pendingPutsForPath.filter(pp => ids.includes(pp.id))
|
||||
|
||||
pendingPuts[path] = pendingPuts[path].filter(pp => !ids.includes(pp.id))
|
||||
|
||||
ackedPuts.forEach(pp => {
|
||||
if (pp.cb) {
|
||||
pp.cb(ack)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {ReturnType<typeof fork>} */
|
||||
// eslint-disable-next-line init-declarations
|
||||
let currentGun
|
||||
|
||||
let lastAlias = ''
|
||||
let lastPass = ''
|
||||
/** @type {GunT.UserPair|null} */
|
||||
let lastPair = null
|
||||
/** @type {import('gun/types/options').IGunConstructorOptions} */
|
||||
let lastOpts = {}
|
||||
let isAuthing = false
|
||||
|
||||
/**
|
||||
* @param {string} alias
|
||||
* @param {string} pass
|
||||
* @returns {Promise<GunT.UserPair>}
|
||||
*/
|
||||
const auth = (alias, pass) => {
|
||||
logger.info(`Authing with ${alias}`)
|
||||
if (isAuthing) {
|
||||
throw new Error(`Double auth?`)
|
||||
}
|
||||
isAuthing = true
|
||||
return new Promise((res, rej) => {
|
||||
/** @type {Smith.SmithMsgAuth} */
|
||||
const msg = {
|
||||
alias,
|
||||
pass,
|
||||
type: 'auth'
|
||||
}
|
||||
|
||||
/** @param {Smith.GunMsg} msg */
|
||||
const _cb = msg => {
|
||||
if (msg.type === 'auth') {
|
||||
logger.info(`Received ${msg.ack.sea ? 'ok' : 'bad'} auth reply.`)
|
||||
currentGun.off('message', _cb)
|
||||
|
||||
isAuthing = false
|
||||
|
||||
const { ack } = msg
|
||||
|
||||
if (ack.err) {
|
||||
lastAlias = ''
|
||||
lastPass = ''
|
||||
lastPair = null
|
||||
logger.info('Auth unsuccessful, cached credentials cleared.')
|
||||
rej(new Error(ack.err))
|
||||
} else if (ack.sea) {
|
||||
lastAlias = alias
|
||||
lastPass = pass
|
||||
lastPair = ack.sea
|
||||
logger.info('Auth successful, credentials cached.')
|
||||
res(ack.sea)
|
||||
} else {
|
||||
lastAlias = ''
|
||||
lastPass = ''
|
||||
lastPair = null
|
||||
logger.info('Auth unsuccessful, cached credentials cleared.')
|
||||
rej(new Error('Auth: ack.sea undefined'))
|
||||
}
|
||||
}
|
||||
}
|
||||
currentGun.on('message', _cb)
|
||||
currentGun.send(msg)
|
||||
logger.info('Sent auth message.')
|
||||
})
|
||||
}
|
||||
|
||||
const autoAuth = async () => {
|
||||
if (!lastAlias || !lastPass) {
|
||||
logger.info('No credentials cached, will not auto-auth')
|
||||
return
|
||||
}
|
||||
logger.info('Credentials cached, will auth.')
|
||||
await auth(lastAlias, lastPass)
|
||||
}
|
||||
|
||||
const flushPendingPuts = () => {
|
||||
if (isAuthing || isForging) {
|
||||
throw new Error('Tried to flush pending puts while authing or forging.')
|
||||
}
|
||||
const ids = mapValues(pendingPuts, pendingPutsForPath =>
|
||||
pendingPutsForPath.map(pp => pp.id)
|
||||
)
|
||||
const writes = mapValues(pendingPuts, pendingPutsForPath =>
|
||||
pendingPutsForPath.map(pp => pp.data)
|
||||
)
|
||||
const finalWrites = mapValues(writes, writesForPath =>
|
||||
mergePuts(writesForPath)
|
||||
)
|
||||
const messages = Object.entries(ids).map(([path, ids]) => {
|
||||
/** @type {Smith.SmithMsgMultiPut} */
|
||||
const msg = {
|
||||
data: finalWrites[path],
|
||||
ids,
|
||||
path,
|
||||
type: 'multiPut'
|
||||
}
|
||||
return msg
|
||||
})
|
||||
currentGun.send(messages)
|
||||
logger.info(`Sent ${messages.length} pending puts.`)
|
||||
}
|
||||
|
||||
let isForging = false
|
||||
|
||||
/** @returns {Promise<void>} */
|
||||
const isReady = () =>
|
||||
new Promise(res => {
|
||||
if (isForging || isAuthing) {
|
||||
setTimeout(() => {
|
||||
isReady().then(res)
|
||||
}, 1000)
|
||||
} else {
|
||||
res()
|
||||
}
|
||||
})
|
||||
|
||||
let procCounter = 0
|
||||
|
||||
let killed = false
|
||||
|
||||
const forge = () => {
|
||||
;(async () => {
|
||||
if (killed) {
|
||||
throw new Error('Tried to forge after killing GunSmith')
|
||||
}
|
||||
logger.info(`Forging Gun # ${++procCounter}`)
|
||||
if (isForging) {
|
||||
throw new Error('Double forge?')
|
||||
}
|
||||
|
||||
/** Used only for logs. */
|
||||
const isReforge = !!currentGun
|
||||
|
||||
logger.info(isReforge ? 'Will reforge' : 'Will forge')
|
||||
|
||||
isForging = true
|
||||
if (currentGun) {
|
||||
currentGun.off('message', handleMsg)
|
||||
currentGun.disconnect()
|
||||
currentGun.kill()
|
||||
logger.info('Destroyed current gun')
|
||||
}
|
||||
const newGun = fork('utils/GunSmith/gun.js')
|
||||
currentGun = newGun
|
||||
logger.info('Forged new gun')
|
||||
|
||||
// currentGun.on('', e => {
|
||||
// logger.info('event from subprocess')
|
||||
// logger.info(e)
|
||||
// })
|
||||
|
||||
currentGun.on('message', handleMsg)
|
||||
|
||||
/** @type {Smith.SmithMsgInit} */
|
||||
const initMsg = {
|
||||
opts: lastOpts,
|
||||
type: 'init'
|
||||
}
|
||||
await new Promise(res => {
|
||||
currentGun.on('message', msg => {
|
||||
if (msg.type === 'init') {
|
||||
// @ts-ignore
|
||||
res()
|
||||
}
|
||||
})
|
||||
currentGun.send(initMsg)
|
||||
logger.info('Sent init msg')
|
||||
})
|
||||
|
||||
logger.info('Received init reply')
|
||||
|
||||
const lastGunListeners = Object.keys(pathToListeners).map(path => {
|
||||
/** @type {Smith.SmithMsgOn} */
|
||||
const msg = {
|
||||
path,
|
||||
type: 'on'
|
||||
}
|
||||
return msg
|
||||
})
|
||||
|
||||
if (lastGunListeners.length) {
|
||||
currentGun.send(lastGunListeners)
|
||||
|
||||
logger.info(`Sent ${lastGunListeners.length} pending on() listeners`)
|
||||
}
|
||||
|
||||
const lastGunMapListeners = Object.keys(pathToMapListeners).map(path => {
|
||||
/** @type {Smith.SmithMsgMapOn} */
|
||||
const msg = {
|
||||
path,
|
||||
type: 'map.on'
|
||||
}
|
||||
return msg
|
||||
})
|
||||
|
||||
if (lastGunMapListeners.length) {
|
||||
currentGun.send(lastGunMapListeners)
|
||||
|
||||
logger.info(
|
||||
`Sent ${lastGunMapListeners.length} pending map().on() listeners`
|
||||
)
|
||||
}
|
||||
|
||||
logger.info(
|
||||
isReforge
|
||||
? 'Finished reforging, will now auto-auth'
|
||||
: 'Finished forging, will now auto-auth'
|
||||
)
|
||||
|
||||
await autoAuth()
|
||||
|
||||
// Eslint disable: This should be caught by a if (isForging) {throw} at the
|
||||
// beginning of this function
|
||||
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
isForging = false
|
||||
flushPendingPuts()
|
||||
})()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} path
|
||||
* @param {boolean=} afterMap
|
||||
* @returns {Smith.GunSmithNode}
|
||||
*/
|
||||
function createReplica(path, afterMap = false) {
|
||||
/** @type {(GunT.Listener|GunT.LoadListener)[]} */
|
||||
const listenersForThisRef = []
|
||||
|
||||
return {
|
||||
_: {
|
||||
get get() {
|
||||
const keys = path.split('>')
|
||||
return keys[keys.length - 1]
|
||||
},
|
||||
opt: {
|
||||
// TODO
|
||||
peers: {}
|
||||
},
|
||||
put: {
|
||||
// TODO
|
||||
}
|
||||
},
|
||||
back() {
|
||||
throw new Error('Do not use back() on a GunSmith node.')
|
||||
},
|
||||
get(key) {
|
||||
if (afterMap) {
|
||||
throw new Error(
|
||||
'Cannot call get() after map() on a GunSmith node, you should only call on() after map()'
|
||||
)
|
||||
}
|
||||
return createReplica(path + '>' + key)
|
||||
},
|
||||
map() {
|
||||
if (afterMap) {
|
||||
throw new Error('Cannot call map() after map() on a GunSmith node')
|
||||
}
|
||||
return createReplica(path, true)
|
||||
},
|
||||
off() {
|
||||
for (const l of listenersForThisRef) {
|
||||
// eslint-disable-next-line no-multi-assign
|
||||
const listeners =
|
||||
pathToListeners[path] || (pathToListeners[path] = new Set())
|
||||
|
||||
// eslint-disable-next-line no-multi-assign
|
||||
const mapListeners =
|
||||
pathToMapListeners[path] || (pathToMapListeners[path] = new Set())
|
||||
|
||||
// @ts-expect-error
|
||||
listeners.delete(l)
|
||||
// @ts-expect-error
|
||||
mapListeners.delete(l)
|
||||
}
|
||||
},
|
||||
on(cb) {
|
||||
listenersForThisRef.push(cb)
|
||||
|
||||
if (afterMap) {
|
||||
// eslint-disable-next-line no-multi-assign
|
||||
const listeners =
|
||||
pathToMapListeners[path] || (pathToMapListeners[path] = new Set())
|
||||
|
||||
listeners.add(cb)
|
||||
|
||||
/** @type {Smith.SmithMsgMapOn} */
|
||||
const msg = {
|
||||
path,
|
||||
type: 'map.on'
|
||||
}
|
||||
isReady().then(() => {
|
||||
currentGun.send(msg)
|
||||
})
|
||||
} else {
|
||||
// eslint-disable-next-line no-multi-assign
|
||||
const listeners =
|
||||
pathToListeners[path] || (pathToListeners[path] = new Set())
|
||||
|
||||
listeners.add(cb)
|
||||
|
||||
/** @type {Smith.SmithMsgOn} */
|
||||
const msg = {
|
||||
path,
|
||||
type: 'on'
|
||||
}
|
||||
isReady().then(() => {
|
||||
currentGun.send(msg)
|
||||
})
|
||||
}
|
||||
|
||||
return this
|
||||
},
|
||||
once(cb, opts = { wait: 500 }) {
|
||||
if (afterMap) {
|
||||
throw new Error('Cannot call once() after map() on a GunSmith node')
|
||||
}
|
||||
// We could use this.on() but then we couldn't call .off()
|
||||
const tmp = createReplica(path, afterMap)
|
||||
|
||||
/** @type {GunT.ListenerData} */
|
||||
let lastVal = null
|
||||
|
||||
tmp.on(data => {
|
||||
lastVal = data
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
tmp.off()
|
||||
const keys = path.split('>')
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
cb && cb(lastVal, keys[keys.length - 1])
|
||||
}, opts.wait)
|
||||
|
||||
return this
|
||||
},
|
||||
put(data, cb) {
|
||||
const id = uuid()
|
||||
|
||||
const pendingPutsForPath = pendingPuts[path] || (pendingPuts[path] = [])
|
||||
|
||||
/** @type {Smith.PendingPut} */
|
||||
const pendingPut = {
|
||||
cb: cb || (() => {}),
|
||||
data,
|
||||
id
|
||||
}
|
||||
|
||||
pendingPutsForPath.push(pendingPut)
|
||||
|
||||
/** @type {Smith.SmithMsgPut} */
|
||||
const msg = {
|
||||
data,
|
||||
id,
|
||||
path,
|
||||
type: 'put'
|
||||
}
|
||||
isReady().then(() => {
|
||||
currentGun.send(msg)
|
||||
})
|
||||
return this
|
||||
},
|
||||
set(data, cb) {
|
||||
if (afterMap) {
|
||||
throw new Error('Cannot call set() after map() on a GunSmith node')
|
||||
}
|
||||
|
||||
const id = gunUUID()
|
||||
this.put(
|
||||
{
|
||||
[id]: data
|
||||
},
|
||||
ack => {
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
cb && cb(ack)
|
||||
}
|
||||
)
|
||||
return this.get(id)
|
||||
},
|
||||
user(pub) {
|
||||
if (path !== '$root') {
|
||||
throw new ReferenceError(
|
||||
`Do not call user() on a non-root GunSmith node`
|
||||
)
|
||||
}
|
||||
if (!pub) {
|
||||
return createUserReplica()
|
||||
}
|
||||
const replica = createReplica(pub)
|
||||
// I don't know why Typescript insists on returning a UserGUNNode so here we go:
|
||||
return {
|
||||
...replica,
|
||||
/** @returns {GunT.UserSoul} */
|
||||
get _() {
|
||||
throw new ReferenceError(
|
||||
`Do not access _ on another user's graph (${pub.slice(
|
||||
0,
|
||||
8
|
||||
)}...${pub.slice(-8)})`
|
||||
)
|
||||
},
|
||||
auth() {
|
||||
throw new Error(
|
||||
"Do not call auth() on another user's graph (gun.user(otherUserPub))"
|
||||
)
|
||||
},
|
||||
create() {
|
||||
throw new Error(
|
||||
"Do not call create() on another user's graph (gun.user(otherUserPub))"
|
||||
)
|
||||
},
|
||||
leave() {
|
||||
throw new Error(
|
||||
"Do not call leave() on another user's graph (gun.user(otherUserPub))"
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
then() {
|
||||
return new Promise(res => {
|
||||
this.once(data => {
|
||||
res(data)
|
||||
})
|
||||
})
|
||||
},
|
||||
specialOn(cb) {
|
||||
let canaryPeep = false
|
||||
|
||||
const checkCanary = () =>
|
||||
setTimeout(() => {
|
||||
if (!canaryPeep) {
|
||||
isReady()
|
||||
.then(forge)
|
||||
.then(isReady)
|
||||
.then(checkCanary)
|
||||
}
|
||||
}, 30000)
|
||||
|
||||
checkCanary()
|
||||
return this.on((data, key) => {
|
||||
canaryPeep = true
|
||||
cb(data, key)
|
||||
})
|
||||
},
|
||||
specialOnce(cb, _wait = 1000) {
|
||||
this.once(
|
||||
(data, key) => {
|
||||
if (isPopulated(data) || _wait > 100000) {
|
||||
cb(data, key)
|
||||
} else {
|
||||
isReady()
|
||||
.then(forge)
|
||||
.then(isReady)
|
||||
.then(() => {
|
||||
this.specialOnce(cb, _wait * 3)
|
||||
})
|
||||
}
|
||||
},
|
||||
{ wait: _wait }
|
||||
)
|
||||
return this
|
||||
},
|
||||
specialThen() {
|
||||
return new Promise((res, rej) => {
|
||||
this.specialOnce(data => {
|
||||
if (isPopulated(data)) {
|
||||
res(data)
|
||||
} else {
|
||||
rej(new Error(`Could not fetch data at path ${path}`))
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
pPut(data) {
|
||||
return new Promise((res, rej) => {
|
||||
this.put(data, ack => {
|
||||
if (ack.err) {
|
||||
rej(new Error(ack.err))
|
||||
} else {
|
||||
res()
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
pSet(data) {
|
||||
return new Promise((res, rej) => {
|
||||
this.set(data, ack => {
|
||||
if (ack.err) {
|
||||
rej(new Error(ack.err))
|
||||
} else {
|
||||
res()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let userReplicaCalled = false
|
||||
|
||||
/**
|
||||
* @returns {Smith.UserSmithNode}
|
||||
*/
|
||||
function createUserReplica() {
|
||||
if (userReplicaCalled) {
|
||||
throw new Error('Please only call gun.user() (without a pub) once.')
|
||||
}
|
||||
userReplicaCalled = true
|
||||
|
||||
const baseReplica = createReplica('$user')
|
||||
|
||||
/** @type {Smith.UserSmithNode} */
|
||||
const completeReplica = {
|
||||
...baseReplica,
|
||||
get _() {
|
||||
return {
|
||||
...baseReplica._,
|
||||
// TODO
|
||||
sea: lastPair || {
|
||||
epriv: '',
|
||||
epub: '',
|
||||
priv: '',
|
||||
pub: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
get is() {
|
||||
if (lastAlias && lastPair) {
|
||||
return {
|
||||
alias: lastAlias,
|
||||
pub: lastPair.pub
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
auth(alias, pass, cb) {
|
||||
auth(alias, pass)
|
||||
.then(pair => {
|
||||
cb({
|
||||
err: undefined,
|
||||
sea: pair
|
||||
})
|
||||
})
|
||||
.catch(e => {
|
||||
cb({
|
||||
err: e.message,
|
||||
sea: undefined
|
||||
})
|
||||
})
|
||||
},
|
||||
create(alias, pass, cb) {
|
||||
lastAlias = ''
|
||||
lastPass = ''
|
||||
lastPair = null
|
||||
|
||||
/** @type {Smith.SmithMsgCreate} */
|
||||
const msg = {
|
||||
alias,
|
||||
pass,
|
||||
type: 'create'
|
||||
}
|
||||
|
||||
/** @param {Smith.GunMsg} msg */
|
||||
const _cb = msg => {
|
||||
if (msg.type === 'create') {
|
||||
currentGun.off('message', _cb)
|
||||
|
||||
const { ack } = msg
|
||||
|
||||
if (ack.err) {
|
||||
cb(ack)
|
||||
} else if (ack.pub) {
|
||||
lastAlias = alias
|
||||
lastPass = pass
|
||||
lastPair = msg.pair
|
||||
cb(ack)
|
||||
} else {
|
||||
throw (new Error('Auth: ack.pub undefined'))
|
||||
}
|
||||
}
|
||||
}
|
||||
currentGun.on('message', _cb)
|
||||
currentGun.send(msg)
|
||||
},
|
||||
leave() {
|
||||
lastAlias = ''
|
||||
lastPass = ''
|
||||
lastPair = null
|
||||
|
||||
/** @type {Smith.SmithMsgLeave} */
|
||||
const msg = {
|
||||
type: 'leave'
|
||||
}
|
||||
currentGun.send(msg)
|
||||
}
|
||||
}
|
||||
|
||||
return completeReplica
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('gun/types/options').IGunConstructorOptions} opts
|
||||
* @returns {Smith.GunSmithNode}
|
||||
*/
|
||||
const Gun = opts => {
|
||||
lastOpts = opts
|
||||
forge()
|
||||
|
||||
return createReplica('$root')
|
||||
}
|
||||
|
||||
module.exports = Gun
|
||||
|
||||
module.exports.kill = () => {
|
||||
if (currentGun) {
|
||||
currentGun.send('bye')
|
||||
currentGun.off('message', handleMsg)
|
||||
currentGun.disconnect()
|
||||
currentGun.kill()
|
||||
// @ts-ignore
|
||||
currentGun = null
|
||||
killed = true
|
||||
logger.info('Killed gunsmith.')
|
||||
}
|
||||
}
|
||||
|
||||
module.exports._reforge = forge
|
||||
module.exports._isReady = isReady
|
||||
module.exports._getProcCounter = () => {
|
||||
return procCounter
|
||||
}
|
||||
411
utils/GunSmith/GunSmith.spec.js
Normal file
411
utils/GunSmith/GunSmith.spec.js
Normal file
|
|
@ -0,0 +1,411 @@
|
|||
/**
|
||||
* @format
|
||||
*/
|
||||
// @ts-check
|
||||
const Gun = require('./GunSmith')
|
||||
const words = require('random-words')
|
||||
const fs = require('fs')
|
||||
const debounce = require('lodash/debounce')
|
||||
const once = require('lodash/once')
|
||||
const expect = require('expect')
|
||||
|
||||
const logger = require('../../config/log')
|
||||
|
||||
const { removeBuiltInGunProps } = require('./misc')
|
||||
|
||||
if (!fs.existsSync('./test-radata')) {
|
||||
fs.mkdirSync('./test-radata')
|
||||
}
|
||||
|
||||
const instance = Gun({
|
||||
axe: false,
|
||||
multicast: false,
|
||||
file: './test-radata/' + words({ exactly: 2 }).join('-')
|
||||
})
|
||||
|
||||
const user = instance.user()
|
||||
const alias = words({ exactly: 2 }).join('')
|
||||
const pass = words({ exactly: 2 }).join('')
|
||||
|
||||
/**
|
||||
* @param {number} ms
|
||||
*/
|
||||
const delay = ms => new Promise(res => setTimeout(res, ms))
|
||||
|
||||
describe('gun smith', () => {
|
||||
after(() => {
|
||||
Gun.kill()
|
||||
})
|
||||
|
||||
// **************************************************************************
|
||||
// These tests are long but we run them first to detect if the re-forging
|
||||
// logic is flawed and affecting functionality.
|
||||
// **************************************************************************
|
||||
|
||||
it('writes object items into sets and correctly populates item._.get with the newly created id', done => {
|
||||
const node = instance.get(words()).get(words())
|
||||
|
||||
const obj = {
|
||||
a: 1,
|
||||
b: 'hello'
|
||||
}
|
||||
|
||||
const item = node.set(obj)
|
||||
|
||||
node.get(item._.get).once(data => {
|
||||
expect(removeBuiltInGunProps(data)).toEqual(obj)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('provides an special once() that restarts gun until a value is fetched', done => {
|
||||
const a = words()
|
||||
const b = words()
|
||||
const node = instance.get(a).get(b)
|
||||
const value = words()
|
||||
|
||||
node.specialOnce(data => {
|
||||
expect(data).toEqual(value)
|
||||
done()
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
node.put(value)
|
||||
}, 30000)
|
||||
})
|
||||
|
||||
it('provides an special then() that restarts gun until a value is fetched', async () => {
|
||||
const a = words()
|
||||
const b = words()
|
||||
const node = instance.get(a).get(b)
|
||||
const value = words()
|
||||
|
||||
setTimeout(() => {
|
||||
node.put(value)
|
||||
}, 30000)
|
||||
|
||||
const res = await node.specialThen()
|
||||
|
||||
expect(res).toBe(value)
|
||||
})
|
||||
|
||||
it('provides an special on() that restarts gun when a value has not been obtained in a determinate amount of time', done => {
|
||||
const node = instance.get(words()).get(words())
|
||||
|
||||
const secondValue = words()
|
||||
|
||||
const onceDone = once(done)
|
||||
|
||||
node.specialOn(
|
||||
debounce(data => {
|
||||
if (data === secondValue) {
|
||||
onceDone()
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
setTimeout(() => {
|
||||
node.put(secondValue)
|
||||
}, 32000)
|
||||
})
|
||||
|
||||
it('puts a true and reads it with once()', done => {
|
||||
logger.info('puts a true and reads it with once()')
|
||||
const a = words()
|
||||
const b = words()
|
||||
|
||||
instance
|
||||
.get(a)
|
||||
.get(b)
|
||||
.put(true)
|
||||
|
||||
instance
|
||||
.get(a)
|
||||
.get(b)
|
||||
.once(val => {
|
||||
expect(val).toBe(true)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('puts a false and reads it with once()', done => {
|
||||
const a = words()
|
||||
const b = words()
|
||||
|
||||
instance
|
||||
.get(a)
|
||||
.get(b)
|
||||
.put(false, ack => {
|
||||
if (ack.err) {
|
||||
throw new Error(ack.err)
|
||||
} else {
|
||||
instance
|
||||
.get(a)
|
||||
.get(b)
|
||||
.once(val => {
|
||||
expect(val).toBe(false)
|
||||
done()
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('puts numbers and reads them with once()', done => {
|
||||
const a = words()
|
||||
const b = words()
|
||||
|
||||
instance
|
||||
.get(a)
|
||||
.get(b)
|
||||
.put(5)
|
||||
|
||||
instance
|
||||
.get(a)
|
||||
.get(b)
|
||||
.once(val => {
|
||||
expect(val).toBe(5)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('puts strings and reads them with once()', done => {
|
||||
const a = words()
|
||||
const b = words()
|
||||
const sentence = words({ exactly: 50 }).join(' ')
|
||||
instance
|
||||
.get(a)
|
||||
.get(b)
|
||||
.put(sentence)
|
||||
|
||||
instance
|
||||
.get(a)
|
||||
.get(b)
|
||||
.once(val => {
|
||||
expect(val).toBe(sentence)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('merges puts', async () => {
|
||||
const a = {
|
||||
a: 1
|
||||
}
|
||||
const b = {
|
||||
b: 1
|
||||
}
|
||||
const c = { ...a, ...b }
|
||||
|
||||
const node = instance.get('foo').get('bar')
|
||||
|
||||
node.put(a)
|
||||
node.put(b)
|
||||
|
||||
const data = await node.then()
|
||||
|
||||
if (typeof data !== 'object' || data === null) {
|
||||
throw new Error('Data not an object')
|
||||
}
|
||||
expect(removeBuiltInGunProps(data)).toEqual(c)
|
||||
})
|
||||
|
||||
it('writes primitive items into sets and correctly assigns the id to ._.get', done => {
|
||||
const node = instance.get(words()).get(words())
|
||||
const item = node.set('hello')
|
||||
|
||||
node.once(data => {
|
||||
expect(removeBuiltInGunProps(data)).toEqual({
|
||||
[item._.get]: 'hello'
|
||||
})
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
// TODO: find out why this test fucks up the previous one if it runs before
|
||||
// that one
|
||||
it('maps over a primitive set', done => {
|
||||
const node = instance.get(words()).get(words())
|
||||
|
||||
const items = words({ exactly: 50 })
|
||||
|
||||
const ids = items.map(i => node.set(i)._.get)
|
||||
|
||||
let checked = 0
|
||||
|
||||
node.map().on((data, id) => {
|
||||
expect(items).toContain(data)
|
||||
expect(ids).toContain(id)
|
||||
checked++
|
||||
if (checked === 50) {
|
||||
done()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('maps over an object set', done => {
|
||||
const node = instance.get(words()).get(words())
|
||||
|
||||
const items = words({ exactly: 50 }).map(w => ({
|
||||
word: w
|
||||
}))
|
||||
|
||||
const ids = items.map(i => node.set(i)._.get)
|
||||
|
||||
let checked = 0
|
||||
|
||||
node.map().on((data, id) => {
|
||||
expect(items).toContainEqual(removeBuiltInGunProps(data))
|
||||
expect(ids).toContain(id)
|
||||
checked++
|
||||
if (checked === 50) {
|
||||
done()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('offs `on()`s', async () => {
|
||||
const node = instance.get(words()).get(words())
|
||||
|
||||
let called = false
|
||||
|
||||
node.on(() => {
|
||||
called = true
|
||||
})
|
||||
|
||||
node.off()
|
||||
|
||||
await node.pPut('return')
|
||||
await delay(500)
|
||||
expect(called).toBe(false)
|
||||
})
|
||||
|
||||
it('offs `map().on()`s', async () => {
|
||||
const node = instance.get(words()).get(words())
|
||||
|
||||
let called = false
|
||||
|
||||
const iterateeNode = node.map()
|
||||
|
||||
iterateeNode.on(() => {
|
||||
called = true
|
||||
})
|
||||
|
||||
iterateeNode.off()
|
||||
|
||||
await node.pSet('return')
|
||||
|
||||
await delay(500)
|
||||
|
||||
expect(called).toBe(false)
|
||||
})
|
||||
|
||||
it('provides an user node with create(), auth() and leave()', async () => {
|
||||
const ack = await new Promise(res => user.create(alias, pass, res))
|
||||
expect(ack.err).toBeUndefined()
|
||||
|
||||
const { pub } = ack
|
||||
expect(pub).toBeTruthy()
|
||||
expect(user.is?.pub).toEqual(pub)
|
||||
|
||||
user.leave()
|
||||
expect(user.is).toBeUndefined()
|
||||
|
||||
/** @type {GunT.AuthAck} */
|
||||
const authAck = await new Promise(res =>
|
||||
user.auth(alias, pass, ack => res(ack))
|
||||
)
|
||||
expect(authAck.err).toBeUndefined()
|
||||
expect(authAck.sea?.pub).toEqual(pub)
|
||||
expect(user.is?.pub).toEqual(pub)
|
||||
user.leave()
|
||||
})
|
||||
|
||||
it('reliably provides authentication information across re-forges', async () => {
|
||||
/** @type {GunT.AuthAck} */
|
||||
const authAck = await new Promise(res =>
|
||||
user.auth(alias, pass, ack => res(ack))
|
||||
)
|
||||
const pub = authAck.sea?.pub
|
||||
expect(pub).toBeTruthy()
|
||||
|
||||
Gun._reforge()
|
||||
expect(user.is?.pub).toEqual(pub)
|
||||
await Gun._isReady()
|
||||
expect(user.is?.pub).toEqual(pub)
|
||||
|
||||
user.leave()
|
||||
})
|
||||
|
||||
it('provides thenables for values', async () => {
|
||||
const a = words()
|
||||
const b = words()
|
||||
const node = instance.get(a).get(b)
|
||||
const value = words()
|
||||
|
||||
await new Promise((res, rej) => {
|
||||
node.put(value, ack => {
|
||||
if (ack.err) {
|
||||
rej(new Error(ack.err))
|
||||
} else {
|
||||
// @ts-ignore
|
||||
res()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const fetch = await instance
|
||||
.get(a)
|
||||
.get(b)
|
||||
.then()
|
||||
expect(fetch).toEqual(value)
|
||||
})
|
||||
|
||||
it('provides an special thenable put()', async () => {
|
||||
const a = words()
|
||||
const b = words()
|
||||
const node = instance.get(a).get(b)
|
||||
const value = words()
|
||||
|
||||
await node.pPut(value)
|
||||
|
||||
const res = await node.then()
|
||||
|
||||
expect(res).toBe(value)
|
||||
})
|
||||
|
||||
it('on()s and handles object>primitive>object transitions', done => {
|
||||
const a = {
|
||||
one: 1
|
||||
}
|
||||
const b = 'two'
|
||||
const lastPut = {
|
||||
three: 3
|
||||
}
|
||||
const c = { ...a, ...lastPut }
|
||||
|
||||
const node = instance.get(words()).get(words())
|
||||
|
||||
let checked = 0
|
||||
|
||||
node.on(
|
||||
debounce(data => {
|
||||
checked++
|
||||
if (checked === 1) {
|
||||
expect(removeBuiltInGunProps(data)).toEqual(a)
|
||||
} else if (checked === 2) {
|
||||
expect(data).toEqual(b)
|
||||
} else if (checked === 3) {
|
||||
expect(removeBuiltInGunProps(data)).toEqual(c)
|
||||
done()
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
node.put(a)
|
||||
setTimeout(() => {
|
||||
node.put(b)
|
||||
}, 800)
|
||||
setTimeout(() => {
|
||||
node.put(c)
|
||||
}, 1200)
|
||||
})
|
||||
})
|
||||
85
utils/GunSmith/GunT.ts
Normal file
85
utils/GunSmith/GunT.ts
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
/**
|
||||
* @prettier
|
||||
*/
|
||||
namespace GunT {
|
||||
export 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
|
||||
|
||||
interface OpenListenerDataObj {
|
||||
[k: string]: OpenListenerData
|
||||
}
|
||||
|
||||
export type Listener = (data: ListenerData, key: string) => void
|
||||
|
||||
export type Callback = (ack: Ack) => void
|
||||
|
||||
export interface Peer {
|
||||
url: string
|
||||
id: string
|
||||
wire?: {
|
||||
readyState: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface Soul {
|
||||
get: string
|
||||
put: Primitive | null | object | undefined
|
||||
opt: {
|
||||
peers: Record<string, Peer>
|
||||
}
|
||||
}
|
||||
export type OpenListenerData = Primitive | null | OpenListenerDataObj
|
||||
|
||||
export type OpenListener = (data: OpenListenerData, key: string) => void
|
||||
|
||||
export type LoadListenerData = OpenListenerData
|
||||
|
||||
export type LoadListener = (data: LoadListenerData, key: string) => void
|
||||
|
||||
export interface CreateAck {
|
||||
pub: string | undefined
|
||||
err: string | undefined
|
||||
}
|
||||
|
||||
export type CreateCB = (ack: CreateAck) => void
|
||||
|
||||
export interface AuthAck {
|
||||
err: string | undefined
|
||||
sea: UserPair | 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
|
||||
}
|
||||
}
|
||||
221
utils/GunSmith/Smith.ts
Normal file
221
utils/GunSmith/Smith.ts
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
/**
|
||||
* @format
|
||||
*/
|
||||
/// <reference path="GunT.ts" />
|
||||
namespace Smith {
|
||||
export interface GunSmithNode {
|
||||
_: GunT.Soul
|
||||
/**
|
||||
* Used only inside the subprocess.
|
||||
*/
|
||||
back(
|
||||
path: 'opt'
|
||||
): {
|
||||
peers: Record<
|
||||
string,
|
||||
{
|
||||
url: string
|
||||
id: string
|
||||
wire?: {
|
||||
readyState: number
|
||||
}
|
||||
}
|
||||
>
|
||||
}
|
||||
/**
|
||||
*
|
||||
*/
|
||||
get(key: string): GunSmithNode
|
||||
/**
|
||||
*
|
||||
*/
|
||||
map(): GunSmithNode
|
||||
/**
|
||||
*
|
||||
*/
|
||||
off(): void
|
||||
/**
|
||||
*
|
||||
*/
|
||||
on(cb: GunT.Listener): void
|
||||
/**
|
||||
*
|
||||
*/
|
||||
once(cb?: GunT.Listener, opts?: { wait?: number }): void
|
||||
/**
|
||||
* A promise version of put().
|
||||
* @throws
|
||||
*/
|
||||
pPut(data: GunT.ValidDataValue): Promise<void>
|
||||
/**
|
||||
* A promise version of set().
|
||||
* @throws
|
||||
*/
|
||||
pSet(data: GunT.ValidDataValue): Promise<void>
|
||||
/**
|
||||
*
|
||||
*/
|
||||
put(data: GunT.ValidDataValue, cb?: GunT.Callback): void
|
||||
/**
|
||||
*
|
||||
*/
|
||||
set(data: GunT.ValidDataValue, cb?: GunT.Callback): GunSmithNode
|
||||
/**
|
||||
* Gun will be restarted to force replication of data
|
||||
* if needed.
|
||||
* @param cb
|
||||
*/
|
||||
specialOn(cb: GunT.Listener): void
|
||||
/**
|
||||
* Gun will be restarted to force replication of data
|
||||
* if needed.
|
||||
* @param cb
|
||||
* @param _wait
|
||||
*/
|
||||
specialOnce(cb: GunT.Listener, _wait?: number): GunSmithNode
|
||||
/**
|
||||
* Gun will be restarted to force replication of data
|
||||
* if needed.
|
||||
*/
|
||||
specialThen(): Promise<GunT.ListenerData>
|
||||
then(): Promise<GunT.ListenerData>
|
||||
user(): UserSmithNode
|
||||
user(pub: string): GunSmithNode
|
||||
}
|
||||
|
||||
export interface UserSmithNode extends GunSmithNode {
|
||||
_: GunT.UserSoul
|
||||
auth(alias: string, pass: string, cb: GunT.AuthCB): void
|
||||
is?: {
|
||||
alias: string
|
||||
pub: string
|
||||
}
|
||||
create(user: string, pass: string, cb: GunT.CreateCB): void
|
||||
leave(): void
|
||||
}
|
||||
|
||||
export interface PendingPut {
|
||||
cb: GunT.Callback
|
||||
data: GunT.ValidDataValue
|
||||
id: string
|
||||
}
|
||||
|
||||
export interface SmithMsgInit {
|
||||
opts: Record<string, any>
|
||||
type: 'init'
|
||||
}
|
||||
|
||||
export interface SmithMsgAuth {
|
||||
alias: string
|
||||
pass: string
|
||||
type: 'auth'
|
||||
}
|
||||
|
||||
export interface SmithMsgCreate {
|
||||
alias: string
|
||||
pass: string
|
||||
type: 'create'
|
||||
}
|
||||
|
||||
export interface SmithMsgLeave {
|
||||
type: 'leave'
|
||||
}
|
||||
|
||||
export interface SmithMsgOn {
|
||||
path: string
|
||||
type: 'on'
|
||||
}
|
||||
|
||||
export interface SmithMsgLoad {
|
||||
id: string
|
||||
path: string
|
||||
type: 'load'
|
||||
}
|
||||
|
||||
export interface SmithMsgMapOn {
|
||||
path: string
|
||||
type: 'map.on'
|
||||
}
|
||||
|
||||
export interface SmithMsgPut {
|
||||
id: string
|
||||
data: GunT.ValidDataValue
|
||||
path: string
|
||||
type: 'put'
|
||||
}
|
||||
|
||||
export interface SmithMsgMultiPut {
|
||||
ids: string[]
|
||||
data: GunT.ValidDataValue
|
||||
path: string
|
||||
type: 'multiPut'
|
||||
}
|
||||
|
||||
export type SmithMsg =
|
||||
| SmithMsgInit
|
||||
| SmithMsgAuth
|
||||
| SmithMsgCreate
|
||||
| SmithMsgAuth
|
||||
| SmithMsgOn
|
||||
| SmithMsgLoad
|
||||
| SmithMsgMapOn
|
||||
| SmithMsgPut
|
||||
| SmithMsgMultiPut
|
||||
| BatchSmithMsg
|
||||
|
||||
export type BatchSmithMsg = SmithMsg[]
|
||||
|
||||
export interface GunMsgAuth {
|
||||
ack: GunT.AuthAck
|
||||
type: 'auth'
|
||||
}
|
||||
|
||||
export interface GunMsgCreate {
|
||||
ack: GunT.CreateAck
|
||||
pair: GunT.UserPair
|
||||
type: 'create'
|
||||
}
|
||||
|
||||
export interface GunMsgOn {
|
||||
data: GunT.ListenerData
|
||||
path: string
|
||||
type: 'on'
|
||||
}
|
||||
|
||||
export interface GunMsgMapOn {
|
||||
data: GunT.ListenerData
|
||||
path: string
|
||||
key: string
|
||||
type: 'map.on'
|
||||
}
|
||||
|
||||
export interface GunMsgLoad {
|
||||
id: string
|
||||
data: GunT.LoadListenerData
|
||||
key: string
|
||||
type: 'load'
|
||||
}
|
||||
|
||||
export interface GunMsgPut {
|
||||
ack: GunT.Ack
|
||||
id: string
|
||||
path: string
|
||||
type: 'put'
|
||||
}
|
||||
|
||||
export interface GunMsgMultiPut {
|
||||
ack: GunT.Ack
|
||||
ids: string[]
|
||||
path: string
|
||||
type: 'multiPut'
|
||||
}
|
||||
|
||||
export type GunMsg =
|
||||
| GunMsgAuth
|
||||
| GunMsgCreate
|
||||
| GunMsgOn
|
||||
| GunMsgMapOn
|
||||
| GunMsgLoad
|
||||
| GunMsgPut
|
||||
| GunMsgMultiPut
|
||||
}
|
||||
251
utils/GunSmith/gun.js
Normal file
251
utils/GunSmith/gun.js
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
/**
|
||||
* @format
|
||||
*/
|
||||
// @ts-check
|
||||
/// <reference path="Smith.ts" />
|
||||
/// <reference path="GunT.ts" />
|
||||
const Gun = require('gun')
|
||||
require('gun/nts')
|
||||
require('gun/lib/load')
|
||||
|
||||
const logger = require('../../config/log')
|
||||
|
||||
let dead = false
|
||||
|
||||
/**
|
||||
* @param {any} msg
|
||||
*/
|
||||
const sendMsg = msg => {
|
||||
if (dead) {
|
||||
return
|
||||
}
|
||||
if (process.send) {
|
||||
process.send(msg)
|
||||
} else {
|
||||
logger.error(
|
||||
'Fatal error: Could not send a message from inside the gun process.'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
logger.info('subprocess invoked')
|
||||
|
||||
process.on('uncaughtException', e => {
|
||||
logger.error('Uncaught exception inside Gun subprocess:')
|
||||
logger.error(e)
|
||||
})
|
||||
|
||||
process.on('unhandledRejection', e => {
|
||||
logger.error('Unhandled rejection inside Gun subprocess:')
|
||||
logger.error(e)
|
||||
})
|
||||
|
||||
/**
|
||||
* @type {Smith.GunSmithNode}
|
||||
*/
|
||||
// eslint-disable-next-line init-declarations
|
||||
let gun
|
||||
|
||||
/**
|
||||
* @type {Smith.UserSmithNode}
|
||||
*/
|
||||
// eslint-disable-next-line init-declarations
|
||||
let user
|
||||
|
||||
/**
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const waitForAuth = async () => {
|
||||
if (user.is && user.is.pub) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
await new Promise(res => setTimeout(res, 1000))
|
||||
|
||||
return waitForAuth()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Smith.SmithMsg} msg
|
||||
*/
|
||||
const handleMsg = async msg => {
|
||||
if (dead) {
|
||||
logger.error('Dead sub-process received msg: ', msg)
|
||||
return
|
||||
}
|
||||
// @ts-ignore
|
||||
if (msg === 'bye') {
|
||||
logger.info('KILLING')
|
||||
dead = true
|
||||
}
|
||||
if (Array.isArray(msg)) {
|
||||
msg.forEach(handleMsg)
|
||||
return
|
||||
}
|
||||
if (msg.type === 'init') {
|
||||
gun = /** @type {any} */ (new Gun(msg.opts))
|
||||
|
||||
// Force gun to connect to peers
|
||||
gun
|
||||
.get('foo')
|
||||
.get('baz')
|
||||
.once()
|
||||
|
||||
let currentPeers = ''
|
||||
setInterval(() => {
|
||||
const newPeers = JSON.stringify(
|
||||
Object.values(gun.back('opt').peers)
|
||||
.filter(p => p.wire && p.wire.readyState)
|
||||
.map(p => p.url)
|
||||
)
|
||||
if (newPeers !== currentPeers) {
|
||||
logger.info('Connected peers:', newPeers)
|
||||
currentPeers = newPeers
|
||||
}
|
||||
}, 2000)
|
||||
|
||||
setInterval(() => {
|
||||
// Log regardless of change every 30 seconds
|
||||
logger.info('Connected peers:', currentPeers)
|
||||
}, 30000)
|
||||
user = gun.user()
|
||||
|
||||
sendMsg({
|
||||
type: 'init'
|
||||
})
|
||||
}
|
||||
if (msg.type === 'auth') {
|
||||
const { alias, pass } = msg
|
||||
user.auth(alias, pass, ack => {
|
||||
/** @type {Smith.GunMsgAuth} */
|
||||
const msg = {
|
||||
ack: {
|
||||
err: ack.err,
|
||||
sea: ack.sea
|
||||
},
|
||||
type: 'auth'
|
||||
}
|
||||
sendMsg(msg)
|
||||
})
|
||||
}
|
||||
if (msg.type === 'create') {
|
||||
const { alias, pass } = msg
|
||||
user.create(alias, pass, ack => {
|
||||
/** @type {Smith.GunMsgCreate} */
|
||||
const msg = {
|
||||
ack: {
|
||||
err: ack.err,
|
||||
pub: ack.pub
|
||||
},
|
||||
pair: user._.sea,
|
||||
type: 'create'
|
||||
}
|
||||
sendMsg(msg)
|
||||
})
|
||||
}
|
||||
if (msg.type === 'on') {
|
||||
const [root, ...keys] = msg.path.split('>')
|
||||
|
||||
/** @type {Smith.GunSmithNode} */
|
||||
let node =
|
||||
{
|
||||
$root: gun,
|
||||
$user: user
|
||||
}[root] || gun.user(root)
|
||||
|
||||
for (const key of keys) {
|
||||
node = node.get(key)
|
||||
}
|
||||
node.on(data => {
|
||||
/** @type {Smith.GunMsgOn} */
|
||||
const res = {
|
||||
data,
|
||||
path: msg.path,
|
||||
type: 'on'
|
||||
}
|
||||
sendMsg(res)
|
||||
})
|
||||
}
|
||||
if (msg.type === 'map.on') {
|
||||
const [root, ...keys] = msg.path.split('>')
|
||||
|
||||
/** @type {Smith.GunSmithNode} */
|
||||
let node =
|
||||
{
|
||||
$root: gun,
|
||||
$user: user
|
||||
}[root] || gun.user(root)
|
||||
|
||||
for (const key of keys) {
|
||||
node = node.get(key)
|
||||
}
|
||||
node.map().on((data, key) => {
|
||||
/** @type {Smith.GunMsgMapOn} */
|
||||
const res = {
|
||||
data,
|
||||
key,
|
||||
path: msg.path,
|
||||
type: 'map.on'
|
||||
}
|
||||
sendMsg(res)
|
||||
})
|
||||
}
|
||||
if (msg.type === 'put') {
|
||||
const [root, ...keys] = msg.path.split('>')
|
||||
if (root === '$user') {
|
||||
await waitForAuth()
|
||||
}
|
||||
|
||||
/** @type {Smith.GunSmithNode} */
|
||||
let node =
|
||||
{
|
||||
$root: gun,
|
||||
$user: user
|
||||
}[root] || gun.user(root)
|
||||
|
||||
for (const key of keys) {
|
||||
node = node.get(key)
|
||||
}
|
||||
|
||||
node.put(msg.data, ack => {
|
||||
/** @type {Smith.GunMsgPut} */
|
||||
const reply = {
|
||||
ack: {
|
||||
err: typeof ack.err === 'string' ? ack.err : undefined
|
||||
},
|
||||
id: msg.id,
|
||||
path: msg.path,
|
||||
type: 'put'
|
||||
}
|
||||
sendMsg(reply)
|
||||
})
|
||||
}
|
||||
if (msg.type === 'multiPut') {
|
||||
const [root, ...keys] = msg.path.split('>')
|
||||
|
||||
/** @type {Smith.GunSmithNode} */
|
||||
let node =
|
||||
{
|
||||
$root: gun,
|
||||
$user: user
|
||||
}[root] || gun.user(root)
|
||||
|
||||
for (const key of keys) {
|
||||
node = node.get(key)
|
||||
}
|
||||
node.put(msg.data, ack => {
|
||||
/** @type {Smith.GunMsgMultiPut} */
|
||||
const reply = {
|
||||
ack: {
|
||||
err: ack.err
|
||||
},
|
||||
ids: msg.ids,
|
||||
path: msg.path,
|
||||
type: 'multiPut'
|
||||
}
|
||||
sendMsg(reply)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
process.on('message', handleMsg)
|
||||
1
utils/GunSmith/index.js
Normal file
1
utils/GunSmith/index.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
module.exports = require('./GunSmith')
|
||||
78
utils/GunSmith/misc.js
Normal file
78
utils/GunSmith/misc.js
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
/**
|
||||
* @format
|
||||
*/
|
||||
// @ts-check
|
||||
|
||||
// TODO: Check if merge() is equivalent to what gun does. But it should be.
|
||||
const merge = require('lodash/merge')
|
||||
|
||||
/// <reference path="./GunT.ts" />
|
||||
|
||||
/**
|
||||
* @param {GunT.ValidDataValue[]} values
|
||||
* @returns {GunT.ValidDataValue}
|
||||
*/
|
||||
const mergePuts = values => {
|
||||
/**
|
||||
* @type {GunT.ValidDataValue}
|
||||
* @example
|
||||
* x.put({ a: 1 })
|
||||
* x.put('yo')
|
||||
* assertEquals(await x.then(), 'yo')
|
||||
* x.put({ b: 2 })
|
||||
* assertEquals(await x.then(), { a: 1 , b: 2 })
|
||||
*/
|
||||
const lastObjectValue = {}
|
||||
|
||||
/** @type {GunT.ValidDataValue} */
|
||||
let finalResult = {}
|
||||
|
||||
for (const val of values) {
|
||||
if (typeof val === 'object' && val !== null) {
|
||||
finalResult = {}
|
||||
merge(lastObjectValue, val)
|
||||
merge(finalResult, lastObjectValue)
|
||||
} else {
|
||||
finalResult = val
|
||||
}
|
||||
}
|
||||
|
||||
return finalResult
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} data
|
||||
* @returns {any}
|
||||
*/
|
||||
const removeBuiltInGunProps = data => {
|
||||
if (typeof data === 'object' && data !== null) {
|
||||
const o = { ...data }
|
||||
delete o._
|
||||
delete o['#']
|
||||
return o
|
||||
}
|
||||
|
||||
console.log(data)
|
||||
throw new TypeError(
|
||||
'Non object passed to removeBuiltInGunProps: ' + JSON.stringify(data)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {GunT.ListenerData} data
|
||||
*/
|
||||
const isPopulated = data => {
|
||||
if (data === null || typeof data === 'undefined') {
|
||||
return false
|
||||
}
|
||||
if (typeof data === 'object') {
|
||||
return Object.keys(removeBuiltInGunProps(data)).length > 0
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
mergePuts,
|
||||
removeBuiltInGunProps,
|
||||
isPopulated
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
/**
|
||||
* @format
|
||||
*/
|
||||
const expect = require('expect')
|
||||
|
||||
const { asyncFilter } = require('./helpers')
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
/**
|
||||
* @format
|
||||
*/
|
||||
const Gun = require('gun')
|
||||
|
||||
const { asyncFilter } = require('./helpers')
|
||||
|
||||
|
|
@ -9,10 +8,15 @@ const { asyncFilter } = require('./helpers')
|
|||
* @returns {string}
|
||||
*/
|
||||
const gunUUID = () => {
|
||||
// @ts-expect-error Not typed
|
||||
const uuid = Gun.text.random()
|
||||
|
||||
return uuid
|
||||
// Copied from gun internals
|
||||
let s = ''
|
||||
let l = 24 // you are not going to make a 0 length random number, so no need to check type
|
||||
const c = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXZabcdefghijklmnopqrstuvwxyz'
|
||||
while (l > 0) {
|
||||
s += c.charAt(Math.floor(Math.random() * c.length))
|
||||
l--
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
|
|
|||
|
|
@ -25,7 +25,16 @@ module.exports = {
|
|||
"/api/encryption/exchange": true
|
||||
},
|
||||
PUT: {},
|
||||
DELETE: {}
|
||||
DELETE: {},
|
||||
// Preflight request (CORS)
|
||||
get OPTIONS() {
|
||||
return {
|
||||
...this.POST,
|
||||
...this.GET,
|
||||
...this.PUT,
|
||||
...this.DELETE
|
||||
}
|
||||
}
|
||||
},
|
||||
sensitiveRoutes: {
|
||||
GET: {},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue