mirror of https://codeberg.org/pzp/pzp-invite.git
update to hubClient getting favorite hubs
This commit is contained in:
parent
14de10b5d3
commit
85737b4898
241
lib/index.js
241
lib/index.js
|
@ -1,16 +1,16 @@
|
|||
// @ts-ignore
|
||||
const MultiserverAddress = require('multiserver-address')
|
||||
// @ts-ignore
|
||||
const ip = require('ip')
|
||||
const p = require('promisify-tuple')
|
||||
|
||||
/**
|
||||
* @typedef {{ pubkey: string }} SHSE
|
||||
* @typedef {ReturnType<import('ppppp-promise').init>} PPPPPPromise
|
||||
* @typedef {{connect: (addr: string, cb: CB<any>) => void}} PPPPPNet
|
||||
* @typedef {ReturnType<import('ppppp-net').init>} PPPPPNet
|
||||
* @typedef {ReturnType<import('ppppp-hub-client/plugin').init>} PPPPPHubClient
|
||||
* @typedef {import('ppppp-hub-client/plugin').HubMultiaddr} HubMultiaddr
|
||||
* @typedef {{
|
||||
* type: 'join',
|
||||
* address: string,
|
||||
* multiaddr: string,
|
||||
* }} JoinCommand
|
||||
* @typedef {`join/${string}/${string}/${string}/${string}/${string}/${string}`} JoinCommandStr
|
||||
* @typedef {{
|
||||
|
@ -20,7 +20,7 @@ const p = require('promisify-tuple')
|
|||
* @typedef {`follow/${string}`} FollowCommandStr
|
||||
* @typedef {{
|
||||
* type: 'tunnel-connect',
|
||||
* address: string,
|
||||
* multiaddr: string,
|
||||
* }} TunnelConnectCommand
|
||||
* @typedef {`tunnel-connect/${string}/${string}`} TunnelConnectCommandStr
|
||||
* @typedef {{
|
||||
|
@ -41,6 +41,12 @@ const p = require('promisify-tuple')
|
|||
* | PromiseFollowCommand
|
||||
* | PromiseAccountAddCommand
|
||||
* } Command
|
||||
* @typedef {{
|
||||
* shse: SHSE;
|
||||
* promise: PPPPPPromise;
|
||||
* hubClient: PPPPPHubClient;
|
||||
* net: PPPPPNet;
|
||||
* }} Peer
|
||||
*/
|
||||
|
||||
/**
|
||||
|
@ -96,9 +102,8 @@ function parseJoinCommand(pieces, uri) {
|
|||
pieces.shift()
|
||||
pieces.shift()
|
||||
pieces.shift()
|
||||
const shse = `shse:${cred.replace('.', ':')}`
|
||||
const address = `net:${host}:${port}~${shse}` // TODO: add ws address here
|
||||
return { type: 'join', address }
|
||||
const multiaddr = `/${hostFormat}/${host}/${transport}/${port}/${transform}/${cred}`
|
||||
return { type: 'join', multiaddr }
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -137,8 +142,8 @@ function parseTunnelConnectCommand(pieces, uri) {
|
|||
pieces.shift()
|
||||
pieces.shift()
|
||||
pieces.shift()
|
||||
const address = `tunnel:${hubPubkey}:${targetPubkey}~shse:${targetPubkey}`
|
||||
return { type: 'tunnel-connect', address }
|
||||
const multiaddr = `/tunnel/${hubPubkey}.${targetPubkey}/shse/${targetPubkey}`
|
||||
return { type: 'tunnel-connect', multiaddr }
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -240,11 +245,91 @@ function parse(uri) {
|
|||
}
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* shse: SHSE;
|
||||
* promise: PPPPPPromise;
|
||||
* net: PPPPPNet;
|
||||
* }} peer
|
||||
* Among the join commands, find the first valid hub, and return its
|
||||
* - web protocol (http or https)
|
||||
* - web hostname
|
||||
* - pubkey for shse
|
||||
* @param {Array<JoinCommandStr>} joinCommands
|
||||
* @return {['http' | 'https', string, string]}
|
||||
*/
|
||||
function getFirstHub(joinCommands) {
|
||||
for (const joinCmd of joinCommands) {
|
||||
const [, hostFormat, host, , , shse, cred] = joinCmd.split('/')
|
||||
if (shse !== 'shse') continue
|
||||
const [pubkey, token] = cred.split('.')
|
||||
if (hostFormat === 'dns') {
|
||||
return ['https', host, pubkey]
|
||||
} else {
|
||||
return ['http', host, pubkey]
|
||||
}
|
||||
}
|
||||
throw new Error('No join commands found')
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Peer} peer
|
||||
* @param {number} amountHubs
|
||||
* @param {HubMultiaddr=} hardcodedHub
|
||||
* @returns {Promise<[Error] | [null, Array<JoinCommandStr>]>}
|
||||
*/
|
||||
async function makeJoinCommands(peer, amountHubs, hardcodedHub) {
|
||||
/**@type {Array<JoinCommandStr>}*/
|
||||
const joinCommands = []
|
||||
|
||||
// Get multiaddr of hubs
|
||||
const [err1, hubMultiaddrs] = hardcodedHub
|
||||
? [null, [hardcodedHub]]
|
||||
: await p(peer.hubClient.getHubs)(amountHubs)
|
||||
// prettier-ignore
|
||||
if (err1) return [new Error('Failed to get hubs while creating invite', { cause: err1 })]
|
||||
|
||||
// For each hub, connect and create token
|
||||
const hubErrors = []
|
||||
for (const multiaddr of hubMultiaddrs) {
|
||||
const [err2, rpc] = await p(peer.net.connect)(multiaddr)
|
||||
if (err2) {
|
||||
// prettier-ignore
|
||||
hubErrors.push(new Error('Failed to connect to hub while creating invite', { cause: err2 }))
|
||||
continue
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
if (!rpc.hub) continue
|
||||
// @ts-ignore
|
||||
const [err3, hubToken] = await p(rpc.hub.createToken)()
|
||||
if (err3) {
|
||||
// prettier-ignore
|
||||
hubErrors.push(new Error('Failed to create hub token while creating invite', { cause: err3 }))
|
||||
continue
|
||||
}
|
||||
|
||||
if (/shse\/([^.]+)$/.test(multiaddr) === false) {
|
||||
// prettier-ignore
|
||||
hubErrors.push(new Error(`Invalid hub multiaddr "${multiaddr}" missing shse portion while creating invite`))
|
||||
continue
|
||||
}
|
||||
const joinCommand = `join${multiaddr}.${hubToken}`
|
||||
// @ts-ignore
|
||||
joinCommands.push(joinCommand)
|
||||
}
|
||||
|
||||
// If there are no successful join commands, return error
|
||||
if (joinCommands.length === 0) {
|
||||
if (hubErrors.length === 0) {
|
||||
// prettier-ignore
|
||||
return [new Error('Failed to coordinate with hubs while creating invite, for unknown reasons')]
|
||||
}
|
||||
const cause = new AggregateError(hubErrors)
|
||||
// prettier-ignore
|
||||
return [new Error('Failed to coordinate with hubs while creating invite', { cause })]
|
||||
}
|
||||
|
||||
return [null, joinCommands]
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Peer} peer
|
||||
* @param {unknown} config
|
||||
*/
|
||||
function initInvite(peer, config) {
|
||||
|
@ -252,7 +337,7 @@ function initInvite(peer, config) {
|
|||
* @param {{
|
||||
* hubs?: number,
|
||||
* id: string,
|
||||
* _hubMsAddr?: string,
|
||||
* _hubMultiaddr?: HubMultiaddr
|
||||
* }} opts
|
||||
*
|
||||
* @param {CB<{uri: string, url: string}>} cb
|
||||
|
@ -265,59 +350,37 @@ function initInvite(peer, config) {
|
|||
// prettier-ignore
|
||||
return cb(new Error(`invite.createForFriend opts.id is required for type "follow"`))
|
||||
}
|
||||
const hubs = opts.hubs ?? 1
|
||||
if (typeof hubs !== 'number') {
|
||||
const amountHubs = opts.hubs ?? 1
|
||||
if (typeof amountHubs !== 'number') {
|
||||
// prettier-ignore
|
||||
return cb(new Error(`invite.createForFriend opts.hubs should be a number but was ${hubs}`))
|
||||
return cb(new Error(`invite.createForFriend opts.hubs should be a number but was ${amountHubs}`))
|
||||
}
|
||||
|
||||
if (!opts._hubMsAddr) {
|
||||
// prettier-ignore
|
||||
// FIXME: load hubs from ppppp-net
|
||||
return cb(new Error(`invite.createForFriend expected opts._hubMsAddr because loading from connDB not yet supported`))
|
||||
}
|
||||
|
||||
// Connect to hub and create token
|
||||
const [err, rpc] = await p(peer.net.connect)(opts._hubMsAddr)
|
||||
if (err) return cb(err)
|
||||
const [err2, hubToken] = await p(rpc.hub.createToken)()
|
||||
if (err2) return cb(err2)
|
||||
|
||||
// Parse multiserver address
|
||||
// prettier-ignore
|
||||
const ERROR_MSG = `Invalid multiserver address ${opts._hubMsAddr} for invite.createForFriend`
|
||||
const msAddr = MultiserverAddress.decode(opts._hubMsAddr)
|
||||
const [netShsAddr, wsShsAddr] = msAddr
|
||||
if (!netShsAddr) return cb(new Error(ERROR_MSG))
|
||||
const [net, shse] = netShsAddr
|
||||
if (!net) return cb(new Error(ERROR_MSG))
|
||||
if (net.name !== 'net') return cb(new Error(ERROR_MSG))
|
||||
const [host, port] = net.data
|
||||
const hostFormat = ip.isV4Format(host)
|
||||
? 'ip4'
|
||||
: ip.isV6Format('ipv6')
|
||||
? 'ip6'
|
||||
: 'dns'
|
||||
if (!shse) return cb(new Error(ERROR_MSG))
|
||||
if (shse.name !== 'shse') return cb(new Error(ERROR_MSG))
|
||||
const [pubkey] = shse.data
|
||||
// Create "join hub" commands
|
||||
const [err1, joinCommands] = await makeJoinCommands(
|
||||
peer,
|
||||
amountHubs,
|
||||
opts._hubMultiaddr
|
||||
)
|
||||
if (err1) return cb(err1)
|
||||
const [protocol, hostname] = getFirstHub(joinCommands)
|
||||
|
||||
// Create follow promise
|
||||
const [err3, token] = await p(peer.promise.create)({
|
||||
const [err2, token] = await p(peer.promise.create)({
|
||||
account: opts.id,
|
||||
type: 'follow',
|
||||
})
|
||||
if (err3) return cb(err3)
|
||||
|
||||
/** @type {JoinCommandStr} */
|
||||
const joinCommand = `join/${hostFormat}/${host}/tcp/${port}/shse/${pubkey}.${hubToken}`
|
||||
/** @type {FollowCommandStr} */
|
||||
const followCommand = `follow/${opts.id}`
|
||||
if (err2) return cb(err2)
|
||||
/** @type {PromiseFollowCommandStr} */
|
||||
const promiseCommand = `promise.follow/account.${opts.id}/${token}`
|
||||
|
||||
const uri = `ppppp://invite/${joinCommand}/${followCommand}/${promiseCommand}`
|
||||
const url = `http://${host}/invite#${encodeURIComponent(uri)}`
|
||||
// Create follow command
|
||||
/** @type {FollowCommandStr} */
|
||||
const followCommand = `follow/${opts.id}`
|
||||
|
||||
// prettier-ignore
|
||||
const uri = `ppppp://invite/${joinCommands.join('/')}/${followCommand}/${promiseCommand}`
|
||||
const url = `${protocol}://${hostname}/invite#${encodeURIComponent(uri)}`
|
||||
cb(null, { uri, url })
|
||||
}
|
||||
|
||||
|
@ -325,7 +388,7 @@ function initInvite(peer, config) {
|
|||
* @param {{
|
||||
* hubs?: number,
|
||||
* id: string,
|
||||
* _hubMsAddr?: string,
|
||||
* _hubMultiaddr?: HubMultiaddr
|
||||
* }} opts
|
||||
*
|
||||
* @param {CB<{uri: string, url: string}>} cb
|
||||
|
@ -338,55 +401,35 @@ function initInvite(peer, config) {
|
|||
// prettier-ignore
|
||||
return cb(new Error(`invite.createForMyself opts.id is required for type "follow"`))
|
||||
}
|
||||
const hubs = opts.hubs ?? 1
|
||||
if (typeof hubs !== 'number') {
|
||||
const amountHubs = opts.hubs ?? 1
|
||||
if (typeof amountHubs !== 'number') {
|
||||
// prettier-ignore
|
||||
return cb(new Error(`invite.createForMyself opts.hubs should be a number but was ${hubs}`))
|
||||
return cb(new Error(`invite.createForMyself opts.hubs should be a number but was ${amountHubs}`))
|
||||
}
|
||||
|
||||
if (!opts._hubMsAddr) {
|
||||
// prettier-ignore
|
||||
return cb(new Error(`invite.createForMyself expected opts._hubMsAddr because loading from connDB not yet supported`))
|
||||
}
|
||||
|
||||
// Connect to hub and create token
|
||||
const [err, rpc] = await p(peer.net.connect)(opts._hubMsAddr)
|
||||
if (err) return cb(err)
|
||||
const [err2, hubToken] = await p(rpc.hub.createToken)()
|
||||
if (err2) return cb(err2)
|
||||
|
||||
// Parse multiserver address
|
||||
// prettier-ignore
|
||||
const ERROR_MSG = `Invalid multiserver address ${opts._hubMsAddr} for invite.createForMyself`
|
||||
const msAddr = MultiserverAddress.decode(opts._hubMsAddr)
|
||||
const [netShsAddr, wsShsAddr] = msAddr
|
||||
if (!netShsAddr) return cb(new Error(ERROR_MSG))
|
||||
const [net, shse] = netShsAddr
|
||||
if (!net) return cb(new Error(ERROR_MSG))
|
||||
if (net.name !== 'net') return cb(new Error(ERROR_MSG))
|
||||
const [host, port] = net.data
|
||||
const hostFormat = ip.isV4Format(host)
|
||||
? 'ip4'
|
||||
: ip.isV6Format('ipv6')
|
||||
? 'ip6'
|
||||
: 'dns'
|
||||
if (!shse) return cb(new Error(ERROR_MSG))
|
||||
if (shse.name !== 'shse') return cb(new Error(ERROR_MSG))
|
||||
const [pubkey] = shse.data
|
||||
// Create "join hub" commands
|
||||
const [err1, joinCommands] = await makeJoinCommands(
|
||||
peer,
|
||||
amountHubs,
|
||||
opts._hubMultiaddr
|
||||
)
|
||||
if (err1) return cb(err1)
|
||||
const [protocol, hostname, pubkey] = getFirstHub(joinCommands)
|
||||
|
||||
// Create account-add promise
|
||||
const promise = { type: 'account-add', account: opts.id }
|
||||
const [err3, token] = await p(peer.promise.create)(promise)
|
||||
if (err3) return cb(err3)
|
||||
|
||||
/** @type {JoinCommandStr} */
|
||||
const joinCommand = `join/${hostFormat}/${host}/tcp/${port}/shse/${pubkey}.${hubToken}`
|
||||
/** @type {TunnelConnectCommandStr} */
|
||||
const tunnelCommand = `tunnel-connect/${pubkey}/${peer.shse.pubkey}`
|
||||
/** @type {PromiseAccountAddCommandStr} */
|
||||
const promiseCommand = `promise.account-add/account.${opts.id}/${token}`
|
||||
const uri = `ppppp://invite/${joinCommand}/${tunnelCommand}/${promiseCommand}`
|
||||
const url = `http://${host}/invite#${encodeURIComponent(uri)}`
|
||||
|
||||
// Create tunnel-connect command
|
||||
/** @type {TunnelConnectCommandStr} */
|
||||
const tunnelCommand = `tunnel-connect/${pubkey}/${peer.shse.pubkey}`
|
||||
|
||||
// prettier-ignore
|
||||
const uri = `ppppp://invite/${joinCommands.join('/')}/${tunnelCommand}/${promiseCommand}`
|
||||
const url = `${protocol}://${hostname}/invite#${encodeURIComponent(uri)}`
|
||||
cb(null, { uri, url })
|
||||
}
|
||||
|
||||
|
@ -394,7 +437,7 @@ function initInvite(peer, config) {
|
|||
}
|
||||
|
||||
exports.name = 'invite'
|
||||
exports.needs = ['shse', 'promise', 'net']
|
||||
exports.needs = ['shse', 'promise', 'net', 'hubClient']
|
||||
exports.manifest = {
|
||||
createForFriend: 'async',
|
||||
createForMyself: 'async',
|
||||
|
|
|
@ -24,7 +24,6 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"ip": "~1.1.8",
|
||||
"multiserver-address": "~1.0.1",
|
||||
"promisify-tuple": "1.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -32,7 +31,9 @@
|
|||
"c8": "^7.11.0",
|
||||
"husky": "^4.3.0",
|
||||
"ppppp-caps": "github:staltz/ppppp-caps",
|
||||
"ppppp-hub-client": "github:staltz/ppppp-hub-client",
|
||||
"ppppp-keypair": "github:staltz/ppppp-keypair",
|
||||
"ppppp-net": "github:staltz/ppppp-net",
|
||||
"ppppp-promise": "github:staltz/ppppp-promise",
|
||||
"prettier": "^2.6.2",
|
||||
"pretty-quick": "^3.1.3",
|
||||
|
|
|
@ -25,7 +25,7 @@ test('createForFriend()', async (t) => {
|
|||
return {
|
||||
connect(address, cb) {
|
||||
connectCalled = true
|
||||
assert.equal(address, 'net:example.com:8008~shse:HUB_PUBKEY')
|
||||
assert.equal(address, '/dns/example.com/tcp/8008/shse/HUB_PUBKEY')
|
||||
const mockRpc = {
|
||||
hub: {
|
||||
createToken(cb) {
|
||||
|
@ -56,11 +56,17 @@ test('createForFriend()', async (t) => {
|
|||
},
|
||||
}
|
||||
|
||||
const mockHubClient = {
|
||||
name: 'hubClient',
|
||||
init() {}
|
||||
}
|
||||
|
||||
const local = require('secret-stack/bare')()
|
||||
.use(require('secret-stack/plugins/net'))
|
||||
.use(require('secret-handshake-ext/secret-stack'))
|
||||
.use(mockNet)
|
||||
.use(mockPromise)
|
||||
.use(mockHubClient)
|
||||
.use(require('../lib'))
|
||||
.call(null, {
|
||||
shse: {
|
||||
|
@ -78,7 +84,7 @@ test('createForFriend()', async (t) => {
|
|||
})
|
||||
|
||||
const { uri, url } = await p(local.invite.createForFriend)({
|
||||
_hubMsAddr: 'net:example.com:8008~shse:HUB_PUBKEY',
|
||||
_hubMultiaddr: '/dns/example.com/tcp/8008/shse/HUB_PUBKEY',
|
||||
id: 'MOCK_ID',
|
||||
})
|
||||
assert.equal(
|
||||
|
@ -87,7 +93,7 @@ test('createForFriend()', async (t) => {
|
|||
)
|
||||
assert.equal(
|
||||
url,
|
||||
`http://example.com/invite#ppppp%3A%2F%2Finvite%2Fjoin%2Fdns%2Fexample.com%2Ftcp%2F8008%2Fshse%2FHUB_PUBKEY.MOCK_TOKEN%2Ffollow%2FMOCK_ID%2Fpromise.follow%2Faccount.MOCK_ID%2FMOCK_PROMISE`
|
||||
`https://example.com/invite#ppppp%3A%2F%2Finvite%2Fjoin%2Fdns%2Fexample.com%2Ftcp%2F8008%2Fshse%2FHUB_PUBKEY.MOCK_TOKEN%2Ffollow%2FMOCK_ID%2Fpromise.follow%2Faccount.MOCK_ID%2FMOCK_PROMISE`
|
||||
)
|
||||
|
||||
assert.ok(connectCalled)
|
||||
|
|
|
@ -25,7 +25,7 @@ test('createForMyself()', async (t) => {
|
|||
return {
|
||||
connect(address, cb) {
|
||||
connectCalled = true
|
||||
assert.equal(address, 'net:example.com:8008~shse:HUB_PUBKEY')
|
||||
assert.equal(address, '/dns/example.com/tcp/8008/shse/HUB_PUBKEY')
|
||||
const mockRpc = {
|
||||
hub: {
|
||||
createToken(cb) {
|
||||
|
@ -56,11 +56,17 @@ test('createForMyself()', async (t) => {
|
|||
},
|
||||
}
|
||||
|
||||
const mockHubClient = {
|
||||
name: 'hubClient',
|
||||
init() {},
|
||||
}
|
||||
|
||||
const local = require('secret-stack/bare')()
|
||||
.use(require('secret-stack/plugins/net'))
|
||||
.use(require('secret-handshake-ext/secret-stack'))
|
||||
.use(mockNet)
|
||||
.use(mockPromise)
|
||||
.use(mockHubClient)
|
||||
.use(require('../lib'))
|
||||
.call(null, {
|
||||
shse: { caps },
|
||||
|
@ -76,7 +82,7 @@ test('createForMyself()', async (t) => {
|
|||
})
|
||||
|
||||
const { uri, url } = await p(local.invite.createForMyself)({
|
||||
_hubMsAddr: 'net:example.com:8008~shse:HUB_PUBKEY',
|
||||
_hubMultiaddr: '/dns/example.com/tcp/8008/shse/HUB_PUBKEY',
|
||||
id: 'MOCK_ID',
|
||||
})
|
||||
assert.equal(
|
||||
|
@ -85,7 +91,7 @@ test('createForMyself()', async (t) => {
|
|||
)
|
||||
assert.equal(
|
||||
url,
|
||||
`http://example.com/invite#ppppp%3A%2F%2Finvite%2Fjoin%2Fdns%2Fexample.com%2Ftcp%2F8008%2Fshse%2FHUB_PUBKEY.MOCK_TOKEN%2Ftunnel-connect%2FHUB_PUBKEY%2F${local.shse.pubkey}%2Fpromise.account-add%2Faccount.MOCK_ID%2FMOCK_PROMISE`
|
||||
`https://example.com/invite#ppppp%3A%2F%2Finvite%2Fjoin%2Fdns%2Fexample.com%2Ftcp%2F8008%2Fshse%2FHUB_PUBKEY.MOCK_TOKEN%2Ftunnel-connect%2FHUB_PUBKEY%2F${local.shse.pubkey}%2Fpromise.account-add%2Faccount.MOCK_ID%2FMOCK_PROMISE`
|
||||
)
|
||||
|
||||
assert.ok(connectCalled)
|
||||
|
|
|
@ -21,7 +21,7 @@ test('parse() good friend invite', (t) => {
|
|||
assert.deepEqual(commands, [
|
||||
{
|
||||
type: 'join',
|
||||
address: 'net:example.com:8080~shse:PUBKEY:TOKEN',
|
||||
multiaddr: '/dns/example.com/tcp/8080/shse/PUBKEY.TOKEN',
|
||||
},
|
||||
{
|
||||
type: 'follow',
|
||||
|
@ -42,11 +42,11 @@ test('parse() good myself invite', (t) => {
|
|||
assert.deepEqual(commands, [
|
||||
{
|
||||
type: 'join',
|
||||
address: 'net:example.com:8080~shse:PUBKEY:TOKEN',
|
||||
multiaddr: '/dns/example.com/tcp/8080/shse/PUBKEY.TOKEN',
|
||||
},
|
||||
{
|
||||
type: 'tunnel-connect',
|
||||
address: 'tunnel:HUB_PUBKEY:OLD_PUBKEY~shse:OLD_PUBKEY',
|
||||
multiaddr: '/tunnel/HUB_PUBKEY.OLD_PUBKEY/shse/OLD_PUBKEY',
|
||||
},
|
||||
{
|
||||
type: 'promise.account-add',
|
||||
|
@ -63,7 +63,7 @@ test('parse() good tokenless join invite', (t) => {
|
|||
assert.deepEqual(commands, [
|
||||
{
|
||||
type: 'join',
|
||||
address: 'net:example.com:8080~shse:PUBKEY',
|
||||
multiaddr: '/dns/example.com/tcp/8080/shse/PUBKEY',
|
||||
},
|
||||
])
|
||||
})
|
||||
|
|
Loading…
Reference in New Issue