Skip to content

Commit

Permalink
Plugins cannot read other plugin configs
Browse files Browse the repository at this point in the history
  • Loading branch information
staltz committed Dec 26, 2023
1 parent e2bc2f4 commit d10ad2a
Show file tree
Hide file tree
Showing 16 changed files with 226 additions and 120 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# 8.0.0

- **Breaking change**:

# 7.0.0

- **Breaking change**: Node.js >=16.0.0 is now required, due to the use of new JavaScript syntax
24 changes: 13 additions & 11 deletions PLUGINS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ like Secure-Scuttlebutt. It is highly extensible via plugins.
Plugins are simply NodeJS modules that export an `object` of form `{ name, version, manifest, init }`.

```js
// bluetooth-plugin.js
// bluetooth-plugin.js

module.exports = {
name: 'bluetooth',
Expand All @@ -20,10 +20,12 @@ module.exports = {
init: (api, opts) => {
// .. do things

// In opts, only opts.bluetooth and opts.global are available

// return things promised by the manifest:
return {
localPeers, // an async function (takes a callback)
updates // a function which returns a pull-stream source
updates // a function which returns a pull-stream source
}
}
}
Expand All @@ -37,20 +39,20 @@ method.

var SecretStack = require('secret-stack')

var App = SecretStack({ appKey: '1KHLiKZvAvjbY1ziZEHMXawbCEIM6qwjCDm3VYRan/s=' })
var App = SecretStack({ global: { appKey: '1KHLiKZvAvjbY1ziZEHMXawbCEIM6qwjCDm3VYRan/s=' } })
.use(require('./bluetooth-plugin'))

var app = App()
```

The plugin has now been mounted on the `secret-stack` instance and
methods exposed by the plugin can be accessed at `app.pluginName.methodName`
(e.g. `app.bluetooth.updates`
(e.g. `app.bluetooth.updates`)

---

Plugins can be used to for a number of different use cases, like adding
a persistent underlying database ([ssb-db](https://github.com/ssbc/ssb-db'))
a persistent underlying database ([ssb-db](https://github.com/ssbc/ssb-db'))
or layering indexes on top of the underlying store ([ssb-links](https://github.com/ssbc/ssb-links)).

It becomes very easy to lump a bunch of plugins together and create a
Expand All @@ -60,7 +62,7 @@ more sophisticated application.
var SecretStack = require('secret-stack')
var config = require('./some-config-file')

var Server = SecretStack({ appKey: '1KHLiKZvAvjbY1ziZEHMXawbCEIM6qwjCDm3VYRan/s=' })
var Server = SecretStack({ global: { appKey: '1KHLiKZvAvjbY1ziZEHMXawbCEIM6qwjCDm3VYRan/s=' } })
.use(require('ssb-db')) // added persistent log storage
.use(require('ssb-gossip')) // added peer gossip capabilities
.use(require('ssb-replicate')) // can now replicate other logs with peers
Expand All @@ -69,7 +71,7 @@ var Server = SecretStack({ appKey: '1KHLiKZvAvjbY1ziZEHMXawbCEIM6qwjCDm3VYRan/s=
var server = Server(config) // start application
```

## Plugin Format
## Plugin Format

A valid plugin is an `Object` of form `{ name, version, manifest, init }`

Expand Down Expand Up @@ -99,13 +101,13 @@ of plugins will be called in the order they were registered with `use`.

The `init` function of a plugin will be passed:
- `api` - _Object_ the secret-stack app so far
- `opts` - the merge of the default-config secret-stack factory (App) was created with and the config the app was initialised with (app).
- `opts` - configurations available to this plugin are `opts.global` and `opts[plugin.name]`
- `permissions` - _Object_ the permissions so far
- `manifest` - _Object_ the manifest so far

If `plugin.name` is a string, then the return value of init is mounted like `api[plugin.name] = plugin.init(api, opts)`

(If there's no `plugin.name` then the results of `init` are merged directly withe the `api` object!)
(If there's no `plugin.name` then the results of `init` are merged directly with the `api` object!)

Note, each method on the api gets wrapped with [hoox](https://github.com/dominictarr/hoox)
so that plugins may intercept that function.
Expand All @@ -124,7 +126,7 @@ Any permissions provided will be merged into the main permissions,
prefixed with the plugin name.

e.g. In this case we're giving anyone access to `api.bluetooth.localPeers`,
and the permission would be listed `'bluetooth.localPeers'`
and the permission would be listed `'bluetooth.localPeers'`

```js
module.exports = {
Expand All @@ -143,7 +145,7 @@ module.exports = {
// return things promised by the manifest:
return {
localPeers, // an async function (takes a callback)
updates // a function which returns a pull-stream source
updates // a function which returns a pull-stream source
}
}
}
Expand Down
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ var databasePlugin = require('./some-database')
var bluetoothPlugin = require('./bluetooth')
var config = require('./some-config')

var App = SecretStack({ appKey: '1KHLiKZvAvjbY1ziZEHMXawbCEIM6qwjCDm3VYRan/s=' })
var App = SecretStack({ global: { appKey: '1KHLiKZvAvjbY1ziZEHMXawbCEIM6qwjCDm3VYRan/s=' } })
.use(databasePlugin)
.use(bluetoothPlugin)

Expand Down Expand Up @@ -57,11 +57,12 @@ Returns the App (with plugin now installed)

Start the app and returns an EventEmitter with methods (core and plugin) attached.

`config` is an (optional) Object with any properties:
- `keys` - _String_ a sodium ed25519 key pair
- ... - (optional)
`config` is an (optional) Object with:
- `config.global` - an object containing data available for all plugins
- `config.global.keys` - _String_ a sodium ed25519 key pair
- `config[pluginName]` - an object containing data only available to the plugin with name `pluginName`

`config` will be passed to each plugin as they're initialised (as `merge(opts, config)` which opts were those options `SecretStack` factory was initialised with).
`config` will be passed to each plugin as they're initialised (as `merge(opts, config)` which opts were those options `SecretStack` factory was initialised with), with only `config.global` and `config[pluginName]` available to each plugin.

This `app` as an EventEmitter emits the following events:

Expand Down
26 changes: 22 additions & 4 deletions lib/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ function merge (a, b, mapper) {
!(b[k] instanceof Uint8Array) &&
!Array.isArray(b[k])
) {
a[k] = {}
a[k] ??= {}
merge(a[k], b[k], mapper)
} else {
a[k] = mapper(b[k], k)
Expand All @@ -36,6 +36,19 @@ function merge (a, b, mapper) {
return a
}

/**
* @param {Record<string, any>} obj
* @param {{name?: string}} plugin
*/
function pluckOpts (obj, plugin) {
if (plugin.name) {
const camelCaseName = /** @type {string} */ (u.toCamelCase(plugin.name))
return { [camelCaseName]: obj[camelCaseName], global: obj.global ?? {} }
} else {
return { global: obj.global ?? {} }
}
}

/**
* @param {Array<any>} plugins
* @param {any} defaultConfig
Expand All @@ -48,11 +61,12 @@ function Api (plugins, defaultConfig) {
const opts = merge(merge({}, defaultConfig), inputOpts)
// change event emitter to something with more rigorous security?
let api = new EventEmitter()
create.plugins.forEach((plug) => {
for (const plug of create.plugins) {
const subOpts = pluckOpts(opts, plug)
let _api = plug.init.call(
{},
api,
opts,
subOpts,
create.permissions,
create.manifest
)
Expand Down Expand Up @@ -82,7 +96,7 @@ function Api (plugins, defaultConfig) {
return val
}
)
})
}
return api
}

Expand Down Expand Up @@ -110,6 +124,10 @@ function Api (plugins, defaultConfig) {
}

if (plug.name && typeof plug.name === 'string') {
if (plug.name === 'global') {
console.error('plugin named "global" is reserved, skipping')
return create
}
const found = create.plugins.some((p) => p.name === plug.name)
if (found) {
// prettier-ignore
Expand Down
42 changes: 21 additions & 21 deletions lib/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -151,26 +151,26 @@ module.exports = {
init (api, opts, permissions, manifest) {
/** @type {number} */
let timeoutInactivity
if (opts.timers?.inactivity && u.isNumber(opts.timers?.inactivity)) {
timeoutInactivity = opts.timers?.inactivity
if (u.isNumber(opts.global.timers?.inactivity)) {
timeoutInactivity = /** @type {number} */ (opts.global.timers?.inactivity)
}
// if opts.timers are set, pick a longer default
// but if not, set a short default (as needed in the tests)
timeoutInactivity ??= opts.timers ? 600e3 : 5e3
timeoutInactivity ??= opts.global.timers ? 600e3 : 5e3

if (!opts.connections) {
if (!opts.global.connections) {
/** @type {Incoming} */
const netIn = {
scope: ['device', 'local', 'public'],
transform: 'shs',
...(opts.host ? { host: opts.host } : null),
...(opts.port ? { port: opts.port } : null)
...(opts.global.host ? { host: opts.global.host } : null),
...(opts.global.port ? { port: opts.global.port } : null)
}
/** @type {Outgoing} */
const netOut = {
transform: 'shs'
}
opts.connections = {
opts.global.connections = {
incoming: {
net: [netIn]
},
Expand Down Expand Up @@ -208,10 +208,10 @@ module.exports = {
/** @type {Array<[unknown, unknown]>} */
const clientSuites = []

for (const incTransport in opts.connections?.incoming) {
opts.connections.incoming[incTransport].forEach((inc) => {
transforms.forEach((transform) => {
transports.forEach((transport) => {
for (const incTransport in opts.global.connections?.incoming) {
for (const inc of opts.global.connections.incoming[incTransport]) {
for (const transform of transforms) {
for (const transport of transports) {
if (
transport.name === incTransport &&
transform.name === inc.transform
Expand All @@ -226,15 +226,15 @@ module.exports = {
debug('creating server %s %s host=%s port=%d scope=%s', incTransport, transform.name, inc.host, inc.port, inc.scope ?? 'undefined')
serverSuites.push([msPlugin, msTransformPlugin])
}
})
})
})
}
}
}
}

for (const outTransport in opts.connections?.outgoing) {
opts.connections.outgoing[outTransport].forEach((out) => {
transforms.forEach((transform) => {
transports.forEach((transport) => {
for (const outTransport in opts.global.connections?.outgoing) {
for (const out of opts.global.connections.outgoing[outTransport]) {
for (const transform of transforms) {
for (const transport of transports) {
if (
transport.name === outTransport &&
transform.name === out.transform
Expand All @@ -243,9 +243,9 @@ module.exports = {
const msTransformPlugin = transform.create()
clientSuites.push([msPlugin, msTransformPlugin])
}
})
})
})
}
}
}
}

msClient = MultiServer(clientSuites)
Expand Down
21 changes: 11 additions & 10 deletions lib/plugins/shs.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ function toBuffer (base64) {

/**
*
* @param {NonNullable<Config['keys']>} keys
* @param {NonNullable<Config['global']['keys']>} keys
* @returns
*/
function toSodiumKeys (keys) {
Expand All @@ -37,30 +37,31 @@ module.exports = {

/**
* @param {any} api
* @param {Config} config
* @param {Config & {multiserverShs?: {cap?: string; seed?: Buffer}}} config
*/
init (api, config) {
/** @type {number | undefined} */
let timeoutHandshake
if (u.isNumber(config.timers?.handshake)) {
timeoutHandshake = config.timers?.handshake
if (u.isNumber(config.global.timers?.handshake)) {
timeoutHandshake = config.global.timers?.handshake
}
if (!timeoutHandshake) {
timeoutHandshake = config.timers ? 15e3 : 5e3
timeoutHandshake = config.global.timers ? 15e3 : 5e3
}
// set all timeouts to one setting, needed in the tests.
if (config.timeout) {
timeoutHandshake = config.timeout
if (config.global.timeout) {
timeoutHandshake = config.global.timeout
}

const shsCap = (config.caps && config.caps.shs) ?? config.appKey
const shsCap = config.multiserverShs?.cap ?? config.global.caps?.shs ?? config.global.appKey
if (!shsCap) {
throw new Error('secret-stack/plugins/shs must have caps.shs configured')
}
const seed = config.multiserverShs?.seed ?? config.global.seed

const shs = Shs({
keys: config.keys && toSodiumKeys(config.keys),
seed: config.seed,
keys: config.global.keys && toSodiumKeys(config.global.keys),
seed,
appKey: toBuffer(shsCap),
timeout: timeoutHandshake,

Expand Down
48 changes: 25 additions & 23 deletions lib/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,30 +27,32 @@
* }} Transform
*
* @typedef {{
* caps?: {
* shs?: Buffer | string;
* };
* appKey?: Buffer | string;
* keys?: {
* public?: string;
* private?: string;
* id?: string;
* };
* seed?: unknown;
* connections?: {
* incoming?: {
* [name: string]: Array<Incoming>;
* global: {
* caps?: {
* shs?: Buffer | string;
* };
* outgoing?: {
* [name: string]: Array<Outgoing>;
* appKey?: Buffer | string;
* keys?: {
* public?: string;
* private?: string;
* id?: string;
* };
* };
* timeout?: number;
* timers?: {
* handshake?: number;
* inactivity?: number;
* };
* host?: string;
* port?: number;
* seed?: unknown;
* host?: string;
* port?: number;
* connections?: {
* incoming?: {
* [name: string]: Array<Incoming>;
* };
* outgoing?: {
* [name: string]: Array<Outgoing>;
* };
* };
* timeout?: number;
* timers?: {
* handshake?: number;
* inactivity?: number;
* };
* }
* }} Config
*/
Loading

0 comments on commit d10ad2a

Please sign in to comment.