mirror of https://codeberg.org/pzp/pzp-invite.git
init
This commit is contained in:
commit
29c8eb3d1f
|
@ -0,0 +1,27 @@
|
|||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [16.x, 18.x, 20.x]
|
||||
os: [ubuntu-latest]
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 2
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: npm install
|
||||
- run: npm test
|
|
@ -0,0 +1,4 @@
|
|||
node_modules
|
||||
.nyc_output
|
||||
coverage
|
||||
pnpm-lock.yaml
|
|
@ -0,0 +1,2 @@
|
|||
semi: false
|
||||
singleQuote: true
|
|
@ -0,0 +1,20 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2023 Andre 'Staltz' Medeiros
|
||||
|
||||
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,231 @@
|
|||
// @ts-ignore
|
||||
const MultiserverAddress = require('multiserver-address')
|
||||
const p = require('promisify-tuple')
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @typedef {(...args: [NodeJS.ErrnoException] | [null, T]) => void} CB<T>
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* type: 'follow' | 'join',
|
||||
* hubs: number,
|
||||
* id?: string,
|
||||
* _hubMsAddr?: string,
|
||||
* }} CreateOpts
|
||||
*
|
||||
* @typedef {{type: 'join', address: string}} JoinCommand
|
||||
* @typedef {{type: 'follow', id: string}} FollowCommand
|
||||
* @typedef {{
|
||||
* type: 'promise.follow',
|
||||
* issuer: string,
|
||||
* issuerType: 'pubkey' | 'identity',
|
||||
* token: string
|
||||
* }} PromiseFollowCommand
|
||||
* @typedef {JoinCommand | FollowCommand | PromiseFollowCommand} Command
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {Array<string>} pieces
|
||||
* @param {string} uri
|
||||
* @returns {JoinCommand}
|
||||
*/
|
||||
function parseJoinCommand(pieces, uri) {
|
||||
const [, host, port, pubkey, token] = pieces
|
||||
if (!host) {
|
||||
// prettier-ignore
|
||||
throw new Error(`Invalid URI "${uri}" for invite.parse, missing join hub address`)
|
||||
}
|
||||
// TODO: numeric validation for the port
|
||||
if (!port) {
|
||||
// prettier-ignore
|
||||
throw new Error(`Invalid URI "${uri}" for invite.parse, missing join hub port`)
|
||||
}
|
||||
// TODO: base58 validation for the pubkey
|
||||
if (!pubkey) {
|
||||
// prettier-ignore
|
||||
throw new Error(`Invalid URI "${uri}" for invite.parse, missing join hub pubkey`)
|
||||
}
|
||||
// TODO: base58 validation for the token
|
||||
if (!token) {
|
||||
// prettier-ignore
|
||||
throw new Error(`Invalid URI "${uri}" for invite.parse, missing join hub token`)
|
||||
}
|
||||
pieces.shift()
|
||||
pieces.shift()
|
||||
pieces.shift()
|
||||
pieces.shift()
|
||||
pieces.shift()
|
||||
const shse = `shse:${pubkey}.${token}`
|
||||
const address = `net:${host}:${port}~${shse}` // TODO: add ws address here
|
||||
return { type: 'join', address }
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array<string>} pieces
|
||||
* @param {string} uri
|
||||
* @returns {FollowCommand}
|
||||
*/
|
||||
function parseFollowCommand(pieces, uri) {
|
||||
const [, id] = pieces
|
||||
if (!id) {
|
||||
// prettier-ignore
|
||||
throw new Error(`Invalid URI "${uri}" for invite.parse, missing follow id`)
|
||||
}
|
||||
pieces.shift()
|
||||
pieces.shift()
|
||||
return { type: 'follow', id }
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array<string>} pieces
|
||||
* @param {string} uri
|
||||
* @returns {PromiseFollowCommand}
|
||||
*/
|
||||
function parsePromiseFollowCommand(pieces, uri) {
|
||||
const [, issuerAndType, token] = pieces
|
||||
if (!issuerAndType) {
|
||||
// prettier-ignore
|
||||
throw new Error(`Invalid URI "${uri}" for invite.parse, missing promise.follow issuer`)
|
||||
}
|
||||
if (!token) {
|
||||
// prettier-ignore
|
||||
throw new Error(`Invalid URI "${uri}" for invite.parse, missing promise.follow token`)
|
||||
}
|
||||
pieces.shift()
|
||||
pieces.shift()
|
||||
pieces.shift()
|
||||
const [issuerType, issuer] = issuerAndType.split('.')
|
||||
if (issuerType !== 'pubkey' && 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 }
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {`ppppp://invite/${string}`} uri
|
||||
* @returns {Array<Command>}
|
||||
*/
|
||||
function parse(uri) {
|
||||
const url = new URL(uri)
|
||||
if (url.protocol !== 'ppppp:') {
|
||||
throw new Error(
|
||||
`Invalid protocol in URI "${uri}" for invite.parse, expected "ppppp:"`
|
||||
)
|
||||
}
|
||||
if (url.host !== 'invite') {
|
||||
throw new Error(
|
||||
`Invalid host in URI "${uri}" for invite.parse, expected "invite"`
|
||||
)
|
||||
}
|
||||
const pieces = url.pathname.startsWith('/')
|
||||
? url.pathname.substring(1).split('/')
|
||||
: url.pathname.split('/')
|
||||
|
||||
const commands = []
|
||||
while (pieces.length > 0) {
|
||||
switch (pieces[0]) {
|
||||
case 'join':
|
||||
commands.push(parseJoinCommand(pieces, uri))
|
||||
break
|
||||
case 'follow':
|
||||
commands.push(parseFollowCommand(pieces, uri))
|
||||
break
|
||||
case 'promise.follow':
|
||||
commands.push(parsePromiseFollowCommand(pieces, uri))
|
||||
break
|
||||
default:
|
||||
console.log('Unknown command', pieces[0])
|
||||
pieces.shift()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return commands
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
name: 'invite',
|
||||
manifest: {
|
||||
create: 'async',
|
||||
parse: 'sync',
|
||||
},
|
||||
|
||||
parse,
|
||||
|
||||
/**
|
||||
* @param {any} sstack
|
||||
* @param {any} config
|
||||
*/
|
||||
init(sstack, config) {
|
||||
if (!sstack.promise?.create) {
|
||||
throw new Error('ppppp-invite plugin requires ppppp-promise plugin')
|
||||
}
|
||||
if (!sstack.conn?.connect) {
|
||||
throw new Error('ppppp-invite plugin requires ssb-conn plugin')
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {CreateOpts} opts
|
||||
* @param {CB<string>} cb
|
||||
*/
|
||||
async function create(opts, cb) {
|
||||
if (typeof opts !== 'object') {
|
||||
return cb(new Error('invite.create is missing opts argument'))
|
||||
}
|
||||
const type = opts.type ?? 'follow'
|
||||
if (type !== 'follow' && type !== 'join') {
|
||||
// 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"`))
|
||||
}
|
||||
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}`))
|
||||
}
|
||||
|
||||
if (!opts._hubMsAddr) {
|
||||
// prettier-ignore
|
||||
return cb(new Error(`invite.create 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);
|
||||
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 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 follow promise
|
||||
const [err3, token] = await p(sstack.promise.create)({type: 'follow'})
|
||||
if (err3) return cb(err3)
|
||||
|
||||
const joinCommand = `join/${host}/${port}/${pubkey}/${hubToken}`
|
||||
const followCommand = `follow/${opts.id}`
|
||||
const promiseCommand = `promise.follow/identity.${opts.id}/${token}`
|
||||
const uri = `ppppp://invite/${joinCommand}/${followCommand}/${promiseCommand}`
|
||||
cb(null, uri)
|
||||
}
|
||||
|
||||
return { create, parse }
|
||||
},
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
{
|
||||
"name": "ppppp-invite",
|
||||
"version": "0.0.1",
|
||||
"description": "PPPPP invite code generator and parser",
|
||||
"homepage": "https://github.com/staltz/ppppp-invite",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/staltz/ppppp-invite.git"
|
||||
},
|
||||
"author": "Andre 'Staltz' Medeiros <contact@staltz.com>",
|
||||
"license": "MIT",
|
||||
"type": "commonjs",
|
||||
"main": "lib/index.js",
|
||||
"files": [
|
||||
"lib/**/*"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"require": "./lib/index.js"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"multiserver-address": "~1.0.1",
|
||||
"promisify-tuple": "1.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.2.5",
|
||||
"c8": "^7.11.0",
|
||||
"husky": "^4.3.0",
|
||||
"ppppp-caps": "github:staltz/ppppp-caps",
|
||||
"ppppp-keypair": "github:staltz/ppppp-keypair",
|
||||
"ppppp-promise": "file:../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",
|
||||
"typescript": "^5.0.2"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"test": "node --test",
|
||||
"format-code": "prettier --write \"*.js\" \"(test|lib)/*.js\"",
|
||||
"format-code-staged": "pretty-quick --staged --pattern \"*.js\" --pattern \"(test|lib)/*.js\"",
|
||||
"coverage": "c8 --reporter=lcov npm run test"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "pretty-quick --staged"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
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('create()', async (t) => {
|
||||
const path = Path.join(os.tmpdir(), 'ppppp-promise-create-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: 'follow' })
|
||||
cb(null, 'MOCK_PROMISE')
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const stack = require('secret-stack/lib/api')([], {})
|
||||
.use(require('secret-stack/lib/core'))
|
||||
.use(require('secret-stack/lib/plugins/net'))
|
||||
.use(require('secret-handshake-ext/secret-stack'))
|
||||
.use(mockConn)
|
||||
.use(mockPromise)
|
||||
.use(require('../lib'))
|
||||
.call(null, {
|
||||
path,
|
||||
caps,
|
||||
keypair,
|
||||
connections: {
|
||||
outgoing: {
|
||||
net: [{ transform: 'shse' }],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const uri = await p(stack.invite.create)({
|
||||
_hubMsAddr: 'net:example.com:8008~shse:HUB_PUBKEY',
|
||||
id: 'MOCK_ID',
|
||||
})
|
||||
assert.equal(
|
||||
uri,
|
||||
`ppppp://invite/join/example.com/8008/HUB_PUBKEY/MOCK_TOKEN/follow/MOCK_ID/promise.follow/identity.MOCK_ID/MOCK_PROMISE`
|
||||
)
|
||||
|
||||
assert.ok(connectCalled)
|
||||
assert.ok(createTokenCalled)
|
||||
assert.ok(createPromiseCalled)
|
||||
await p(stack.close)()
|
||||
})
|
|
@ -0,0 +1,37 @@
|
|||
const test = require('node:test')
|
||||
const assert = require('node:assert')
|
||||
const plugin = require('../lib/index')
|
||||
|
||||
test('parse() error cases', (t) => {
|
||||
assert.throws(() => {
|
||||
plugin.parse('ssb://invite/join/HUB_ADDR/HUB_PUBKEY/HUB_TOKEN')
|
||||
})
|
||||
assert.throws(() => {
|
||||
plugin.parse('ppppp:invite')
|
||||
})
|
||||
assert.throws(() => {
|
||||
plugin.parse('ppppp:invite/join/HUB_ADDR')
|
||||
})
|
||||
})
|
||||
|
||||
test('parse() good cases', (t) => {
|
||||
const commands = plugin.parse(
|
||||
'ppppp://invite/join/HOST/PORT/PUBKEY/TOKEN/follow/ALICE/promise.follow/identity.ALICE/ALICE_TOKEN'
|
||||
)
|
||||
assert.deepEqual(commands, [
|
||||
{
|
||||
type: 'join',
|
||||
address: 'net:HOST:PORT~shse:PUBKEY.TOKEN',
|
||||
},
|
||||
{
|
||||
type: 'follow',
|
||||
id: 'ALICE',
|
||||
},
|
||||
{
|
||||
type: 'promise.follow',
|
||||
issuerType: 'identity',
|
||||
issuer: 'ALICE',
|
||||
token: 'ALICE_TOKEN',
|
||||
},
|
||||
])
|
||||
})
|
|
@ -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