createForFriend() and createForMyself()

This commit is contained in:
Andre Staltz 2023-07-20 19:09:23 +03:00
parent 1b5b35bb14
commit 7283a1e5b7
No known key found for this signature in database
GPG Key ID: 9EDE23EA7E8A4890
5 changed files with 293 additions and 55 deletions

View File

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

View File

@ -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": {

View File

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

View File

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

View File

@ -29,8 +29,7 @@ test('parse() good cases', (t) => {
},
{
type: 'promise.follow',
issuerType: 'identity',
issuer: 'ALICE',
issuerID: 'ALICE',
token: 'ALICE_TOKEN',
},
])