update with latest plugins in 2024

This commit is contained in:
Andre Staltz 2024-01-02 13:30:04 +02:00
parent c606e76ae0
commit b08e3d7c9f
No known key found for this signature in database
GPG Key ID: 9EDE23EA7E8A4890
6 changed files with 209 additions and 50 deletions

View File

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

View File

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

118
protospec.md Normal file
View File

@ -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
```

View File

@ -56,18 +56,23 @@ 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, {
path,
keypair,
connections: {
outgoing: {
net: [{ transform: 'shse' }],
shse: {
caps,
},
global: {
path,
keypair,
connections: {
outgoing: {
net: [{ transform: 'shse' }],
},
},
},
})

View File

@ -56,18 +56,21 @@ 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, {
path,
keypair,
connections: {
outgoing: {
net: [{ transform: 'shse' }],
shse: { caps },
global: {
path,
keypair,
connections: {
outgoing: {
net: [{ transform: 'shse' }],
},
},
},
})

View File

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