mirror of https://codeberg.org/pzp/pzp-invite.git
createForFriend() and createForMyself()
This commit is contained in:
parent
1b5b35bb14
commit
7283a1e5b7
232
lib/index.js
232
lib/index.js
|
@ -9,21 +9,49 @@ const p = require('promisify-tuple')
|
|||
|
||||
/**
|
||||
* @typedef {{
|
||||
* type: 'follow' | 'join',
|
||||
* hubs: number,
|
||||
* id?: string,
|
||||
* _hubMsAddr?: string,
|
||||
* }} CreateOpts
|
||||
* type: 'join',
|
||||
* address: string,
|
||||
* }} JoinCommand
|
||||
*
|
||||
* @typedef {`join/${string}/${string}/${string}/${string}`} JoinCommandStr
|
||||
*
|
||||
* @typedef {{
|
||||
* type: 'follow',
|
||||
* id: string,
|
||||
* }} FollowCommand
|
||||
*
|
||||
* @typedef {`follow/${string}`} FollowCommandStr
|
||||
*
|
||||
* @typedef {{
|
||||
* type: 'tunnel-connect',
|
||||
* hubPubkey: string,
|
||||
* targetPubkey: string,
|
||||
* }} TunnelConnectCommand
|
||||
*
|
||||
* @typedef {`tunnel-connect/${string}/${string}`} TunnelConnectCommandStr
|
||||
*
|
||||
* @typedef {{type: 'join', address: string}} JoinCommand
|
||||
* @typedef {{type: 'follow', id: string}} FollowCommand
|
||||
* @typedef {{
|
||||
* type: 'promise.follow',
|
||||
* issuer: string,
|
||||
* issuerType: 'pubkey' | 'identity',
|
||||
* token: string
|
||||
* issuerID: string,
|
||||
* token: string,
|
||||
* }} PromiseFollowCommand
|
||||
* @typedef {JoinCommand | FollowCommand | PromiseFollowCommand} Command
|
||||
*
|
||||
* @typedef {`promise.follow/identity.${string}/${string}`} PromiseFollowCommandStr
|
||||
*
|
||||
* @typedef {{
|
||||
* type: 'promise.identity-add',
|
||||
* issuerID: string,
|
||||
* token: string,
|
||||
* }} PromiseIdentityAddCommand
|
||||
*
|
||||
* @typedef {`promise.identity-add/identity.${string}/${string}`} PromiseIdentityAddCommandStr
|
||||
*
|
||||
* @typedef {| JoinCommand
|
||||
* | FollowCommand
|
||||
* | TunnelConnectCommand
|
||||
* | PromiseFollowCommand
|
||||
* | PromiseIdentityAddCommand
|
||||
* } Command
|
||||
*/
|
||||
|
||||
/**
|
||||
|
@ -42,7 +70,7 @@ function parseJoinCommand(pieces, uri) {
|
|||
// prettier-ignore
|
||||
throw new Error(`Invalid URI "${uri}" for invite.parse, missing join hub port`)
|
||||
}
|
||||
// TODO: base58 validation for the pubkey
|
||||
// TODO: base58 validation for the pubkey (and maybe length)
|
||||
if (!pubkey) {
|
||||
// prettier-ignore
|
||||
throw new Error(`Invalid URI "${uri}" for invite.parse, missing join hub pubkey`)
|
||||
|
@ -78,6 +106,29 @@ function parseFollowCommand(pieces, uri) {
|
|||
return { type: 'follow', id }
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array<string>} pieces
|
||||
* @param {string} uri
|
||||
* @returns {TunnelConnectCommand}
|
||||
*/
|
||||
function parseTunnelConnectCommand(pieces, uri) {
|
||||
const [, hubPubkey, targetPubkey] = pieces
|
||||
// TODO: base58 validation for the hubPubkey (and maybe length)
|
||||
if (!hubPubkey) {
|
||||
// prettier-ignore
|
||||
throw new Error(`Invalid URI "${uri}" for invite.parse, missing tunnel connect hubPubkey`)
|
||||
}
|
||||
// TODO: base58 validation for the targetPubkey (and maybe length)
|
||||
if (!targetPubkey) {
|
||||
// prettier-ignore
|
||||
throw new Error(`Invalid URI "${uri}" for invite.parse, missing tunnel connect targetPubkey`)
|
||||
}
|
||||
pieces.shift()
|
||||
pieces.shift()
|
||||
pieces.shift()
|
||||
return { type: 'tunnel-connect', hubPubkey, targetPubkey }
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array<string>} pieces
|
||||
* @param {string} uri
|
||||
|
@ -96,12 +147,38 @@ function parsePromiseFollowCommand(pieces, uri) {
|
|||
pieces.shift()
|
||||
pieces.shift()
|
||||
pieces.shift()
|
||||
const [issuerType, issuer] = issuerAndType.split('.')
|
||||
if (issuerType !== 'pubkey' && issuerType !== 'identity') {
|
||||
const [issuerType, issuerID] = issuerAndType.split('.')
|
||||
if (issuerType !== 'identity') {
|
||||
// prettier-ignore
|
||||
throw new Error(`Invalid URI "${uri}" for invite.parse, invalid promise.follow issuer type "${issuerType}"`)
|
||||
}
|
||||
return { type: 'promise.follow', issuer, issuerType, token }
|
||||
return { type: 'promise.follow', issuerID, token }
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array<string>} pieces
|
||||
* @param {string} uri
|
||||
* @returns {PromiseIdentityAddCommand}
|
||||
*/
|
||||
function parsePromiseIdentityAddCommand(pieces, uri) {
|
||||
const [, issuerAndType, token] = pieces
|
||||
if (!issuerAndType) {
|
||||
// prettier-ignore
|
||||
throw new Error(`Invalid URI "${uri}" for invite.parse, missing promise.identity-add issuer`)
|
||||
}
|
||||
if (!token) {
|
||||
// prettier-ignore
|
||||
throw new Error(`Invalid URI "${uri}" for invite.parse, missing promise.identity-add token`)
|
||||
}
|
||||
pieces.shift()
|
||||
pieces.shift()
|
||||
pieces.shift()
|
||||
const [issuerType, issuerID] = issuerAndType.split('.')
|
||||
if (issuerType !== 'identity') {
|
||||
// prettier-ignore
|
||||
throw new Error(`Invalid URI "${uri}" for invite.parse, invalid promise.identity-add issuer type "${issuerType}"`)
|
||||
}
|
||||
return { type: 'promise.identity-add', issuerID, token }
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -126,20 +203,24 @@ function parse(uri) {
|
|||
|
||||
const commands = []
|
||||
while (pieces.length > 0) {
|
||||
switch (pieces[0]) {
|
||||
switch (/** @type {Command['type']} */ (pieces[0])) {
|
||||
case 'join':
|
||||
commands.push(parseJoinCommand(pieces, uri))
|
||||
break
|
||||
case 'follow':
|
||||
commands.push(parseFollowCommand(pieces, uri))
|
||||
break
|
||||
case 'tunnel-connect':
|
||||
commands.push(parseTunnelConnectCommand(pieces, uri))
|
||||
break
|
||||
case 'promise.follow':
|
||||
commands.push(parsePromiseFollowCommand(pieces, uri))
|
||||
break
|
||||
default:
|
||||
console.log('Unknown command', pieces[0])
|
||||
pieces.shift()
|
||||
case 'promise.identity-add':
|
||||
commands.push(parsePromiseIdentityAddCommand(pieces, uri))
|
||||
break
|
||||
default:
|
||||
throw new Error(`Unknown command: "${pieces[0]}"`)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -149,61 +230,62 @@ function parse(uri) {
|
|||
module.exports = {
|
||||
name: 'invite',
|
||||
manifest: {
|
||||
create: 'async',
|
||||
createForFriend: 'async',
|
||||
createForMyself: 'async',
|
||||
parse: 'sync',
|
||||
},
|
||||
|
||||
parse,
|
||||
|
||||
/**
|
||||
* @param {any} sstack
|
||||
* @param {any} local
|
||||
* @param {any} config
|
||||
*/
|
||||
init(sstack, config) {
|
||||
if (!sstack.promise?.create) {
|
||||
init(local, config) {
|
||||
if (!local.promise?.create) {
|
||||
throw new Error('ppppp-invite plugin requires ppppp-promise plugin')
|
||||
}
|
||||
if (!sstack.conn?.connect) {
|
||||
if (!local.conn?.connect) {
|
||||
throw new Error('ppppp-invite plugin requires ssb-conn plugin')
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {CreateOpts} opts
|
||||
* @param {{
|
||||
* hubs?: number,
|
||||
* id: string,
|
||||
* _hubMsAddr?: string,
|
||||
* }} opts
|
||||
*
|
||||
* @param {CB<{uri: string, url: string}>} cb
|
||||
*/
|
||||
async function create(opts, cb) {
|
||||
async function createForFriend(opts, cb) {
|
||||
if (typeof opts !== 'object') {
|
||||
return cb(new Error('invite.create is missing opts argument'))
|
||||
return cb(new Error('invite.createForFriend is missing opts argument'))
|
||||
}
|
||||
const type = opts.type ?? 'follow'
|
||||
if (type !== 'follow' && type !== 'join') {
|
||||
if (!opts.id) {
|
||||
// prettier-ignore
|
||||
return cb(new Error(`invite.create opts.type should be "follow" or "join" but was "${type}"`))
|
||||
}
|
||||
if (type === 'follow' && !opts.id) {
|
||||
// prettier-ignore
|
||||
return cb(new Error(`invite.create opts.id is required for type "follow"`))
|
||||
return cb(new Error(`invite.createForFriend opts.id is required for type "follow"`))
|
||||
}
|
||||
const hubs = opts.hubs ?? 1
|
||||
if (typeof hubs !== 'number') {
|
||||
// prettier-ignore
|
||||
return cb(new Error(`invite.create opts.hubs should be a number but was ${hubs}`))
|
||||
return cb(new Error(`invite.createForFriend opts.hubs should be a number but was ${hubs}`))
|
||||
}
|
||||
|
||||
if (!opts._hubMsAddr) {
|
||||
// prettier-ignore
|
||||
return cb(new Error(`invite.create expected opts._hubMsAddr because loading from connDB not yet supported`))
|
||||
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(sstack.conn.connect)(opts._hubMsAddr);
|
||||
const [err, rpc] = await p(local.conn.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.create`
|
||||
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))
|
||||
|
@ -216,17 +298,85 @@ module.exports = {
|
|||
const [pubkey] = shse.data
|
||||
|
||||
// Create follow promise
|
||||
const [err3, token] = await p(sstack.promise.create)({type: 'follow'})
|
||||
const [err3, token] = await p(local.promise.create)({ type: 'follow' })
|
||||
if (err3) return cb(err3)
|
||||
|
||||
/** @type {JoinCommandStr} */
|
||||
const joinCommand = `join/${host}/${port}/${pubkey}/${hubToken}`
|
||||
/** @type {FollowCommandStr} */
|
||||
const followCommand = `follow/${opts.id}`
|
||||
/** @type {PromiseFollowCommandStr} */
|
||||
const promiseCommand = `promise.follow/identity.${opts.id}/${token}`
|
||||
|
||||
const uri = `ppppp://invite/${joinCommand}/${followCommand}/${promiseCommand}`
|
||||
const url = `http://${host}/invite#${encodeURIComponent(uri)}`
|
||||
cb(null, {uri, url})
|
||||
cb(null, { uri, url })
|
||||
}
|
||||
|
||||
return { create, parse }
|
||||
/**
|
||||
* @param {{
|
||||
* hubs?: number,
|
||||
* id: string,
|
||||
* _hubMsAddr?: string,
|
||||
* }} opts
|
||||
*
|
||||
* @param {CB<{uri: string, url: string}>} cb
|
||||
*/
|
||||
async function createForMyself(opts, cb) {
|
||||
if (typeof opts !== 'object') {
|
||||
return cb(new Error('invite.createForMyself is missing opts argument'))
|
||||
}
|
||||
if (!opts.id) {
|
||||
// prettier-ignore
|
||||
return cb(new Error(`invite.createForMyself opts.id is required for type "follow"`))
|
||||
}
|
||||
const hubs = opts.hubs ?? 1
|
||||
if (typeof hubs !== 'number') {
|
||||
// prettier-ignore
|
||||
return cb(new Error(`invite.createForMyself opts.hubs should be a number but was ${hubs}`))
|
||||
}
|
||||
|
||||
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(local.conn.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
|
||||
if (!shse) return cb(new Error(ERROR_MSG))
|
||||
if (shse.name !== 'shse') return cb(new Error(ERROR_MSG))
|
||||
const [pubkey] = shse.data
|
||||
|
||||
// Create identity-add promise
|
||||
const promise = { type: 'identity-add', identity: opts.id }
|
||||
const [err3, token] = await p(local.promise.create)(promise)
|
||||
if (err3) return cb(err3)
|
||||
|
||||
/** @type {JoinCommandStr} */
|
||||
const joinCommand = `join/${host}/${port}/${pubkey}/${hubToken}`
|
||||
/** @type {TunnelConnectCommandStr} */
|
||||
const tunnelCommand = `tunnel-connect/${pubkey}/${local.shse.pubkey}`
|
||||
/** @type {PromiseIdentityAddCommandStr} */
|
||||
const promiseCommand = `promise.identity-add/identity.${opts.id}/${token}`
|
||||
const uri = `ppppp://invite/${joinCommand}/${tunnelCommand}/${promiseCommand}`
|
||||
const url = `http://${host}/invite#${encodeURIComponent(uri)}`
|
||||
cb(null, { uri, url })
|
||||
}
|
||||
|
||||
return { createForFriend, createForMyself, parse }
|
||||
},
|
||||
}
|
||||
|
|
|
@ -32,12 +32,12 @@
|
|||
"husky": "^4.3.0",
|
||||
"ppppp-caps": "github:staltz/ppppp-caps",
|
||||
"ppppp-keypair": "github:staltz/ppppp-keypair",
|
||||
"ppppp-promise": "file:../promise",
|
||||
"ppppp-promise": "github:staltz/ppppp-promise",
|
||||
"prettier": "^2.6.2",
|
||||
"pretty-quick": "^3.1.3",
|
||||
"rimraf": "^5.0.1",
|
||||
"secret-handshake-ext": "~0.0.6",
|
||||
"secret-stack": "^6.4.2",
|
||||
"secret-handshake-ext": "0.0.8",
|
||||
"secret-stack": "ssbc/secret-stack#bare-mode",
|
||||
"typescript": "^5.0.2"
|
||||
},
|
||||
"scripts": {
|
||||
|
|
|
@ -7,8 +7,8 @@ const rimraf = require('rimraf')
|
|||
const Keypair = require('ppppp-keypair')
|
||||
const caps = require('ppppp-caps')
|
||||
|
||||
test('create()', async (t) => {
|
||||
const path = Path.join(os.tmpdir(), 'ppppp-promise-create-0')
|
||||
test('createForFriend()', async (t) => {
|
||||
const path = Path.join(os.tmpdir(), 'ppppp-promise-createForFriend-0')
|
||||
rimraf.sync(path)
|
||||
const keypair = Keypair.generate('ed25519', 'alice')
|
||||
|
||||
|
@ -56,16 +56,14 @@ test('create()', async (t) => {
|
|||
},
|
||||
}
|
||||
|
||||
const stack = require('secret-stack/lib/api')([], {})
|
||||
.use(require('secret-stack/lib/core'))
|
||||
.use(require('secret-stack/lib/plugins/net'))
|
||||
const local = require('secret-stack/bare')({caps})
|
||||
.use(require('secret-stack/plugins/net'))
|
||||
.use(require('secret-handshake-ext/secret-stack'))
|
||||
.use(mockConn)
|
||||
.use(mockPromise)
|
||||
.use(require('../lib'))
|
||||
.call(null, {
|
||||
path,
|
||||
caps,
|
||||
keypair,
|
||||
connections: {
|
||||
outgoing: {
|
||||
|
@ -74,8 +72,7 @@ test('create()', async (t) => {
|
|||
},
|
||||
})
|
||||
|
||||
const { uri, url } = await p(stack.invite.create)({
|
||||
type: 'follow',
|
||||
const { uri, url } = await p(local.invite.createForFriend)({
|
||||
_hubMsAddr: 'net:example.com:8008~shse:HUB_PUBKEY',
|
||||
id: 'MOCK_ID',
|
||||
})
|
||||
|
@ -91,5 +88,5 @@ test('create()', async (t) => {
|
|||
assert.ok(connectCalled)
|
||||
assert.ok(createTokenCalled)
|
||||
assert.ok(createPromiseCalled)
|
||||
await p(stack.close)()
|
||||
await p(local.close)()
|
||||
})
|
|
@ -0,0 +1,92 @@
|
|||
const test = require('node:test')
|
||||
const assert = require('node:assert')
|
||||
const Path = require('node:path')
|
||||
const os = require('node:os')
|
||||
const p = require('node:util').promisify
|
||||
const rimraf = require('rimraf')
|
||||
const Keypair = require('ppppp-keypair')
|
||||
const caps = require('ppppp-caps')
|
||||
|
||||
test('createForMyself()', async (t) => {
|
||||
const path = Path.join(os.tmpdir(), 'ppppp-promise-createForMyself-0')
|
||||
rimraf.sync(path)
|
||||
const keypair = Keypair.generate('ed25519', 'alice')
|
||||
|
||||
let connectCalled = false
|
||||
let createTokenCalled = false
|
||||
let createPromiseCalled = false
|
||||
|
||||
const mockConn = {
|
||||
name: 'conn',
|
||||
manifest: {
|
||||
connect: 'async',
|
||||
},
|
||||
init() {
|
||||
return {
|
||||
connect(address, cb) {
|
||||
connectCalled = true
|
||||
assert.equal(address, 'net:example.com:8008~shse:HUB_PUBKEY')
|
||||
const mockRpc = {
|
||||
hub: {
|
||||
createToken(cb) {
|
||||
createTokenCalled = true
|
||||
cb(null, 'MOCK_TOKEN')
|
||||
},
|
||||
},
|
||||
}
|
||||
cb(null, mockRpc)
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const mockPromise = {
|
||||
name: 'promise',
|
||||
manifest: {
|
||||
create: 'async',
|
||||
},
|
||||
init() {
|
||||
return {
|
||||
create(opts, cb) {
|
||||
createPromiseCalled = true
|
||||
assert.deepEqual(opts, { type: 'identity-add', identity: 'MOCK_ID' })
|
||||
cb(null, 'MOCK_PROMISE')
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const local = require('secret-stack/bare')({ caps })
|
||||
.use(require('secret-stack/plugins/net'))
|
||||
.use(require('secret-handshake-ext/secret-stack'))
|
||||
.use(mockConn)
|
||||
.use(mockPromise)
|
||||
.use(require('../lib'))
|
||||
.call(null, {
|
||||
path,
|
||||
keypair,
|
||||
connections: {
|
||||
outgoing: {
|
||||
net: [{ transform: 'shse' }],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const { uri, url } = await p(local.invite.createForMyself)({
|
||||
_hubMsAddr: 'net:example.com:8008~shse:HUB_PUBKEY',
|
||||
id: 'MOCK_ID',
|
||||
})
|
||||
assert.equal(
|
||||
uri,
|
||||
`ppppp://invite/join/example.com/8008/HUB_PUBKEY/MOCK_TOKEN/tunnel-connect/HUB_PUBKEY/${local.shse.pubkey}/promise.identity-add/identity.MOCK_ID/MOCK_PROMISE`
|
||||
)
|
||||
assert.equal(
|
||||
url,
|
||||
`http://example.com/invite#ppppp%3A%2F%2Finvite%2Fjoin%2Fexample.com%2F8008%2FHUB_PUBKEY%2FMOCK_TOKEN%2Ftunnel-connect%2FHUB_PUBKEY%2F${local.shse.pubkey}%2Fpromise.identity-add%2Fidentity.MOCK_ID%2FMOCK_PROMISE`
|
||||
)
|
||||
|
||||
assert.ok(connectCalled)
|
||||
assert.ok(createTokenCalled)
|
||||
assert.ok(createPromiseCalled)
|
||||
await p(local.close)()
|
||||
})
|
|
@ -29,8 +29,7 @@ test('parse() good cases', (t) => {
|
|||
},
|
||||
{
|
||||
type: 'promise.follow',
|
||||
issuerType: 'identity',
|
||||
issuer: 'ALICE',
|
||||
issuerID: 'ALICE',
|
||||
token: 'ALICE_TOKEN',
|
||||
},
|
||||
])
|
||||
|
|
Loading…
Reference in New Issue