mirror of https://codeberg.org/pzp/pzp-invite.git
update with latest plugins in 2024
This commit is contained in:
parent
c606e76ae0
commit
b08e3d7c9f
90
lib/index.js
90
lib/index.js
|
@ -3,48 +3,36 @@ const MultiserverAddress = require('multiserver-address')
|
||||||
const p = require('promisify-tuple')
|
const p = require('promisify-tuple')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @template T
|
* @typedef {{ pubkey: string }} SHSE
|
||||||
* @typedef {(...args: [NodeJS.ErrnoException] | [null, T]) => void} CB<T>
|
* @typedef {ReturnType<import('ppppp-promise').init>} PPPPPPromise
|
||||||
*/
|
* @typedef {{connect: (addr: string, cb: CB<any>) => void}} ConnPlugin
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {{
|
* @typedef {{
|
||||||
* type: 'join',
|
* type: 'join',
|
||||||
* address: string,
|
* address: string,
|
||||||
* }} JoinCommand
|
* }} JoinCommand
|
||||||
*
|
|
||||||
* @typedef {`join/${string}/${string}/${string}/${string}`} JoinCommandStr
|
* @typedef {`join/${string}/${string}/${string}/${string}`} JoinCommandStr
|
||||||
*
|
|
||||||
* @typedef {{
|
* @typedef {{
|
||||||
* type: 'follow',
|
* type: 'follow',
|
||||||
* id: string,
|
* id: string,
|
||||||
* }} FollowCommand
|
* }} FollowCommand
|
||||||
*
|
|
||||||
* @typedef {`follow/${string}`} FollowCommandStr
|
* @typedef {`follow/${string}`} FollowCommandStr
|
||||||
*
|
|
||||||
* @typedef {{
|
* @typedef {{
|
||||||
* type: 'tunnel-connect',
|
* type: 'tunnel-connect',
|
||||||
* address: string,
|
* address: string,
|
||||||
* }} TunnelConnectCommand
|
* }} TunnelConnectCommand
|
||||||
*
|
|
||||||
* @typedef {`tunnel-connect/${string}/${string}`} TunnelConnectCommandStr
|
* @typedef {`tunnel-connect/${string}/${string}`} TunnelConnectCommandStr
|
||||||
*
|
|
||||||
* @typedef {{
|
* @typedef {{
|
||||||
* type: 'promise.follow',
|
* type: 'promise.follow',
|
||||||
* issuerID: string,
|
* issuerID: string,
|
||||||
* token: string,
|
* token: string,
|
||||||
* }} PromiseFollowCommand
|
* }} PromiseFollowCommand
|
||||||
*
|
|
||||||
* @typedef {`promise.follow/account.${string}/${string}`} PromiseFollowCommandStr
|
* @typedef {`promise.follow/account.${string}/${string}`} PromiseFollowCommandStr
|
||||||
*
|
|
||||||
* @typedef {{
|
* @typedef {{
|
||||||
* type: 'promise.account-add',
|
* type: 'promise.account-add',
|
||||||
* issuerID: string,
|
* issuerID: string,
|
||||||
* token: string,
|
* token: string,
|
||||||
* }} PromiseAccountAddCommand
|
* }} PromiseAccountAddCommand
|
||||||
*
|
|
||||||
* @typedef {`promise.account-add/account.${string}/${string}`} PromiseAccountAddCommandStr
|
* @typedef {`promise.account-add/account.${string}/${string}`} PromiseAccountAddCommandStr
|
||||||
*
|
|
||||||
* @typedef {| JoinCommand
|
* @typedef {| JoinCommand
|
||||||
* | FollowCommand
|
* | FollowCommand
|
||||||
* | TunnelConnectCommand
|
* | TunnelConnectCommand
|
||||||
|
@ -53,6 +41,11 @@ const p = require('promisify-tuple')
|
||||||
* } Command
|
* } Command
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template T
|
||||||
|
* @typedef {(...args: [NodeJS.ErrnoException] | [null, T]) => void} CB<T>
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Array<string>} pieces
|
* @param {Array<string>} pieces
|
||||||
* @param {string} uri
|
* @param {string} uri
|
||||||
|
@ -227,6 +220,31 @@ function parse(uri) {
|
||||||
return commands
|
return commands
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {{ shse: SHSE | null }} peer
|
||||||
|
* @returns {asserts peer is { shse: SHSE }}
|
||||||
|
*/
|
||||||
|
function assertSHSEExists(peer) {
|
||||||
|
if (!peer.shse) throw new Error('"invite" plugin requires "shse" plugin')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {{ promise: PPPPPPromise | null }} peer
|
||||||
|
* @returns {asserts peer is { promise: PPPPPPromise }}
|
||||||
|
*/
|
||||||
|
function assertPromisePlugin(peer) {
|
||||||
|
// prettier-ignore
|
||||||
|
if (!peer.promise) throw new Error('"invite" plugin requires "promise" plugin')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {{ conn: ConnPlugin | null }} peer
|
||||||
|
* @returns {asserts peer is { conn: ConnPlugin }}
|
||||||
|
*/
|
||||||
|
function assertConnPlugin(peer) {
|
||||||
|
if (!peer.conn) throw new Error('"invite" plugin requires "conn" plugin')
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
name: 'invite',
|
name: 'invite',
|
||||||
manifest: {
|
manifest: {
|
||||||
|
@ -238,16 +256,17 @@ module.exports = {
|
||||||
parse,
|
parse,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {any} local
|
* @param {{
|
||||||
* @param {any} config
|
* shse: SHSE | null;
|
||||||
|
* promise: PPPPPPromise | null;
|
||||||
|
* conn: ConnPlugin | null;
|
||||||
|
* }} peer
|
||||||
|
* @param {unknown} config
|
||||||
*/
|
*/
|
||||||
init(local, config) {
|
init(peer, config) {
|
||||||
if (!local.promise?.create) {
|
assertSHSEExists(peer)
|
||||||
throw new Error('ppppp-invite plugin requires ppppp-promise plugin')
|
assertPromisePlugin(peer)
|
||||||
}
|
assertConnPlugin(peer)
|
||||||
if (!local.conn?.connect) {
|
|
||||||
throw new Error('ppppp-invite plugin requires ssb-conn plugin')
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {{
|
* @param {{
|
||||||
|
@ -259,6 +278,12 @@ module.exports = {
|
||||||
* @param {CB<{uri: string, url: string}>} cb
|
* @param {CB<{uri: string, url: string}>} cb
|
||||||
*/
|
*/
|
||||||
async function createForFriend(opts, cb) {
|
async function createForFriend(opts, cb) {
|
||||||
|
try {
|
||||||
|
assertConnPlugin(peer)
|
||||||
|
assertPromisePlugin(peer)
|
||||||
|
} catch (err) {
|
||||||
|
return cb(/**@type {Error}*/ (err))
|
||||||
|
}
|
||||||
if (typeof opts !== 'object') {
|
if (typeof opts !== 'object') {
|
||||||
return cb(new Error('invite.createForFriend is missing opts argument'))
|
return cb(new Error('invite.createForFriend is missing opts argument'))
|
||||||
}
|
}
|
||||||
|
@ -278,7 +303,7 @@ module.exports = {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connect to hub and create token
|
// Connect to hub and create token
|
||||||
const [err, rpc] = await p(local.conn.connect)(opts._hubMsAddr)
|
const [err, rpc] = await p(peer.conn.connect)(opts._hubMsAddr)
|
||||||
if (err) return cb(err)
|
if (err) return cb(err)
|
||||||
const [err2, hubToken] = await p(rpc.hub.createToken)()
|
const [err2, hubToken] = await p(rpc.hub.createToken)()
|
||||||
if (err2) return cb(err2)
|
if (err2) return cb(err2)
|
||||||
|
@ -298,7 +323,7 @@ module.exports = {
|
||||||
const [pubkey] = shse.data
|
const [pubkey] = shse.data
|
||||||
|
|
||||||
// Create follow promise
|
// Create follow promise
|
||||||
const [err3, token] = await p(local.promise.create)({ type: 'follow' })
|
const [err3, token] = await p(peer.promise.create)({ type: 'follow' })
|
||||||
if (err3) return cb(err3)
|
if (err3) return cb(err3)
|
||||||
|
|
||||||
/** @type {JoinCommandStr} */
|
/** @type {JoinCommandStr} */
|
||||||
|
@ -323,6 +348,13 @@ module.exports = {
|
||||||
* @param {CB<{uri: string, url: string}>} cb
|
* @param {CB<{uri: string, url: string}>} cb
|
||||||
*/
|
*/
|
||||||
async function createForMyself(opts, cb) {
|
async function createForMyself(opts, cb) {
|
||||||
|
try {
|
||||||
|
assertSHSEExists(peer)
|
||||||
|
assertConnPlugin(peer)
|
||||||
|
assertPromisePlugin(peer)
|
||||||
|
} catch (err) {
|
||||||
|
return cb(/**@type {Error}*/ (err))
|
||||||
|
}
|
||||||
if (typeof opts !== 'object') {
|
if (typeof opts !== 'object') {
|
||||||
return cb(new Error('invite.createForMyself is missing opts argument'))
|
return cb(new Error('invite.createForMyself is missing opts argument'))
|
||||||
}
|
}
|
||||||
|
@ -342,7 +374,7 @@ module.exports = {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connect to hub and create token
|
// Connect to hub and create token
|
||||||
const [err, rpc] = await p(local.conn.connect)(opts._hubMsAddr)
|
const [err, rpc] = await p(peer.conn.connect)(opts._hubMsAddr)
|
||||||
if (err) return cb(err)
|
if (err) return cb(err)
|
||||||
const [err2, hubToken] = await p(rpc.hub.createToken)()
|
const [err2, hubToken] = await p(rpc.hub.createToken)()
|
||||||
if (err2) return cb(err2)
|
if (err2) return cb(err2)
|
||||||
|
@ -363,13 +395,13 @@ module.exports = {
|
||||||
|
|
||||||
// Create account-add promise
|
// Create account-add promise
|
||||||
const promise = { type: 'account-add', account: opts.id }
|
const promise = { type: 'account-add', account: opts.id }
|
||||||
const [err3, token] = await p(local.promise.create)(promise)
|
const [err3, token] = await p(peer.promise.create)(promise)
|
||||||
if (err3) return cb(err3)
|
if (err3) return cb(err3)
|
||||||
|
|
||||||
/** @type {JoinCommandStr} */
|
/** @type {JoinCommandStr} */
|
||||||
const joinCommand = `join/${host}/${port}/${pubkey}/${hubToken}`
|
const joinCommand = `join/${host}/${port}/${pubkey}/${hubToken}`
|
||||||
/** @type {TunnelConnectCommandStr} */
|
/** @type {TunnelConnectCommandStr} */
|
||||||
const tunnelCommand = `tunnel-connect/${pubkey}/${local.shse.pubkey}`
|
const tunnelCommand = `tunnel-connect/${pubkey}/${peer.shse.pubkey}`
|
||||||
/** @type {PromiseAccountAddCommandStr} */
|
/** @type {PromiseAccountAddCommandStr} */
|
||||||
const promiseCommand = `promise.account-add/account.${opts.id}/${token}`
|
const promiseCommand = `promise.account-add/account.${opts.id}/${token}`
|
||||||
const uri = `ppppp://invite/${joinCommand}/${tunnelCommand}/${promiseCommand}`
|
const uri = `ppppp://invite/${joinCommand}/${tunnelCommand}/${promiseCommand}`
|
||||||
|
|
12
package.json
12
package.json
|
@ -14,14 +14,14 @@
|
||||||
"files": [
|
"files": [
|
||||||
"lib/**/*"
|
"lib/**/*"
|
||||||
],
|
],
|
||||||
"engines": {
|
|
||||||
"node": ">=16"
|
|
||||||
},
|
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"require": "./lib/index.js"
|
"require": "./lib/index.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"multiserver-address": "~1.0.1",
|
"multiserver-address": "~1.0.1",
|
||||||
"promisify-tuple": "1.2.0"
|
"promisify-tuple": "1.2.0"
|
||||||
|
@ -36,9 +36,9 @@
|
||||||
"prettier": "^2.6.2",
|
"prettier": "^2.6.2",
|
||||||
"pretty-quick": "^3.1.3",
|
"pretty-quick": "^3.1.3",
|
||||||
"rimraf": "^5.0.1",
|
"rimraf": "^5.0.1",
|
||||||
"secret-handshake-ext": "0.0.8",
|
"secret-handshake-ext": "0.0.11",
|
||||||
"secret-stack": "~7.1.0",
|
"secret-stack": "~8.0.0",
|
||||||
"typescript": "^5.0.2"
|
"typescript": "^5.1.3"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clean-check": "tsc --build --clean",
|
"clean-check": "tsc --build --clean",
|
||||||
|
|
|
@ -0,0 +1,118 @@
|
||||||
|
`ppppp://invite` URIs are followed by any number of "commands", where each command has a name plus a fixed-length list of arguments.
|
||||||
|
|
||||||
|
## Inviting a new user to the network
|
||||||
|
|
||||||
|
**Invite URL:**
|
||||||
|
|
||||||
|
```
|
||||||
|
ppppp://invite/join/HOST/PORT/PUBKEY/TOKEN/follow/ALICE_ID/promise.follow/account.ALICE_ID/ALICE_TOKEN
|
||||||
|
```
|
||||||
|
|
||||||
|
made of 3 "commands":
|
||||||
|
|
||||||
|
- `join/HOST/PORT/PUBKEY/TOKEN`
|
||||||
|
- Meaning "join" this hub at this address, claiming this token to become a member
|
||||||
|
- `follow/ALICE_ID`
|
||||||
|
- Meaning that you should follow Alice
|
||||||
|
- `promise.follow/account.ALICE_ID/ALICE_TOKEN`
|
||||||
|
- Meaning that Alice (ALICE_ID the `account`, not any single `pubkey`) promised to follow you back if you claim ALICE_TOKEN
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
|
||||||
|
participant A as Alice
|
||||||
|
participant H as Hub
|
||||||
|
participant B as Bob
|
||||||
|
|
||||||
|
note over A: creates aliceToken<br />for follow promise
|
||||||
|
A->>A: publishes self-encrypted<br/>msg about aliceToken
|
||||||
|
A->>H: ask for hub token
|
||||||
|
activate H
|
||||||
|
H->>H: create hubToken
|
||||||
|
H-->>A: hubToken
|
||||||
|
deactivate H
|
||||||
|
A->>B: Externally: send invite URL
|
||||||
|
B->>H: HTTP: open URL
|
||||||
|
activate H
|
||||||
|
H-->>B: HTML with PPPPP invite URI
|
||||||
|
deactivate H
|
||||||
|
B->>B: open PPPPP app
|
||||||
|
note over B: parse URI and detect 3 commands
|
||||||
|
note over B: execute command "join"
|
||||||
|
B->>H: connect with hubToken in SHSe
|
||||||
|
activate H
|
||||||
|
H->>H: add Bob as member
|
||||||
|
H-->>B: OK
|
||||||
|
deactivate H
|
||||||
|
note over B: execute command "follow"
|
||||||
|
B->>B: follow aliceID
|
||||||
|
B->>H: muxrpc: connect to anyone online<br />and try to replicate aliceID
|
||||||
|
H-->>B: OK
|
||||||
|
note over B: execute command "promise.follow"
|
||||||
|
alt If some pubkey of aliceID is online
|
||||||
|
B->>A: connect with SHS
|
||||||
|
activate A
|
||||||
|
B->>A: muxrpc: promise.follow(aliceToken, bobID)
|
||||||
|
A->>A: detect aliceToken,<br />apply followback on bobID,<br />delete aliceToken
|
||||||
|
A-->>B: OK
|
||||||
|
deactivate A
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
## Inviting a new device to my account
|
||||||
|
|
||||||
|
**Invite URL:**
|
||||||
|
|
||||||
|
```
|
||||||
|
ppppp://invite/join/HOST/PORT/PUBKEY/TOKEN/tunnel-connect/HUB_PUBKEY/OLD_PUBKEY/promise.account-add/peer.PUBKEY/OLD_TOKEN/promise.account-internal-encryption-key/peer.PUBKEY/OLD_TOKEN
|
||||||
|
```
|
||||||
|
|
||||||
|
made of 3 "commands":
|
||||||
|
|
||||||
|
- `join/HOST/PORT/PUBKEY/TOKEN`
|
||||||
|
- Meaning "join" this hub at this address, claiming this token
|
||||||
|
- `tunnel-connect/HUB_PUBKEY/OLD_PUBKEY`
|
||||||
|
- Meaning that you should connect to the old device via a tunnel in the hub
|
||||||
|
- `promise.account-add/peer.PUBKEY/OLD_TOKEN` TODO implement with peer.PUBKEY
|
||||||
|
- Meaning that the old device promised to add your pubkey if you claim OLD_TOKEN
|
||||||
|
- `promise.account-internal-encryption-key/peer.PUBKEY/OLD_TOKEN` TODO implement
|
||||||
|
- Meaning that the old device promised to send you the internal encryption key
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
|
||||||
|
participant O as Old device
|
||||||
|
participant H as Hub
|
||||||
|
participant N as New device
|
||||||
|
|
||||||
|
note over N: instruct user to create<br />an invite on the old
|
||||||
|
note over O: creates oToken<br />with account-add perm
|
||||||
|
O->>H: ask for hub token
|
||||||
|
activate H
|
||||||
|
H->>H: create hubToken
|
||||||
|
H-->>O: hubToken
|
||||||
|
deactivate H
|
||||||
|
O->>N: Externally: send invite URL or URI
|
||||||
|
N->>N: input URL or URI
|
||||||
|
note over N: parse URI and detect 3 commands
|
||||||
|
note over N: execute command "join"
|
||||||
|
N->>H: connect with hubToken in SHSe
|
||||||
|
activate H
|
||||||
|
H->>H: add New as member
|
||||||
|
H-->>N: OK
|
||||||
|
deactivate H
|
||||||
|
note over N: execute command "tunnel-connect"
|
||||||
|
alt If old pubkey is online
|
||||||
|
N->>O: connect with SHS
|
||||||
|
activate O
|
||||||
|
note over N: execute command "promise.account-add"
|
||||||
|
N->>N: consent = sign(":account-add:ACCOUNT_ID", new privkey)
|
||||||
|
N->>O: muxrpc: promise.accountAdd(oToken, new pubkey, consent)
|
||||||
|
O->>O: detect oToken,<br />apply account-add on New,<br />delete oToken
|
||||||
|
O-->>N: OK
|
||||||
|
deactivate O
|
||||||
|
else If Old is offline
|
||||||
|
N->>O: connect with SHS
|
||||||
|
O-->>N: Failure
|
||||||
|
end
|
||||||
|
```
|
|
@ -56,13 +56,17 @@ test('createForFriend()', async (t) => {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const local = require('secret-stack/bare')({ caps })
|
const local = require('secret-stack/bare')()
|
||||||
.use(require('secret-stack/plugins/net'))
|
.use(require('secret-stack/plugins/net'))
|
||||||
.use(require('secret-handshake-ext/secret-stack'))
|
.use(require('secret-handshake-ext/secret-stack'))
|
||||||
.use(mockConn)
|
.use(mockConn)
|
||||||
.use(mockPromise)
|
.use(mockPromise)
|
||||||
.use(require('../lib'))
|
.use(require('../lib'))
|
||||||
.call(null, {
|
.call(null, {
|
||||||
|
shse: {
|
||||||
|
caps,
|
||||||
|
},
|
||||||
|
global: {
|
||||||
path,
|
path,
|
||||||
keypair,
|
keypair,
|
||||||
connections: {
|
connections: {
|
||||||
|
@ -70,6 +74,7 @@ test('createForFriend()', async (t) => {
|
||||||
net: [{ transform: 'shse' }],
|
net: [{ transform: 'shse' }],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const { uri, url } = await p(local.invite.createForFriend)({
|
const { uri, url } = await p(local.invite.createForFriend)({
|
||||||
|
|
|
@ -56,13 +56,15 @@ test('createForMyself()', async (t) => {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const local = require('secret-stack/bare')({ caps })
|
const local = require('secret-stack/bare')()
|
||||||
.use(require('secret-stack/plugins/net'))
|
.use(require('secret-stack/plugins/net'))
|
||||||
.use(require('secret-handshake-ext/secret-stack'))
|
.use(require('secret-handshake-ext/secret-stack'))
|
||||||
.use(mockConn)
|
.use(mockConn)
|
||||||
.use(mockPromise)
|
.use(mockPromise)
|
||||||
.use(require('../lib'))
|
.use(require('../lib'))
|
||||||
.call(null, {
|
.call(null, {
|
||||||
|
shse: { caps },
|
||||||
|
global: {
|
||||||
path,
|
path,
|
||||||
keypair,
|
keypair,
|
||||||
connections: {
|
connections: {
|
||||||
|
@ -70,6 +72,7 @@ test('createForMyself()', async (t) => {
|
||||||
net: [{ transform: 'shse' }],
|
net: [{ transform: 'shse' }],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const { uri, url } = await p(local.invite.createForMyself)({
|
const { uri, url } = await p(local.invite.createForMyself)({
|
||||||
|
|
|
@ -3,13 +3,14 @@
|
||||||
"exclude": ["coverage/", "node_modules/", "test/"],
|
"exclude": ["coverage/", "node_modules/", "test/"],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"checkJs": true,
|
"checkJs": true,
|
||||||
"noEmit": true,
|
"declaration": true,
|
||||||
|
"emitDeclarationOnly": true,
|
||||||
"exactOptionalPropertyTypes": true,
|
"exactOptionalPropertyTypes": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"lib": ["es2021", "dom"],
|
"lib": ["es2022", "dom"],
|
||||||
"module": "node16",
|
"module": "node16",
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"target": "es2021"
|
"target": "es2022"
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue