mirror of https://codeberg.org/pzp/pzp-promise.git
add identityAdd API
This commit is contained in:
parent
99a1570c5a
commit
d70d119e7c
79
lib/index.js
79
lib/index.js
|
@ -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 }
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -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": {
|
||||||
|
|
|
@ -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)()
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in New Issue