diff --git a/lib/index.js b/lib/index.js index 3fe5657..859b61e 100644 --- a/lib/index.js +++ b/lib/index.js @@ -6,7 +6,14 @@ const bs58 = require('bs58') const b4a = require('b4a') /** - * @typedef {import('ppppp-db/msg-v3').AccountAdd} AccountAdd + * @typedef {ReturnType} PPPPPDB + * @typedef {import('ppppp-db/msg-v4').AccountAdd} AccountAdd + * @typedef {Buffer | Uint8Array} B4A + * @typedef {{global: {path: string}}} ExpectedConfig + * @typedef {{global: {path?: string}}} Config + * @typedef {{type: 'follow'}} FollowPromise + * @typedef {{type: 'account-add', account: string}} AccountAddPromise + * @typedef {FollowPromise | AccountAddPromise} PPromise */ /** @@ -15,16 +22,23 @@ const b4a = require('b4a') */ /** - * @typedef {Buffer | Uint8Array} B4A + * @param {{ db: PPPPPDB | null }} peer + * @returns {asserts peer is { db: PPPPPDB }} */ +function assertDBPlugin(peer) { + // prettier-ignore + if (!peer.db) throw new Error('promise plugin plugin requires ppppp-db plugin') +} /** - * @typedef {{type: 'follow'}} FollowPromise - * @typedef {{type: 'account-add', account: string}} AccountAddPromise - * @typedef {FollowPromise | AccountAddPromise} PPromise + * @param {Config} config + * @returns {asserts config is ExpectedConfig} */ - -const FILENAME = 'promises.json' +function assertValidConfig(config) { + if (typeof config.global?.path !== 'string') { + throw new Error('promise plugin requires config.global.path') + } +} module.exports = { name: 'promise', @@ -43,11 +57,13 @@ module.exports = { }, /** - * @param {any} local - * @param {any} config + * @param {{ db: PPPPPDB | null }} peer + * @param {Config} config */ - init(local, config) { - const devicePromisesFile = Path.join(config.path, FILENAME) + init(peer, config) { + assertDBPlugin(peer) + assertValidConfig(config) + const devicePromisesFile = Path.join(config.global.path, 'promises.json') const promises = /** @type {Map} */ (new Map()) let loaded = false @@ -164,22 +180,60 @@ module.exports = { setTimeout(() => accountAdd(token, addition, cb), 100) return } + + try { + assertDBPlugin(peer) + } catch (err) { + cb(/**@type {Error}*/ (err)) + return + } + + if (!addition?.consent) { + // prettier-ignore + cb(new Error('Invalid key to be added, missing "consent": ' + JSON.stringify(addition))) + return + } + if ( !addition?.key?.purpose || !addition?.key?.algorithm || - !addition?.key?.bytes || - !addition?.consent || - addition?.key?.purpose !== 'sig' || - addition?.key?.algorithm !== 'ed25519' + !addition?.key?.bytes ) { - cb(new Error('Invalid key to be added: ' + JSON.stringify(addition))) + // prettier-ignore + cb(new Error('Invalid key to be added, missing purpose/algorithm/bytes: ' + JSON.stringify(addition))) return } + const { algorithm, purpose } = addition.key + switch (purpose) { + case 'sig': + case 'shs-and-sig': + if (algorithm !== 'ed25519') { + // prettier-ignore + cb(new Error(`Invalid key to be added, expected algorithm "ed25519" for "${purpose}": ${JSON.stringify(addition)}`)) + return + } else { + break + } + case 'external-encryption': + if (algorithm !== 'x25519-xsalsa20-poly1305') { + // prettier-ignore + cb(new Error(`Invalid key to be added, expected algorithm "x25519-xsalsa20-poly1305" for "${purpose}": ${JSON.stringify(addition)}`)) + return + } else { + break + } + default: + // prettier-ignore + cb(new Error(`Invalid key to be added, expected purpose "sig", "shs-and-sig", or "external-encryption": ${JSON.stringify(addition)}`)) + return + } + if (!promises.has(token)) { cb(new Error('Invalid token')) return } + const promise = /** @type {AccountAddPromise} */ (promises.get(token)) const { type, account } = promise if (type !== 'account-add') { @@ -187,18 +241,17 @@ module.exports = { return } - const keypair = { curve: 'ed25519', public: addition.key.bytes } - if (local.db.account.has({ account, keypair })) { + const keypair = { + curve: /**@type {const}*/ ('ed25519'), + public: addition.key.bytes, + } + if (peer.db.account.has({ account, keypair })) { cb(null, false) return } - local.db.account.add( + peer.db.account.add( { account, keypair, consent: addition.consent }, - /** - * @param {Error | null} err - * @param {any} rec - */ (err, rec) => { if (err) return cb(err) promises.delete(token) diff --git a/package.json b/package.json index ef8b0da..2f43c22 100644 --- a/package.json +++ b/package.json @@ -14,14 +14,14 @@ "files": [ "lib/**/*" ], - "engines": { - "node": ">=16" - }, "exports": { ".": { "require": "./lib/index.js" } }, + "engines": { + "node": ">=16" + }, "dependencies": { "atomic-file-rw": "~0.3.0", "b4a": "^1.6.4", @@ -38,9 +38,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", diff --git a/test/index.test.js b/test/index.test.js index 3631a91..45e459b 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -8,28 +8,33 @@ const rimraf = require('rimraf') const Keypair = require('ppppp-keypair') const caps = require('ppppp-caps') -function setup() { +async function setup() { setup.counter ??= 0 setup.counter += 1 const path = Path.join(os.tmpdir(), 'ppppp-promise-' + setup.counter) rimraf.sync(path) const keypair = Keypair.generate('ed25519', 'alice') - 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(require('ppppp-db')) .use(require('../lib')) .call(null, { - path, - keypair, + shse: { caps }, + global: { + path, + keypair, + }, }) + await local.db.loaded() + return { local, path, keypair } } test('create()', async (t) => { - const { local, path } = setup() + const { local, path } = await setup() const promise = { type: 'follow' } const token = await p(local.promise.create)(promise) @@ -45,9 +50,9 @@ test('create()', async (t) => { }) test('follow()', async (t) => { - const { local, path } = setup() + const { local, path } = await setup() - assert.rejects(() => p(local.promise.follow)('randomnottoken', 'MY_ID')) + assert.rejects(() => p(local.promise.follow)('randomnottoken', 'FRIEND_ID')) const promise = { type: 'follow' } const token = await p(local.promise.create)(promise) @@ -56,24 +61,24 @@ test('follow()', async (t) => { const contentsBefore = fs.readFileSync(file, 'utf-8') assert.strictEqual(contentsBefore, JSON.stringify([[token, promise]])) - const result1 = await p(local.promise.follow)(token, 'MY_ID') + const result1 = await p(local.promise.follow)(token, 'FRIEND_ID') assert.strictEqual(result1, true) const contentsAfter = fs.readFileSync(file, 'utf-8') assert.strictEqual(contentsAfter, '[]') - assert.rejects(() => p(local.promise.follow)(token, 'MY_ID')) + assert.rejects(() => p(local.promise.follow)(token, 'FRIEND_ID')) await p(local.close)() }) test('accountAdd()', async (t) => { - const { local, path, keypair } = setup() + const { local, path, keypair } = await setup() assert.rejects(() => p(local.promise.accountAdd)('randomnottoken', {})) const account = await p(local.db.account.findOrCreate)({ - domain: 'account', + subdomain: 'account', }) const promise = { type: 'account-add', account } @@ -86,10 +91,10 @@ test('accountAdd()', async (t) => { 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) + assert.equal(dbBefore[0].key.algorithm, 'ed25519') + assert.equal(dbBefore[0].key.bytes, keypair.public) + assert.equal(dbBefore[0].key.purpose, 'shs-and-sig') + assert(dbBefore[0].nonce) const keypair2 = Keypair.generate('ed25519', 'bob') const consent = local.db.account.consent({ account, keypair: keypair2 }) @@ -106,15 +111,15 @@ test('accountAdd()', async (t) => { 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[0].key.algorithm, 'ed25519') + assert.equal(dbAfter[0].key.bytes, keypair.public) + assert.equal(dbAfter[0].key.purpose, 'shs-and-sig') + assert(dbAfter[0].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) + assert.equal(dbAfter[1].key.algorithm, 'ed25519') + assert.equal(dbAfter[1].key.bytes, keypair2.public) + assert.equal(dbAfter[1].key.purpose, 'sig') + assert(dbAfter[1].consent) const contentsAfter = fs.readFileSync(file, 'utf-8') assert.strictEqual(contentsAfter, '[]') @@ -125,7 +130,7 @@ test('accountAdd()', async (t) => { }) test('revoke()', async (t) => { - const { local, path } = setup() + const { local, path } = await setup() const promise = { type: 'follow' } const token = await p(local.promise.create)(promise)