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')
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @typedef {(...args: [NodeJS.ErrnoException] | [null, T]) => void} CB<T>
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{ pubkey: string }} SHSE
|
||||
* @typedef {ReturnType<import('ppppp-promise').init>} PPPPPPromise
|
||||
* @typedef {{connect: (addr: string, cb: CB<any>) => void}} ConnPlugin
|
||||
* @typedef {{
|
||||
* 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',
|
||||
* address: string,
|
||||
* }} TunnelConnectCommand
|
||||
*
|
||||
* @typedef {`tunnel-connect/${string}/${string}`} TunnelConnectCommandStr
|
||||
*
|
||||
* @typedef {{
|
||||
* type: 'promise.follow',
|
||||
* issuerID: string,
|
||||
* token: string,
|
||||
* }} PromiseFollowCommand
|
||||
*
|
||||
* @typedef {`promise.follow/account.${string}/${string}`} PromiseFollowCommandStr
|
||||
*
|
||||
* @typedef {{
|
||||
* type: 'promise.account-add',
|
||||
* issuerID: string,
|
||||
* token: string,
|
||||
* }} PromiseAccountAddCommand
|
||||
*
|
||||
* @typedef {`promise.account-add/account.${string}/${string}`} PromiseAccountAddCommandStr
|
||||
*
|
||||
* @typedef {| JoinCommand
|
||||
* | FollowCommand
|
||||
* | TunnelConnectCommand
|
||||
|
@ -53,6 +41,11 @@ const p = require('promisify-tuple')
|
|||
* } Command
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @typedef {(...args: [NodeJS.ErrnoException] | [null, T]) => void} CB<T>
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {Array<string>} pieces
|
||||
* @param {string} uri
|
||||
|
@ -227,6 +220,31 @@ function parse(uri) {
|
|||
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 = {
|
||||
name: 'invite',
|
||||
manifest: {
|
||||
|
@ -238,16 +256,17 @@ module.exports = {
|
|||
parse,
|
||||
|
||||
/**
|
||||
* @param {any} local
|
||||
* @param {any} config
|
||||
* @param {{
|
||||
* shse: SHSE | null;
|
||||
* promise: PPPPPPromise | null;
|
||||
* conn: ConnPlugin | null;
|
||||
* }} peer
|
||||
* @param {unknown} config
|
||||
*/
|
||||
init(local, config) {
|
||||
if (!local.promise?.create) {
|
||||
throw new Error('ppppp-invite plugin requires ppppp-promise plugin')
|
||||
}
|
||||
if (!local.conn?.connect) {
|
||||
throw new Error('ppppp-invite plugin requires ssb-conn plugin')
|
||||
}
|
||||
init(peer, config) {
|
||||
assertSHSEExists(peer)
|
||||
assertPromisePlugin(peer)
|
||||
assertConnPlugin(peer)
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
|
@ -259,6 +278,12 @@ module.exports = {
|
|||
* @param {CB<{uri: string, url: string}>} cb
|
||||
*/
|
||||
async function createForFriend(opts, cb) {
|
||||
try {
|
||||
assertConnPlugin(peer)
|
||||
assertPromisePlugin(peer)
|
||||
} catch (err) {
|
||||
return cb(/**@type {Error}*/ (err))
|
||||
}
|
||||
if (typeof opts !== 'object') {
|
||||
return cb(new Error('invite.createForFriend is missing opts argument'))
|
||||
}
|
||||
|
@ -278,7 +303,7 @@ module.exports = {
|
|||
}
|
||||
|
||||
// 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)
|
||||
const [err2, hubToken] = await p(rpc.hub.createToken)()
|
||||
if (err2) return cb(err2)
|
||||
|
@ -298,7 +323,7 @@ module.exports = {
|
|||
const [pubkey] = shse.data
|
||||
|
||||
// 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)
|
||||
|
||||
/** @type {JoinCommandStr} */
|
||||
|
@ -323,6 +348,13 @@ module.exports = {
|
|||
* @param {CB<{uri: string, url: string}>} cb
|
||||
*/
|
||||
async function createForMyself(opts, cb) {
|
||||
try {
|
||||
assertSHSEExists(peer)
|
||||
assertConnPlugin(peer)
|
||||
assertPromisePlugin(peer)
|
||||
} catch (err) {
|
||||
return cb(/**@type {Error}*/ (err))
|
||||
}
|
||||
if (typeof opts !== 'object') {
|
||||
return cb(new Error('invite.createForMyself is missing opts argument'))
|
||||
}
|
||||
|
@ -342,7 +374,7 @@ module.exports = {
|
|||
}
|
||||
|
||||
// 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)
|
||||
const [err2, hubToken] = await p(rpc.hub.createToken)()
|
||||
if (err2) return cb(err2)
|
||||
|
@ -363,13 +395,13 @@ module.exports = {
|
|||
|
||||
// Create account-add promise
|
||||
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)
|
||||
|
||||
/** @type {JoinCommandStr} */
|
||||
const joinCommand = `join/${host}/${port}/${pubkey}/${hubToken}`
|
||||
/** @type {TunnelConnectCommandStr} */
|
||||
const tunnelCommand = `tunnel-connect/${pubkey}/${local.shse.pubkey}`
|
||||
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}`
|
||||
|
|
12
package.json
12
package.json
|
@ -14,14 +14,14 @@
|
|||
"files": [
|
||||
"lib/**/*"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"require": "./lib/index.js"
|
||||
}
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"dependencies": {
|
||||
"multiserver-address": "~1.0.1",
|
||||
"promisify-tuple": "1.2.0"
|
||||
|
@ -36,9 +36,9 @@
|
|||
"prettier": "^2.6.2",
|
||||
"pretty-quick": "^3.1.3",
|
||||
"rimraf": "^5.0.1",
|
||||
"secret-handshake-ext": "0.0.8",
|
||||
"secret-stack": "~7.1.0",
|
||||
"typescript": "^5.0.2"
|
||||
"secret-handshake-ext": "0.0.11",
|
||||
"secret-stack": "~8.0.0",
|
||||
"typescript": "^5.1.3"
|
||||
},
|
||||
"scripts": {
|
||||
"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-handshake-ext/secret-stack'))
|
||||
.use(mockConn)
|
||||
.use(mockPromise)
|
||||
.use(require('../lib'))
|
||||
.call(null, {
|
||||
shse: {
|
||||
caps,
|
||||
},
|
||||
global: {
|
||||
path,
|
||||
keypair,
|
||||
connections: {
|
||||
|
@ -70,6 +74,7 @@ test('createForFriend()', async (t) => {
|
|||
net: [{ transform: 'shse' }],
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
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-handshake-ext/secret-stack'))
|
||||
.use(mockConn)
|
||||
.use(mockPromise)
|
||||
.use(require('../lib'))
|
||||
.call(null, {
|
||||
shse: { caps },
|
||||
global: {
|
||||
path,
|
||||
keypair,
|
||||
connections: {
|
||||
|
@ -70,6 +72,7 @@ test('createForMyself()', async (t) => {
|
|||
net: [{ transform: 'shse' }],
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const { uri, url } = await p(local.invite.createForMyself)({
|
||||
|
|
|
@ -3,13 +3,14 @@
|
|||
"exclude": ["coverage/", "node_modules/", "test/"],
|
||||
"compilerOptions": {
|
||||
"checkJs": true,
|
||||
"noEmit": true,
|
||||
"declaration": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"exactOptionalPropertyTypes": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"lib": ["es2021", "dom"],
|
||||
"lib": ["es2022", "dom"],
|
||||
"module": "node16",
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"target": "es2021"
|
||||
"target": "es2022"
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue