Merge pull request #484 from shocknet/gunsmith-rebased

Gunsmith rebased
This commit is contained in:
Daniel Lugo 2021-11-11 14:07:47 -04:00 committed by GitHub
commit 951055ea0c
31 changed files with 2656 additions and 2503 deletions

View file

@ -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
View file

@ -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
View file

@ -0,0 +1 @@
v14.17.6

View file

@ -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"
]
}

View file

@ -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

View file

@ -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",

View file

@ -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__
}

View file

@ -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') &&

View file

@ -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}

View file

@ -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')

View file

@ -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

View file

@ -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}`)

View file

@ -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
}

View 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)
})
})

View file

@ -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()

View file

@ -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) => {

View file

@ -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({

View file

@ -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
}

View file

@ -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 */

View file

@ -188,5 +188,7 @@ module.exports = {
encryptMessage,
decryptMessage,
authorizeDevice,
generateRandomString
generateRandomString,
nodeKeyPairs,
devicePublicKeys
}

760
utils/GunSmith/GunSmith.js Normal file
View 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
}

View 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
View 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
View 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
View 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
View file

@ -0,0 +1 @@
module.exports = require('./GunSmith')

78
utils/GunSmith/misc.js Normal file
View 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
}

View file

@ -1,6 +1,7 @@
/**
* @format
*/
const expect = require('expect')
const { asyncFilter } = require('./helpers')

View file

@ -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 = {

View file

@ -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: {},

2630
yarn.lock

File diff suppressed because it is too large Load diff