Skip to content

Commit

Permalink
Merge pull request #7 from ssbc/nicer_api_no_break
Browse files Browse the repository at this point in the history
tweak api, add linting
  • Loading branch information
mixmix authored Dec 10, 2023
2 parents e919467 + 1d39893 commit 813a2a0
Show file tree
Hide file tree
Showing 7 changed files with 145 additions and 110 deletions.
59 changes: 33 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ const caps = require('ssb-caps')


const stack = Stack({ caps })
.use(require('ssb-db')) // << required
.use(require('ssb-profile'))
.use(require('ssb-d2'))
.use(require('ssb-recps-guard')) // << must be last

const config = {
Expand All @@ -28,20 +27,24 @@ const sever = stack(config)

auto-blocked:
```js
const unallowedMsg = { type: 'profile' }
const unallowedMsg = {
content: { type: 'profile' }
}

server.publish(unallowedMsg, (err, msg) => {
server.db.create(unallowedMsg, (err, msg) => {
console.log(err)
// => Error: recps-guard - no accidental public messages allowed!
})
```

config-allowed:
```js
const allowedMsg = { type: 'contact' }
const allowedMsg = {
content: { type: 'contact' }
}
// this type was allowed in our config (see above)

server.publish(allowedType, (err, msg) => {
server.db.create(allowedType, (err, msg) => {
console.log(msg.value.content)
// => { type: 'contact' }
})
Expand All @@ -50,39 +53,27 @@ server.publish(allowedType, (err, msg) => {
explictly public:
```js
const explicitPublicMsg = {
content: { type: 'profile' },
options: { allowPublic: true }
}

server.publish(explicitPublicMsg, (err, msg) => {
console.log(msg.value.content)
// => { type: 'profile' }

// NOTE: only `content` is published
})

// with ssb-db2's ssb.db.create, note different option format!
const explicitPublicMsgDb2 = {
content: { type: 'profile' },
allowPublic: true
}

server.db.create(explicitPublicMsgDb2, (err, msg) => {
server.db.create(explicitPublicMsg, (err, msg) => {
console.log(msg.value.content)
// => { type: 'profile' }

// NOTE: only `content` is published (as usual)
})
```


private:
```js
const privateMsg = {
type: 'profile'
recps: ['@ye+QM09iPcDJD6YvQYjoQc7sLF/IFhmNbEqgdzQo3lQ=.ed25519']
content: {
type: 'profile'
recps: ['@ye+QM09iPcDJD6YvQYjoQc7sLF/IFhmNbEqgdzQo3lQ=.ed25519']
}
}

server.publish(privateMsg, (err, msg) => {
server.db.create(privateMsg, (err, msg) => {
console.log(msg.value.content)
// => VayTFa.....yZ3Wqsg==.box

Expand All @@ -91,6 +82,21 @@ server.publish(privateMsg, (err, msg) => {
})
```

NOTE that if you are using _classic_ `ssb-db`, the API behaves the same:

```js
const explicitPublicMsgDB1 = {
content: { type: 'profile' },
allowPublic: true
}

server.db.create(explicitPublicMsgDN!, (err, msg) => {
console.log(msg.value.content)
// => { type: 'profile' }
})
```


## Installation

Because `ssb-recps-guard` hooks the publish method you **must install it as the LAST plugin**
Expand Down Expand Up @@ -118,14 +124,15 @@ where `allowedTypes` is an Array of message types which are allowed to be publis
## Explicit bypass

Messages which would normally be blocked by the guard bypass the guard by changing what's passed to the
publish method to be of form `{ content, options: { allowPublic: true } }`
publish method to be of form `{ content, allowPublic: true }`

The `content` is what will be passed to the normal publish function.

Design: this is deliberately verbose to avoid accidental publishing.
It also has the benefit that if `ssb-guard-recps` isn't installed this publish will error because publish
will expect the `type` to be in a different place.


## API

You can check if `ssb-recps-guard` is installed in your server by looking to
Expand Down
78 changes: 44 additions & 34 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
/* eslint-disable brace-style */
const get = require('lodash.get')

const isString = (t) => (typeof t === 'string')
const NotBothError = () => new Error(
'recps-guard: should not have recps && allowPublic, check your code'
)
const NotAllowedTypeError = (type) => new Error(
`recps-guard: public messages of type "${type}" not allowed`
)

let warnings = 0

module.exports = {
name: 'recpsGuard',
Expand All @@ -9,61 +19,61 @@ module.exports = {
},
init (ssb, config) {
const allowedTypes = getAllowedTypes(ssb, config)
const isAllowedType = (type) => allowedTypes.has(type)

const publishHook = (publish, args) => {
function publishHook (publish, args) {
const [input, cb] = args

if (get(input, ['options', 'allowPublic']) === true) {
// allowPublic and has recps, disallowed
if (hasRecps(input.content)) {
return cb(new Error('recps-guard: should not have recps && allowPublic, check your code'))
}
const modernAllow = input.allowPublic === true
const legacyAllow = get(input, ['options', 'allowPublic']) === true
if (legacyAllow && warnings < 5) {
console.trace('input.options.allowPublic is deprecated, please use input.allowPublic')
warnings++
}

// allowPublic and no recps, allowed
return publish(input.content, cb)
} else {
// without allowPublic, content isn't nested with db1 publish
const isExplictAllow = (modernAllow || legacyAllow)

if (isExplictAllow) {
const content = input.content

if (hasRecps(content)) cb(NotBothError())
else publish(content, cb)
}
else {
const content = input

// no allowPublic and has recps/can publish without recps, allowed
if (
isString(content) ||
const isAllowed = (
isString(content) || // already encrypted
hasRecps(content) ||
allowedTypes.has(content.type)
) return publish(content, cb)
isAllowedType(content.type)
)

// no allowPublic and no recps, disallowed
return cb(new Error(`recps-guard: public messages of type "${content.type}" not allowed`))
if (isAllowed) publish(content, cb)
else cb(NotAllowedTypeError(content.type))
}
}

const createHook = (create, args) => {
function createHook (create, args) {
const [input, cb] = args

if (input.allowPublic === true) {
// allowPublic and has recps, disallowed
if (hasRecps(input.content)) {
return cb(new Error('recps-guard: should not have recps && allowPublic, check your code'))
}
if (hasRecps(input.content)) return cb(NotBothError())

// allowPublic and no recps, allowed
return create(input, cb)
} else {
// without allowPublic, content isn't nested with db1 publish
}
else {
const content = input.content

// no allowPublic and has recps/can publish without recps, allowed
if (
input.encryptionFormat ||
isString(content) ||
const isAllowed = (
isString(content) || // already encrypted
input.encryptionFormat || // signed up for encryption
hasRecps(content) ||
allowedTypes.has(content.type)
) return create(input, cb)
isAllowedType(content.type)
)

// no allowPublic and no recps, disallowed
return cb(new Error(`recps-guard: public messages of type "${content.type}" not allowed`))
if (isAllowed) create(input, cb)
else cb(NotAllowedTypeError(content.type))
}

}

if (ssb.publish) {
Expand Down
13 changes: 8 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
"description": "guards against unencrypted messages being accidentally published!",
"main": "index.js",
"scripts": {
"test": "tape test/**/*.test.js | tap-spec"
"test": "npm run test:js && npm run lint",
"test:js": "tape test/**/*.test.js | tap-arc",
"lint": "standard --fix"
},
"repository": {
"type": "git",
Expand All @@ -21,6 +23,9 @@
"url": "https://github.com/ssbc/ssb-recps-guard/issues"
},
"homepage": "https://github.com/ssbc/ssb-recps-guard#readme",
"dependencies": {
"lodash.get": "^4.4.2"
},
"devDependencies": {
"scuttle-testbot": "^2.2.0",
"ssb-box": "^1.0.1",
Expand All @@ -30,10 +35,8 @@
"ssb-db2": "^8.1.0",
"ssb-private1": "^1.0.1",
"ssb-tribes": "^4.0.0",
"tap-spec": "^5.0.0",
"standard": "^17.1.0",
"tap-arc": "^1.2.2",
"tape": "^5.7.2"
},
"dependencies": {
"lodash.get": "^4.4.2"
}
}
89 changes: 52 additions & 37 deletions test/core-tests.js
Original file line number Diff line number Diff line change
@@ -1,39 +1,54 @@
module.exports = (server, t, cb) => {
const msg = { type: 'profile' }
server.publish(msg, (err, data) => {
t.match(err.message, /recps-guard: public messages of type "profile" not allowed/, 'public blocked')

const msg = { type: 'profile', recps: [server.id] }
server.publish(msg, (err, data) => {
t.error(err, 'msgs with recps allowed')
t.equal(typeof data.value.content, 'string', '(msg content encrypted)')

const msg = Buffer.from('cats are cool').toString('base64') + '.box7'
server.publish(msg, (err, data) => {
t.error(err, 'pre-encrypted content published fine')
t.equal(typeof data.value.content, 'string', '(msg content encrypted)')

const content = { type: 'profile', name: 'mix' }
server.publish({ content, options: { allowPublic: true } }, (err, data) => {
if (err) return cb(err)
t.error(err, 'msgs { content, options: { allowPublic: true } allowed')
t.deepEqual(data.value.content, content, '(msg content unencrypted, allowPublic pruned)')

const weird = {
content: { type: 'profile', recps: [server.id] },
options: { allowPublic: true }
}
server.publish(weird, (err, data) => {
t.match(
err.message,
/recps-guard: should not have recps && allowPublic, check your code/,
'disallow recps AND allowPublic'
)

cb(null)
})
})
})
const { promisify: p } = require('util')

module.exports = async (server, t, cb) => {
let description, content, input

description = 'public blocked'
content = { type: 'profile' }
await p(server.publish)(content)
.then(() => t.fail(description))
.catch(err => {
t.match(err.message, /recps-guard: public messages of type "profile" not allowed/, description)
})
})

description = 'msgs with recps allowed'
content = { type: 'profile', recps: [server.id] }
await p(server.publish)(content)
.then(data => t.equal(typeof data.value.content, 'string', description))
.catch(err => t.error(err, description))

description = 'pre-encrypted content published fine'
content = Buffer.from('cats are cool').toString('base64') + '.box7'
await p(server.publish)(content)
.then(data => t.equal(typeof data.value.content, 'string', description))
.catch(err => t.fail(err, description))

description = 'msgs { content, allowPublic: true } allowed'
content = { type: 'profile', name: 'mix' }
input = { content, allowPublic: true }
await p(server.publish)(input)
.then(data => t.deepEqual(data.value.content, content, description))
.catch(err => t.fail(err, description))

description = 'legacy: msgs { content, options: { allowPublic: true } }'
content = { type: 'profile', name: 'mix' }
input = { content, options: { allowPublic: true } }
await p(server.publish)(input)
.then(data => t.deepEqual(data.value.content, content, description))
.catch(err => t.fail(err, description))

description = 'disallow recps AND allowPublic'
input = {
content: { type: 'profile', recps: [server.id] },
allowPublic: true
}
await p(server.publish)(input)
.then(() => t.fail(description))
.catch(err => t.match(
err.message,
/recps-guard: should not have recps && allowPublic, check your code/,
'disallow recps AND allowPublic'
))

cb(null)
}
Loading

0 comments on commit 813a2a0

Please sign in to comment.