init
This commit is contained in:
commit
4f371a08cf
|
@ -0,0 +1,9 @@
|
||||||
|
.vscode
|
||||||
|
node_modules
|
||||||
|
pnpm-lock.yaml
|
||||||
|
package-lock.json
|
||||||
|
coverage
|
||||||
|
*~
|
||||||
|
|
||||||
|
# For misc scripts and experiments:
|
||||||
|
/gitignored
|
|
@ -0,0 +1,2 @@
|
||||||
|
semi: false
|
||||||
|
singleQuote: true
|
|
@ -0,0 +1,20 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2023 Andre 'Staltz' Medeiros <contact@staltz.com>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
the Software without restriction, including without limitation the rights to
|
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@ -0,0 +1,227 @@
|
||||||
|
const debug = require('debug')('ppppp:hub-client')
|
||||||
|
const pull = require('pull-stream')
|
||||||
|
// @ts-ignore
|
||||||
|
const getSeverity = require('ssb-network-errors')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {(pull.Sink<AttendantsEvent> & {abort: () => void})} Drain
|
||||||
|
* @typedef {{type: 'state', pubkeys: Array<string>}} AttendantsEventState
|
||||||
|
* @typedef {{type: 'joined', pubkey: string}} AttendantsEventJoined
|
||||||
|
* @typedef {{type: 'left', pubkey: string}} AttendantsEventLeft
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {AttendantsEventState | AttendantsEventJoined | AttendantsEventLeft} AttendantsEvent
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = class HubObserver {
|
||||||
|
/**
|
||||||
|
* @readonly
|
||||||
|
* @type {any}
|
||||||
|
*/
|
||||||
|
#sstack
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
#hubPubkey
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
#address
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {{name: string, admin: string}}
|
||||||
|
*/
|
||||||
|
#hubMetadata
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {Set<string>}
|
||||||
|
*/
|
||||||
|
#attendants
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {Drain | undefined}
|
||||||
|
*/
|
||||||
|
#attendantsDrain
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {any} sstack
|
||||||
|
* @param {string} hubPubkey
|
||||||
|
* @param {string} address
|
||||||
|
* @param {{name: string, admin: string}} hubMetadata
|
||||||
|
* @param {any} rpc
|
||||||
|
* @param {any} onConnect
|
||||||
|
*/
|
||||||
|
constructor(sstack, hubPubkey, address, hubMetadata, rpc, onConnect) {
|
||||||
|
this.#sstack = sstack
|
||||||
|
this.#hubPubkey = hubPubkey
|
||||||
|
this.#address = address
|
||||||
|
this.#hubMetadata = hubMetadata
|
||||||
|
this.#attendants = new Set()
|
||||||
|
|
||||||
|
this.rpc = rpc
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {any} stream
|
||||||
|
* @param {string} peerPubkey
|
||||||
|
*/
|
||||||
|
this.handler = (stream, peerPubkey) => {
|
||||||
|
stream.address = `tunnel:${this.#hubPubkey}:${peerPubkey}`
|
||||||
|
// prettier-ignore
|
||||||
|
debug('Handler will call onConnect for the stream.address: %s', stream.address);
|
||||||
|
onConnect(stream)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If metadata is a plain object with at least one field
|
||||||
|
if (
|
||||||
|
typeof this.#hubMetadata === 'object' &&
|
||||||
|
this.#hubMetadata &&
|
||||||
|
Object.keys(this.#hubMetadata).length >= 1
|
||||||
|
) {
|
||||||
|
/** @type {Record<string, string>} */
|
||||||
|
const metadata = { type: 'hub' }
|
||||||
|
const { name, admin } = this.#hubMetadata
|
||||||
|
if (name) metadata.name = name
|
||||||
|
if (admin) metadata.admin = admin
|
||||||
|
this.#sstack.conn.db().update(this.#address, metadata)
|
||||||
|
this.#sstack.conn.hub().update(this.#address, metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
debug('Announcing myself to hub %s', this.#hubPubkey)
|
||||||
|
pull(
|
||||||
|
this.rpc.hub.attendants(),
|
||||||
|
(this.#attendantsDrain = /** @type {Drain} */ (
|
||||||
|
pull.drain(this.#attendantsUpdated, this.#attendantsEnded)
|
||||||
|
))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {AttendantsEvent} event
|
||||||
|
*/
|
||||||
|
#attendantsUpdated = (event) => {
|
||||||
|
// debug log
|
||||||
|
if (event.type === 'state') {
|
||||||
|
// prettier-ignore
|
||||||
|
debug('initial attendants in %s: %s', this.#hubPubkey, JSON.stringify(event.pubkeys))
|
||||||
|
} else if (event.type === 'joined') {
|
||||||
|
debug('attendant joined %s: %s', this.#hubPubkey, event.pubkey)
|
||||||
|
} else if (event.type === 'left') {
|
||||||
|
debug('attendant left %s: %s', this.#hubPubkey, event.pubkey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update attendants set
|
||||||
|
if (event.type === 'state') {
|
||||||
|
this.#attendants.clear()
|
||||||
|
for (const pubkey of event.pubkeys) {
|
||||||
|
this.#attendants.add(pubkey)
|
||||||
|
}
|
||||||
|
} else if (event.type === 'joined') {
|
||||||
|
this.#attendants.add(event.pubkey)
|
||||||
|
} else if (event.type === 'left') {
|
||||||
|
this.#attendants.delete(event.pubkey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update onlineCount metadata for this hub
|
||||||
|
const onlineCount = this.#attendants.size
|
||||||
|
this.#sstack.conn.hub().update(this.#address, { onlineCount })
|
||||||
|
|
||||||
|
// Update ssb-conn-staging
|
||||||
|
const hubName = this.#hubMetadata?.name
|
||||||
|
if (event.type === 'state') {
|
||||||
|
for (const pubkey of event.pubkeys) {
|
||||||
|
this.#notifyNewAttendant(pubkey, this.#hubPubkey, hubName)
|
||||||
|
}
|
||||||
|
} else if (event.type === 'joined') {
|
||||||
|
this.#notifyNewAttendant(event.pubkey, this.#hubPubkey, hubName)
|
||||||
|
} else if (event.type === 'left') {
|
||||||
|
const address = this.#getAddress(event.pubkey)
|
||||||
|
debug('Will disconnect and unstage %s', address)
|
||||||
|
this.#sstack.conn.unstage(address)
|
||||||
|
this.#sstack.conn.disconnect(address)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Error | boolean | null | undefined} err
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
#attendantsEnded = (err) => {
|
||||||
|
if (err && err !== true) {
|
||||||
|
this.#handleStreamError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Typically, this should stage the new attendant, but it's not up to us to
|
||||||
|
* decide that. We just notify other modules (like the ssb-conn scheduler) and
|
||||||
|
* they listen to the notify stream and stage it if they want.
|
||||||
|
*
|
||||||
|
* @param {string} attendantPubkey
|
||||||
|
* @param {string} hubPubkey
|
||||||
|
* @param {string} hubName
|
||||||
|
*/
|
||||||
|
#notifyNewAttendant(attendantPubkey, hubPubkey, hubName) {
|
||||||
|
if (attendantPubkey === hubPubkey) return
|
||||||
|
if (attendantPubkey === this.#sstack.id) return
|
||||||
|
const address = this.#getAddress(attendantPubkey)
|
||||||
|
this.#sstack.hubClient._notifyDiscoveredAttendant({
|
||||||
|
address,
|
||||||
|
attendantPubkey,
|
||||||
|
hubPubkey,
|
||||||
|
hubName,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Error} err
|
||||||
|
*/
|
||||||
|
#handleStreamError(err) {
|
||||||
|
const severity = getSeverity(err)
|
||||||
|
if (severity === 1) {
|
||||||
|
// pre-emptively destroy the stream, assuming the other
|
||||||
|
// end is packet-stream 2.0.0 sending end messages.
|
||||||
|
this.close()
|
||||||
|
} else if (severity >= 2) {
|
||||||
|
// prettier-ignore
|
||||||
|
console.error(`error getting updates from hub ${this.#hubPubkey} because ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} pubkey
|
||||||
|
*/
|
||||||
|
#getAddress(pubkey) {
|
||||||
|
return `tunnel:${this.#hubPubkey}:${pubkey}~shse:${pubkey}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Similar to close(), but just destroys this "observer", not the
|
||||||
|
* underlying connections.
|
||||||
|
*/
|
||||||
|
cancel() {
|
||||||
|
this.#attendantsDrain?.abort()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Similar to cancel(), but also closes connection with the hub server.
|
||||||
|
*/
|
||||||
|
close() {
|
||||||
|
this.#attendantsDrain?.abort()
|
||||||
|
for (const pubkey of this.#attendants) {
|
||||||
|
const address = this.#getAddress(pubkey)
|
||||||
|
this.#sstack.conn.unstage(address)
|
||||||
|
}
|
||||||
|
for (const [addr, data] of this.#sstack.conn.staging().entries()) {
|
||||||
|
if (data.hub === this.#hubPubkey) {
|
||||||
|
this.#sstack.conn.unstage(addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.rpc.close(true, (/** @type {any} */ err) => {
|
||||||
|
if (err) debug('error when closing connection with room: %o', err)
|
||||||
|
})
|
||||||
|
this.#sstack.conn.disconnect(this.#address, () => {})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
const hub = require('./plugin-hub')
|
||||||
|
const hubClient = require('./plugin-hub-client')
|
||||||
|
|
||||||
|
module.exports = [hub, hubClient]
|
|
@ -0,0 +1,165 @@
|
||||||
|
const debug = require('debug')('ppppp:hub-client')
|
||||||
|
const pull = require('pull-stream')
|
||||||
|
const run = require('promisify-tuple')
|
||||||
|
const HubObserver = require('./hub-observer')
|
||||||
|
const { muxrpcMissing } = require('./utils')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Map<string, HubObserver>} Hubs
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Hubs} hubs
|
||||||
|
* @param {any} sstack
|
||||||
|
*/
|
||||||
|
const makeTunnelPlugin = (hubs, sstack) => (/** @type {any}} */ msConfig) => {
|
||||||
|
const self = {
|
||||||
|
name: 'tunnel',
|
||||||
|
|
||||||
|
scope() {
|
||||||
|
return msConfig.scope
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {CallableFunction} onConnect
|
||||||
|
* @param {CallableFunction} startedCB
|
||||||
|
*/
|
||||||
|
server(onConnect, startedCB) {
|
||||||
|
// Once a peer connects, detect hubs, and setup observers
|
||||||
|
pull(
|
||||||
|
sstack.conn.hub().listen(),
|
||||||
|
pull.filter(({ type }) => type === 'connected'),
|
||||||
|
pull.drain(async ({ address, key, details }) => {
|
||||||
|
if (!key) return
|
||||||
|
if (hubs.has(key)) return
|
||||||
|
if (!details?.rpc) return
|
||||||
|
if (address.startsWith('tunnel:')) return
|
||||||
|
const rpc = details.rpc
|
||||||
|
const [err, res] = await run(rpc.hub.metadata)()
|
||||||
|
if (muxrpcMissing(err)) return
|
||||||
|
if (err) {
|
||||||
|
debug('failure when calling hub.metadata: %s', err.message ?? err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
debug('connected to hub %s', key)
|
||||||
|
if (hubs.has(key)) {
|
||||||
|
hubs.get(key)?.cancel()
|
||||||
|
hubs.delete(key)
|
||||||
|
}
|
||||||
|
const obs = new HubObserver(sstack, key, address, rpc, res, onConnect)
|
||||||
|
hubs.set(key, obs)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
// Once a hub disconnects, teardown
|
||||||
|
pull(
|
||||||
|
sstack.conn.hub().listen(),
|
||||||
|
pull.filter(({ type }) => type === 'disconnected'),
|
||||||
|
pull.drain(({ key }) => {
|
||||||
|
if (!key) return
|
||||||
|
if (!hubs.has(key)) return
|
||||||
|
hubs.get(key)?.close()
|
||||||
|
hubs.delete(key)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
startedCB()
|
||||||
|
|
||||||
|
// close this ms plugin
|
||||||
|
return () => {
|
||||||
|
for (const hubObserver of hubs.values()) {
|
||||||
|
hubObserver.close()
|
||||||
|
}
|
||||||
|
hubs.clear()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} address
|
||||||
|
* @param {CallableFunction} cb
|
||||||
|
*/
|
||||||
|
async client(address, cb) {
|
||||||
|
debug(`we wish to connect to %o`, address)
|
||||||
|
const opts = self.parse(address)
|
||||||
|
if (!opts) {
|
||||||
|
cb(new Error(`invalid tunnel address ${address}`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const { hub, target } = opts
|
||||||
|
const addrStr = `tunnel:${hub}:${target}`
|
||||||
|
|
||||||
|
let hubRPC = hubs.get(hub)?.rpc
|
||||||
|
|
||||||
|
// If no hub found, look up hub in connDB and connect to it
|
||||||
|
if (!hubRPC) {
|
||||||
|
for (const [msaddr] of sstack.conn.db().entries()) {
|
||||||
|
const pubkey = msaddr.split('~shse:')[1]
|
||||||
|
if (pubkey === hub) {
|
||||||
|
// prettier-ignore
|
||||||
|
debug(`to connect to ${addrStr} we first have to connect to ${hub}`)
|
||||||
|
const [err, rpc] = await run(sstack.conn.connect)(msaddr)
|
||||||
|
if (err) {
|
||||||
|
// prettier-ignore
|
||||||
|
cb(new Error(`cant connect to ${addrStr} because cant reach the hub ${hub} due to: ` + err.message ?? err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hubRPC = rpc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no hub found, find tunnel addr in connDB and connect to its `hub`
|
||||||
|
if (!hubRPC) {
|
||||||
|
const addrStrPlusShse = `tunnel:${hub}:${target}~shse:${target}`
|
||||||
|
const peerData = sstack.conn.db().get(addrStrPlusShse)
|
||||||
|
if (peerData?.hub === hub && peerData?.hubAddress) {
|
||||||
|
// prettier-ignore
|
||||||
|
debug(`to connect to ${addrStr} we first have to connect to ${hub}`)
|
||||||
|
const [err, rpc] = await run(sstack.conn.connect)(peerData.hubAddress)
|
||||||
|
if (err) {
|
||||||
|
// prettier-ignore
|
||||||
|
cb(new Error(`cant connect to ${addrStr} because cant reach the hub ${hub} due to: ` + err.message ?? err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hubRPC = rpc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If still no hub is found, consider it unknown
|
||||||
|
if (!hubRPC) {
|
||||||
|
// prettier-ignore
|
||||||
|
cb(new Error(`cant connect to ${addrStr} because hub ${hub} is offline or unknown`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
debug(`will call createTunnel with ${target} via hub ${hub}`)
|
||||||
|
const duplex = hubRPC.hub.createTunnel(
|
||||||
|
target,
|
||||||
|
(/** @type {any} */ err) => {
|
||||||
|
// prettier-ignore
|
||||||
|
if (err) debug('tunnel duplex broken with %o because %s', address, err.message ?? err)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
cb(null, duplex)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {any} addr
|
||||||
|
*/
|
||||||
|
parse(addr) {
|
||||||
|
if (typeof addr === 'object') return addr
|
||||||
|
|
||||||
|
const [prefix, hub, target] = addr.split(':')
|
||||||
|
if (prefix !== 'tunnel') return
|
||||||
|
return { prefix, hub, target }
|
||||||
|
},
|
||||||
|
|
||||||
|
stringify() {
|
||||||
|
return undefined
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = makeTunnelPlugin
|
|
@ -0,0 +1,78 @@
|
||||||
|
// @ts-ignore
|
||||||
|
const DuplexPair = require('pull-pair/duplex') // @ts-ignore
|
||||||
|
const Notify = require('pull-notify')
|
||||||
|
const debug = require('debug')('ppppp:hub-client')
|
||||||
|
const makeTunnelPlugin = require('./ms-tunnel')
|
||||||
|
const { ErrorDuplex } = require('./utils')
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
name: 'hubClient',
|
||||||
|
manifest: {
|
||||||
|
add: 'async',
|
||||||
|
connect: 'duplex',
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* @param {any} sstack
|
||||||
|
* @param {any} config
|
||||||
|
*/
|
||||||
|
init(sstack, config) {
|
||||||
|
if (!sstack.conn?.connect) {
|
||||||
|
throw new Error('hub-client is missing the required ssb-conn plugin')
|
||||||
|
}
|
||||||
|
|
||||||
|
const hubs = new Map()
|
||||||
|
|
||||||
|
sstack.multiserver.transport({
|
||||||
|
name: 'tunnel',
|
||||||
|
create: makeTunnelPlugin(hubs, sstack),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Setup discoveredAttendants source pull-stream
|
||||||
|
const _notifyDiscoveredAttendant = Notify()
|
||||||
|
function discoveredAttendants() {
|
||||||
|
return _notifyDiscoveredAttendant.listen()
|
||||||
|
}
|
||||||
|
// @ts-ignore
|
||||||
|
sstack.close.hook(function (fn, args) {
|
||||||
|
_notifyDiscoveredAttendant?.end()
|
||||||
|
// @ts-ignore
|
||||||
|
fn.apply(this, args)
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
* @param {string} origin
|
||||||
|
* @returns {import('pull-stream').Duplex<unknown, unknown>}
|
||||||
|
*/
|
||||||
|
connect(origin) {
|
||||||
|
// @ts-ignore
|
||||||
|
const hub = this.id // FIXME: this comes from secret-stack
|
||||||
|
debug('received hubClient.connect(%s) via hub %s', origin, hub)
|
||||||
|
if (hubs.has(hub) && origin) {
|
||||||
|
debug('connect() will resolve because handler exists')
|
||||||
|
const handler = hubs.get(hub).handler
|
||||||
|
const [ins, outs] = DuplexPair()
|
||||||
|
handler(ins, origin)
|
||||||
|
return outs
|
||||||
|
} else {
|
||||||
|
return ErrorDuplex(`Could not connect to ${origin} via ${hub}`)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Needed due to https://github.com/ssb-ngi-pointer/ssb-room-client/pull/3#issuecomment-808322434
|
||||||
|
ping() {
|
||||||
|
return Date.now()
|
||||||
|
},
|
||||||
|
|
||||||
|
// Internal method, needed for api-plugin.ts
|
||||||
|
getHubsMap() {
|
||||||
|
return hubs
|
||||||
|
},
|
||||||
|
|
||||||
|
discoveredAttendants,
|
||||||
|
|
||||||
|
// underscore so other modules IN THIS LIBRARY can use it
|
||||||
|
_notifyDiscoveredAttendant,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
const pull = require('pull-stream')
|
||||||
|
const { ErrorDuplex } = require('./utils')
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
name: 'hub',
|
||||||
|
version: '1.0.0',
|
||||||
|
manifest: {
|
||||||
|
ping: 'sync',
|
||||||
|
metadata: 'async',
|
||||||
|
attendants: 'source',
|
||||||
|
createTunnel: 'duplex',
|
||||||
|
createToken: 'async',
|
||||||
|
},
|
||||||
|
permissions: {
|
||||||
|
anonymous: {
|
||||||
|
allow: ['createTunnel', 'ping', 'attendants', 'createToken', 'metadata'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {any} sstack
|
||||||
|
*/
|
||||||
|
init(sstack) {
|
||||||
|
return {
|
||||||
|
attendants() {
|
||||||
|
return pull.error(new Error('Not implemented on the client'))
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} origin
|
||||||
|
* @returns {pull.Duplex<unknown, unknown>}
|
||||||
|
*/
|
||||||
|
connect(origin) {
|
||||||
|
return ErrorDuplex('Not implemented on the client')
|
||||||
|
},
|
||||||
|
|
||||||
|
ping() {
|
||||||
|
throw new Error('Not implemented on the client')
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {CallableFunction} cb
|
||||||
|
*/
|
||||||
|
createToken(cb) {
|
||||||
|
cb(new Error('Not implemented on the client'))
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {CallableFunction} cb
|
||||||
|
*/
|
||||||
|
metadata(cb) {
|
||||||
|
cb(new Error('Not implemented on the client'))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
/**
|
||||||
|
* @param {Error | string | null | undefined} err
|
||||||
|
*/
|
||||||
|
function muxrpcMissing(err) {
|
||||||
|
if (!err) return false
|
||||||
|
const errString =
|
||||||
|
typeof err === 'string'
|
||||||
|
? err
|
||||||
|
: typeof err.message === 'string'
|
||||||
|
? err.message
|
||||||
|
: ''
|
||||||
|
return errString.endsWith('not in list of allowed methods')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} message
|
||||||
|
* @returns {import('pull-stream').Duplex<unknown, unknown>}
|
||||||
|
*/
|
||||||
|
function ErrorDuplex(message) {
|
||||||
|
const err = new Error(message)
|
||||||
|
return {
|
||||||
|
source(_abort, cb) {
|
||||||
|
cb(err)
|
||||||
|
},
|
||||||
|
sink(read) {
|
||||||
|
read(err, () => {})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
muxrpcMissing,
|
||||||
|
ErrorDuplex,
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
{
|
||||||
|
"name": "ppppp-hub-client",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "secret-stack plugin to connect to a ppppp-hub",
|
||||||
|
"author": "Andre Staltz <contact@staltz.com>",
|
||||||
|
"license": "MIT",
|
||||||
|
"homepage": "https://github.com/staltz/ppppp-hub-client",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git@github.com:staltz/ppppp-hub-client.git"
|
||||||
|
},
|
||||||
|
"main": "index.js",
|
||||||
|
"files": [
|
||||||
|
"lib/**/*.js"
|
||||||
|
],
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"require": "./lib/index.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "commonjs",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "^4.3.4",
|
||||||
|
"promisify-tuple": "~1.2.0",
|
||||||
|
"pull-notify": "~0.1.2",
|
||||||
|
"pull-pair": "~1.1.0",
|
||||||
|
"pull-stream": "~3.7.0",
|
||||||
|
"ssb-network-errors": "~1.0.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/debug": "^4.1.8",
|
||||||
|
"@types/pull-stream": "^3.6.2",
|
||||||
|
"bs58": "^5.0.0",
|
||||||
|
"c8": "7",
|
||||||
|
"husky": "^4.3.0",
|
||||||
|
"prettier": "^2.6.2",
|
||||||
|
"pretty-quick": "^3.1.3",
|
||||||
|
"typescript": "^5.1.3"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test": "node --test",
|
||||||
|
"format-code": "prettier --write \"(lib|test)/**/*.js\"",
|
||||||
|
"format-code-staged": "pretty-quick --staged --pattern \"(lib|test)/**/*.js\"",
|
||||||
|
"coverage": "c8 --reporter=lcov npm run test"
|
||||||
|
},
|
||||||
|
"husky": {
|
||||||
|
"hooks": {
|
||||||
|
"pre-commit": "npm run format-code-staged"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"include": ["lib/**/*.js"],
|
||||||
|
"exclude": ["coverage/", "node_modules/", "test/"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"checkJs": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"exactOptionalPropertyTypes": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"lib": ["es2021", "dom"],
|
||||||
|
"module": "node16",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"target": "es2021"
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue