From d70d119e7ccff7bf282ff3b69aa7f24ee980c9ba Mon Sep 17 00:00:00 2001 From: Andre Staltz Date: Tue, 18 Jul 2023 10:39:54 +0300 Subject: [PATCH] add identityAdd API --- lib/index.js | 79 +++++++++++++++++++++++++++++++++++--- package.json | 5 ++- test/index.test.js | 96 ++++++++++++++++++++++++++++++++++++---------- 3 files changed, 152 insertions(+), 28 deletions(-) diff --git a/lib/index.js b/lib/index.js index d19507a..753fdb6 100644 --- a/lib/index.js +++ b/lib/index.js @@ -5,6 +5,10 @@ const crypto = require('node:crypto') const bs58 = require('bs58') const b4a = require('b4a') +/** + * @typedef {import('ppppp-db/msg-v3').IdentityAdd} IdentityAdd + */ + /** * @template T * @typedef {(...args: [NodeJS.ErrnoException] | [null, T]) => void} CB @@ -16,7 +20,8 @@ const b4a = require('b4a') /** * @typedef {{type: 'follow'}} FollowPromise - * @typedef {FollowPromise} PPromise + * @typedef {{type: 'identity-add', identity: string}} IdentityAddPromise + * @typedef {FollowPromise | IdentityAddPromise} PPromise */ const FILENAME = 'promises.json' @@ -29,18 +34,19 @@ module.exports = { revoke: 'async', // promises follow: 'async', + identityAdd: 'async', }, permissions: { anonymous: { - allow: ['follow'], + allow: ['follow', 'identityAdd'], }, }, /** - * @param {any} sstack + * @param {any} local * @param {any} config */ - init(sstack, config) { + init(local, config) { const devicePromisesFile = Path.join(config.path, FILENAME) const promises = /** @type {Map} */ (new Map()) @@ -79,10 +85,17 @@ module.exports = { if ( typeof promise !== 'object' || typeof promise.type !== 'string' || - promise.type !== 'follow' + (promise.type !== 'follow' && promise.type !== 'identity-add') ) { 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 } @@ -141,6 +154,60 @@ module.exports = { }) } + /** + * @param {string} token + * @param {IdentityAdd} addition + * @param {CB} 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 {CB} cb @@ -155,6 +222,6 @@ module.exports = { save(cb) } - return { create, revoke, follow } + return { create, revoke, follow, identityAdd } }, } diff --git a/package.json b/package.json index 573116e..10f6108 100644 --- a/package.json +++ b/package.json @@ -33,12 +33,13 @@ "c8": "^7.11.0", "husky": "^4.3.0", "ppppp-caps": "github:staltz/ppppp-caps", + "ppppp-db": "github:staltz/ppppp-db", "ppppp-keypair": "github:staltz/ppppp-keypair", "prettier": "^2.6.2", "pretty-quick": "^3.1.3", "rimraf": "^5.0.1", - "secret-handshake-ext": "~0.0.6", - "secret-stack": "^6.4.2", + "secret-handshake-ext": "~0.0.8", + "secret-stack": "ssbc/secret-stack#bare-mode", "typescript": "^5.0.2" }, "scripts": { diff --git a/test/index.test.js b/test/index.test.js index 4a18736..a206875 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -15,25 +15,24 @@ function setup() { rimraf.sync(path) const keypair = Keypair.generate('ed25519', 'alice') - const stack = require('secret-stack/lib/api')([], {}) - .use(require('secret-stack/lib/core')) - .use(require('secret-stack/lib/plugins/net')) + const local = require('secret-stack/bare')({ caps }) + .use(require('secret-stack/plugins/net')) .use(require('secret-handshake-ext/secret-stack')) + .use(require('ppppp-db')) .use(require('../lib')) .call(null, { path, - caps, keypair, }) - return { stack, path, keypair } + return { local, path, keypair } } test('create()', async (t) => { - const { stack, path } = setup() + const { local, path } = setup() 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.ok(token.length > 42) @@ -42,46 +41,103 @@ test('create()', async (t) => { const contents = fs.readFileSync(file, 'utf-8') assert.strictEqual(contents, JSON.stringify([[token, promise]])) - await p(stack.close)() + await p(local.close)() }) 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 token = await p(stack.promise.create)(promise) + 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]])) - const result1 = await p(stack.promise.follow)(token, 'MY_ID') + const result1 = await p(local.promise.follow)(token, 'MY_ID') assert.strictEqual(result1, true) const contentsAfter = fs.readFileSync(file, 'utf-8') 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) => { - const { stack, path } = setup() +test('identityAdd()', async (t) => { + const { local, path, keypair } = setup() - const promise = { type: 'follow' } - const token = await p(stack.promise.create)(promise) + assert.rejects(() => p(local.promise.identityAdd)('randomnottoken', {})) + + 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 contentsBefore = fs.readFileSync(file, 'utf-8') 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') 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)() })