add identityAdd API

This commit is contained in:
Andre Staltz 2023-07-18 10:39:54 +03:00
parent 99a1570c5a
commit d70d119e7c
No known key found for this signature in database
GPG Key ID: 9EDE23EA7E8A4890
3 changed files with 152 additions and 28 deletions

View File

@ -5,6 +5,10 @@ const crypto = require('node:crypto')
const bs58 = require('bs58') const bs58 = require('bs58')
const b4a = require('b4a') const b4a = require('b4a')
/**
* @typedef {import('ppppp-db/msg-v3').IdentityAdd} IdentityAdd
*/
/** /**
* @template T * @template T
* @typedef {(...args: [NodeJS.ErrnoException] | [null, T]) => void} CB<T> * @typedef {(...args: [NodeJS.ErrnoException] | [null, T]) => void} CB<T>
@ -16,7 +20,8 @@ const b4a = require('b4a')
/** /**
* @typedef {{type: 'follow'}} FollowPromise * @typedef {{type: 'follow'}} FollowPromise
* @typedef {FollowPromise} PPromise * @typedef {{type: 'identity-add', identity: string}} IdentityAddPromise
* @typedef {FollowPromise | IdentityAddPromise} PPromise
*/ */
const FILENAME = 'promises.json' const FILENAME = 'promises.json'
@ -29,18 +34,19 @@ module.exports = {
revoke: 'async', revoke: 'async',
// promises // promises
follow: 'async', follow: 'async',
identityAdd: 'async',
}, },
permissions: { permissions: {
anonymous: { anonymous: {
allow: ['follow'], allow: ['follow', 'identityAdd'],
}, },
}, },
/** /**
* @param {any} sstack * @param {any} local
* @param {any} config * @param {any} config
*/ */
init(sstack, config) { init(local, config) {
const devicePromisesFile = Path.join(config.path, FILENAME) const devicePromisesFile = Path.join(config.path, FILENAME)
const promises = /** @type {Map<string, PPromise>} */ (new Map()) const promises = /** @type {Map<string, PPromise>} */ (new Map())
@ -79,10 +85,17 @@ module.exports = {
if ( if (
typeof promise !== 'object' || typeof promise !== 'object' ||
typeof promise.type !== 'string' || typeof promise.type !== 'string' ||
promise.type !== 'follow' (promise.type !== 'follow' && promise.type !== 'identity-add')
) { ) {
return Error('Invalid promise created: ' + JSON.stringify(promise)) return Error('Invalid promise created: ' + JSON.stringify(promise))
} }
if (
promise.type === 'identity-add' &&
typeof promise.identity !== 'string'
) {
// prettier-ignore
return Error('Invalid identity-add promise missing "identity" field: ' + JSON.stringify(promise))
}
return null return null
} }
@ -141,6 +154,60 @@ module.exports = {
}) })
} }
/**
* @param {string} token
* @param {IdentityAdd} addition
* @param {CB<boolean>} cb
*/
function identityAdd(token, addition, cb) {
if (!loaded) {
setTimeout(() => identityAdd(token, addition, cb), 100)
return
}
if (
!addition?.key?.purpose ||
!addition?.key?.algorithm ||
!addition?.key?.bytes ||
!addition?.consent ||
addition?.key?.purpose !== 'sig' ||
addition?.key?.algorithm !== 'ed25519'
) {
cb(new Error('Invalid key to be added: ' + JSON.stringify(addition)))
return
}
if (!promises.has(token)) {
cb(new Error('Invalid token'))
return
}
const promise = /** @type {PPromise} */ (promises.get(token))
if (promise.type !== 'identity-add') {
cb(new Error('Invalid token'))
return
}
// TODO identity.has, and if true, then call cb with false
local.db.identity.add(
{
identity: promise.identity,
keypair: { curve: 'ed25519', public: addition.key.bytes },
consent: addition.consent,
},
/**
* @param {Error | null} err
* @param {any} rec
*/
(err, rec) => {
if (err) return cb(err)
promises.delete(token)
save(() => {
cb(null, true)
})
}
)
}
/** /**
* @param {string} token * @param {string} token
* @param {CB<any>} cb * @param {CB<any>} cb
@ -155,6 +222,6 @@ module.exports = {
save(cb) save(cb)
} }
return { create, revoke, follow } return { create, revoke, follow, identityAdd }
}, },
} }

View File

@ -33,12 +33,13 @@
"c8": "^7.11.0", "c8": "^7.11.0",
"husky": "^4.3.0", "husky": "^4.3.0",
"ppppp-caps": "github:staltz/ppppp-caps", "ppppp-caps": "github:staltz/ppppp-caps",
"ppppp-db": "github:staltz/ppppp-db",
"ppppp-keypair": "github:staltz/ppppp-keypair", "ppppp-keypair": "github:staltz/ppppp-keypair",
"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.6", "secret-handshake-ext": "~0.0.8",
"secret-stack": "^6.4.2", "secret-stack": "ssbc/secret-stack#bare-mode",
"typescript": "^5.0.2" "typescript": "^5.0.2"
}, },
"scripts": { "scripts": {

View File

@ -15,25 +15,24 @@ function setup() {
rimraf.sync(path) rimraf.sync(path)
const keypair = Keypair.generate('ed25519', 'alice') const keypair = Keypair.generate('ed25519', 'alice')
const stack = require('secret-stack/lib/api')([], {}) const local = require('secret-stack/bare')({ caps })
.use(require('secret-stack/lib/core')) .use(require('secret-stack/plugins/net'))
.use(require('secret-stack/lib/plugins/net'))
.use(require('secret-handshake-ext/secret-stack')) .use(require('secret-handshake-ext/secret-stack'))
.use(require('ppppp-db'))
.use(require('../lib')) .use(require('../lib'))
.call(null, { .call(null, {
path, path,
caps,
keypair, keypair,
}) })
return { stack, path, keypair } return { local, path, keypair }
} }
test('create()', async (t) => { test('create()', async (t) => {
const { stack, path } = setup() const { local, path } = setup()
const promise = { type: 'follow' } const promise = { type: 'follow' }
const token = await p(stack.promise.create)(promise) const token = await p(local.promise.create)(promise)
assert.strictEqual(typeof token, 'string') assert.strictEqual(typeof token, 'string')
assert.ok(token.length > 42) assert.ok(token.length > 42)
@ -42,46 +41,103 @@ test('create()', async (t) => {
const contents = fs.readFileSync(file, 'utf-8') const contents = fs.readFileSync(file, 'utf-8')
assert.strictEqual(contents, JSON.stringify([[token, promise]])) assert.strictEqual(contents, JSON.stringify([[token, promise]]))
await p(stack.close)() await p(local.close)()
}) })
test('follow()', async (t) => { test('follow()', async (t) => {
const { stack, path } = setup() const { local, path } = setup()
assert.rejects(() => p(stack.promise.follow)('randomnottoken', 'MY_ID')) assert.rejects(() => p(local.promise.follow)('randomnottoken', 'MY_ID'))
const promise = { type: 'follow' } const promise = { type: 'follow' }
const token = await p(stack.promise.create)(promise) const token = await p(local.promise.create)(promise)
const file = Path.join(path, 'promises.json') const file = Path.join(path, 'promises.json')
const contentsBefore = fs.readFileSync(file, 'utf-8') const contentsBefore = fs.readFileSync(file, 'utf-8')
assert.strictEqual(contentsBefore, JSON.stringify([[token, promise]])) assert.strictEqual(contentsBefore, JSON.stringify([[token, promise]]))
const result1 = await p(stack.promise.follow)(token, 'MY_ID') const result1 = await p(local.promise.follow)(token, 'MY_ID')
assert.strictEqual(result1, true) assert.strictEqual(result1, true)
const contentsAfter = fs.readFileSync(file, 'utf-8') const contentsAfter = fs.readFileSync(file, 'utf-8')
assert.strictEqual(contentsAfter, '[]') assert.strictEqual(contentsAfter, '[]')
assert.rejects(() => p(stack.promise.follow)(token, 'MY_ID')); assert.rejects(() => p(local.promise.follow)(token, 'MY_ID'))
await p(stack.close)() await p(local.close)()
}) })
test('revoke()', async (t) => { test('identityAdd()', async (t) => {
const { stack, path } = setup() const { local, path, keypair } = setup()
const promise = { type: 'follow' } assert.rejects(() => p(local.promise.identityAdd)('randomnottoken', {}))
const token = await p(stack.promise.create)(promise)
const identity = await p(local.db.identity.findOrCreate)({
domain: 'account',
})
const promise = { type: 'identity-add', identity }
const token = await p(local.promise.create)(promise)
const file = Path.join(path, 'promises.json') const file = Path.join(path, 'promises.json')
const contentsBefore = fs.readFileSync(file, 'utf-8') const contentsBefore = fs.readFileSync(file, 'utf-8')
assert.strictEqual(contentsBefore, JSON.stringify([[token, promise]])) assert.strictEqual(contentsBefore, JSON.stringify([[token, promise]]))
await p(stack.promise.revoke)(token) const dbBefore = [...local.db.msgs()].map(({data}) => data)
assert.equal(dbBefore.length, 1)
assert.equal(dbBefore[0].action, 'add')
assert.equal(dbBefore[0].add.key.algorithm, 'ed25519')
assert.equal(dbBefore[0].add.key.bytes, keypair.public)
assert.equal(dbBefore[0].add.key.purpose, 'sig')
assert(dbBefore[0].add.nonce)
const keypair2 = Keypair.generate('ed25519', 'bob')
const consent = local.db.identity.consent({ identity, keypair: keypair2 })
const result1 = await p(local.promise.identityAdd)(token, {
key: {
purpose: 'sig',
algorithm: 'ed25519',
bytes: keypair2.public,
},
consent,
})
assert.strictEqual(result1, true)
const dbAfter = [...local.db.msgs()].map(({data}) => data)
assert.equal(dbAfter.length, 2)
assert.equal(dbAfter[0].action, 'add')
assert.equal(dbAfter[0].add.key.algorithm, 'ed25519')
assert.equal(dbAfter[0].add.key.bytes, keypair.public)
assert.equal(dbAfter[0].add.key.purpose, 'sig')
assert(dbAfter[0].add.nonce)
assert.equal(dbAfter[1].action, 'add')
assert.equal(dbAfter[1].add.key.algorithm, 'ed25519')
assert.equal(dbAfter[1].add.key.bytes, keypair2.public)
assert.equal(dbAfter[1].add.key.purpose, 'sig')
assert(dbAfter[1].add.consent)
const contentsAfter = fs.readFileSync(file, 'utf-8') const contentsAfter = fs.readFileSync(file, 'utf-8')
assert.strictEqual(contentsAfter, '[]') assert.strictEqual(contentsAfter, '[]')
await p(stack.close)() assert.rejects(() => p(local.promise.identityAdd)(token, {}))
await p(local.close)()
})
test('revoke()', async (t) => {
const { local, path } = setup()
const promise = { type: 'follow' }
const token = await p(local.promise.create)(promise)
const file = Path.join(path, 'promises.json')
const contentsBefore = fs.readFileSync(file, 'utf-8')
assert.strictEqual(contentsBefore, JSON.stringify([[token, promise]]))
await p(local.promise.revoke)(token)
const contentsAfter = fs.readFileSync(file, 'utf-8')
assert.strictEqual(contentsAfter, '[]')
await p(local.close)()
}) })