From d10ad2a3dfb20ad9daf34587adcb7eeebc666636 Mon Sep 17 00:00:00 2001 From: Andre Staltz Date: Tue, 26 Dec 2023 15:47:07 +0200 Subject: [PATCH 1/3] Plugins cannot read other plugin configs --- CHANGELOG.md | 4 ++++ PLUGINS.md | 24 ++++++++++---------- README.md | 11 +++++----- lib/api.js | 26 ++++++++++++++++++---- lib/core.js | 42 +++++++++++++++++------------------ lib/plugins/shs.js | 21 +++++++++--------- lib/types.js | 48 +++++++++++++++++++++------------------- test/api.js | 55 ++++++++++++++++++++++++++++++++++++++++++---- test/app-key.js | 23 ++++++++++++++----- test/auth.js | 16 ++++++++++---- test/auth2.js | 16 ++++++++++---- test/close.js | 9 ++++---- test/flood.js | 6 +++-- test/local.js | 12 +++++----- test/server.js | 21 ++++++++++-------- test/timeout.js | 12 +++++----- 16 files changed, 226 insertions(+), 120 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9361964..0df3ae6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/PLUGINS.md b/PLUGINS.md index af8835f..2c11c63 100644 --- a/PLUGINS.md +++ b/PLUGINS.md @@ -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', @@ -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 } } } @@ -37,7 +39,7 @@ 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() @@ -45,12 +47,12 @@ 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 @@ -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 @@ -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 }` @@ -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. @@ -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 = { @@ -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 } } } diff --git a/README.md b/README.md index 7c1ec3b..937455e 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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: diff --git a/lib/api.js b/lib/api.js index 4f46dc8..e1625b8 100644 --- a/lib/api.js +++ b/lib/api.js @@ -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) @@ -36,6 +36,19 @@ function merge (a, b, mapper) { return a } +/** + * @param {Record} 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} plugins * @param {any} defaultConfig @@ -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 ) @@ -82,7 +96,7 @@ function Api (plugins, defaultConfig) { return val } ) - }) + } return api } @@ -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 diff --git a/lib/core.js b/lib/core.js index 303936f..04e9599 100644 --- a/lib/core.js +++ b/lib/core.js @@ -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] }, @@ -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 @@ -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 @@ -243,9 +243,9 @@ module.exports = { const msTransformPlugin = transform.create() clientSuites.push([msPlugin, msTransformPlugin]) } - }) - }) - }) + } + } + } } msClient = MultiServer(clientSuites) diff --git a/lib/plugins/shs.js b/lib/plugins/shs.js index 99a5d4c..e010bc5 100644 --- a/lib/plugins/shs.js +++ b/lib/plugins/shs.js @@ -18,7 +18,7 @@ function toBuffer (base64) { /** * - * @param {NonNullable} keys + * @param {NonNullable} keys * @returns */ function toSodiumKeys (keys) { @@ -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, diff --git a/lib/types.js b/lib/types.js index 487b766..1b909fe 100644 --- a/lib/types.js +++ b/lib/types.js @@ -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; + * global: { + * caps?: { + * shs?: Buffer | string; * }; - * outgoing?: { - * [name: string]: Array; + * 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; + * }; + * outgoing?: { + * [name: string]: Array; + * }; + * }; + * timeout?: number; + * timers?: { + * handshake?: number; + * inactivity?: number; + * }; + * } * }} Config */ diff --git a/test/api.js b/test/api.js index d6ae525..0d0a0b8 100644 --- a/test/api.js +++ b/test/api.js @@ -4,7 +4,7 @@ var Api = require('../lib/api') tape('add a core api + a plugin', function (t) { var Create = Api([{ init: function (api, opts) { - t.deepEqual(opts, { okay: true }) + t.deepEqual(opts, { global: { okay: true } }) return { hello: function (name) { return 'Hello, ' + name + '.' @@ -13,13 +13,13 @@ tape('add a core api + a plugin', function (t) { } }]) - var api = Create({ okay: true }) + var api = Create({ global: { okay: true } }) t.equal(api.hello('Foo'), 'Hello, Foo.') Create.use({ init: function (api, opts) { - t.deepEqual(opts, { okay: true }) + t.deepEqual(opts, { global: { okay: true } }) api.hello.hook(function (greet, args) { var value = greet(args[0]) return value.substring(0, value.length - 1) + '!!!' @@ -27,7 +27,7 @@ tape('add a core api + a plugin', function (t) { } }) - var api2 = Create({ okay: true }) + var api2 = Create({ global: { okay: true } }) t.equal(api2.hello('Foo'), 'Hello, Foo!!!') t.end() }) @@ -99,6 +99,53 @@ tape('camel-case plugin', function (t) { t.end() }) +tape('plugin cannot read other plugin config', function (t) { + t.plan(2) + // core, not a plugin. + var Create = Api([{ + init: () => {} + }]) + + Create.use({ + name: 'foo', + init(api, config) { + t.deepEqual(config.foo, { x: 10 }) + t.notOk(config.bar) + return { } + } + }) + + Create({ + foo: { x: 10 }, + bar: { y: 20 } + }) +}) + +tape('plugin cannot be named global', function (t) { + // core, not a plugin. + var Create = Api([{ + manifest: {}, + init: function (api) { + return {} + } + }]) + + const consoleError = console.error + let called = false + console.error = function (msg) { + t.equal(msg, 'plugin named "global" is reserved, skipping', 'global warn') + called = true + } + Create.use({ + name: 'global', + init: function () { } + }) + t.equal(called, true, 'console.error was called') + console.error = consoleError + + t.end() +}) + tape('compound (array) plugins', function (t) { // core, not a plugin. var Create = Api([{ diff --git a/test/app-key.js b/test/app-key.js index 9cd9527..7c205ff 100644 --- a/test/app-key.js +++ b/test/app-key.js @@ -48,15 +48,24 @@ create.use({ }) var alice = create({ - seed: seeds.alice, caps: { shs: appkey1 } + multiserverShs: { + seed: seeds.alice, + cap: appkey1 + } }) var bob = create({ - seed: seeds.bob, caps: { shs: appkey1 } + multiserverShs: { + seed: seeds.bob, + cap: appkey1 + } }) var carol = create({ - seed: seeds.carol, caps: { shs: appkey1 } + multiserverShs: { + seed: seeds.carol, + cap: appkey1 + } }) tape('alice *can* use alice_only api', function (t) { @@ -81,11 +90,15 @@ tape('carol *cannot* use alice_only api', function (t) { }) var antialice = create({ - seed: seeds.alice, appKey: appkey2 + global: { + seed: seeds.alice, appKey: appkey2 + } }) var antibob = create({ - seed: seeds.bob, appKey: appkey2 + global: { + seed: seeds.bob, appKey: appkey2 + } }) tape('antialice cannot connect to alice because they use different appkeys', function (t) { diff --git a/test/auth.js b/test/auth.js index af133a2..ece8d77 100644 --- a/test/auth.js +++ b/test/auth.js @@ -11,7 +11,9 @@ function hash (s) { var appkey = hash('test_key') var create = SecretStack({ - appKey: appkey + global: { + appKey: appkey + } }) create.use({ @@ -46,15 +48,21 @@ create.use({ }) var alice = create({ - seed: seeds.alice + multiserverShs: { + seed: seeds.alice + } }) var bob = create({ - seed: seeds.bob + multiserverShs: { + seed: seeds.bob + } }) var carol = create({ - seed: seeds.carol + multiserverShs: { + seed: seeds.carol + } }) tape('alice *can* use alice_only api', function (t) { diff --git a/test/auth2.js b/test/auth2.js index ca0a0a4..ef2c0ed 100644 --- a/test/auth2.js +++ b/test/auth2.js @@ -18,7 +18,9 @@ function hash (s) { var appkey = hash('test_key') var create = SecretStack({ - appKey: appkey + global: { + appKey: appkey + } }) create.use({ @@ -54,15 +56,21 @@ create.use({ }) var alice = create({ - keys: keys.alice + global: { + keys: keys.alice + } }) var bob = create({ - keys: keys.bob + global: { + keys: keys.bob + } }) var carol = create({ - keys: keys.carol + global: { + keys: keys.carol + } }) tape('bob has address', function (t) { diff --git a/test/close.js b/test/close.js index 57e4948..d616d90 100644 --- a/test/close.js +++ b/test/close.js @@ -8,10 +8,9 @@ function hash (s) { } var create = SecretStack({ - appKey: hash('test_flood'), - permissions: { - anonymous: { allow: null } - } + global: { + appKey: hash('test_flood'), + }, }) .use({ manifest: { @@ -26,7 +25,7 @@ var create = SecretStack({ } }) function createPeer (name) { - var alice = create({ seed: seeds[name] }) + var alice = create({ multiserverShs: { seed: seeds[name] } }) return alice.on('flood:message', function (msg) { console.log(name, 'received', msg) }) diff --git a/test/flood.js b/test/flood.js index 644ff87..1e03030 100644 --- a/test/flood.js +++ b/test/flood.js @@ -11,7 +11,9 @@ function hash (s) { } var create = SecretStack({ - appKey: hash('test_flood') + global: { + appKey: hash('test_flood') + } }) .use({ manifest: { @@ -67,7 +69,7 @@ var create = SecretStack({ }) function createPeer (name) { - var alice = create({ seed: seeds[name] }) + var alice = create({ multiserverShs: { seed: seeds[name] } }) return alice.on('flood:message', function (msg) { console.log(name, 'received', msg) }) diff --git a/test/local.js b/test/local.js index 8804b30..9ff2434 100644 --- a/test/local.js +++ b/test/local.js @@ -2,11 +2,9 @@ var tape = require('tape') var SecretStack = require('../lib') var seeds = require('./seeds') -var appkey = Buffer.alloc(32) +var appKey = Buffer.alloc(32) -var create = SecretStack({ - appKey: appkey -}) +var create = SecretStack({ global: { appKey } }) create.use({ manifest: { ping: 'sync' @@ -24,8 +22,10 @@ create.use({ }) var alice = create({ - seed: seeds.alice, - timeout: 100 + global: { + seed: seeds.alice, + timeout: 100 + } }) tape('do not timeout local client rpc', function (t) { diff --git a/test/server.js b/test/server.js index a7339bb..f4048a1 100644 --- a/test/server.js +++ b/test/server.js @@ -10,7 +10,7 @@ function hash (s) { var appKey = hash('test_key') -var create = SecretStack({ appKey }) +var create = SecretStack({ global: { appKey } }) .use(require('../lib/plugins/net')) .use(require('../lib/plugins/shs')) .use({ @@ -29,8 +29,8 @@ var create = SecretStack({ appKey }) } }) -var alice = create({ seed: seeds.alice }) -var bob = create({ seed: seeds.bob }) +var alice = create({ global: { seed: seeds.alice } }) +var bob = create({ global: { seed: seeds.bob } }) tape('alice connects to bob', function (t) { alice.connect(bob.address(), function (err, rpc) { @@ -47,8 +47,6 @@ tape('alice connects to bob', function (t) { }) tape('alice is client, bob is server', function (t) { - t.plan(4) - alice.on('rpc:connect', function (rpc, isClient) { t.true(rpc.stream.address.substr(0, 4) === 'net:' && rpc.stream.address.length > 40) t.ok(isClient) @@ -58,11 +56,16 @@ tape('alice is client, bob is server', function (t) { t.notOk(isClient) }) - alice.connect(bob.address(), function () {}) + alice.connect(bob.address(), function (err, rpc) { + t.error(err) + setTimeout(() => { + rpc.close(true, t.end) + }, 50) + }) }) tape('cleanup', function (t) { - alice.close(true, () => {}) - bob.close(true, () => {}) - t.end() + alice.close(true, () => { + bob.close(true, t.end) + }) }) diff --git a/test/timeout.js b/test/timeout.js index a6989b3..91921dc 100644 --- a/test/timeout.js +++ b/test/timeout.js @@ -8,11 +8,9 @@ function hash (s) { return crypto.createHash('sha256').update(s).digest() } -var appkey = hash('test_key') +var appKey = hash('test_key') -var create = SecretStack({ - appKey: appkey -}).use({ +var create = SecretStack({ global: { appKey } }).use({ manifest: { hello: 'sync' }, @@ -28,9 +26,9 @@ var create = SecretStack({ } }) -var alice = create({ seed: seeds.alice, timeout: 200, defaultTimeout: 5e3 }) -var carol = create({ seed: seeds.alice, timeout: 0, defaultTimeout: 10 }) -var bob = create({ seed: seeds.bob, timeout: 200, defaultTimeout: 2000 }) +var alice = create({ global: { seed: seeds.alice, timeout: 200, defaultTimeout: 5e3 } }) +var carol = create({ global: { seed: seeds.alice, timeout: 0, defaultTimeout: 10 } }) +var bob = create({ global: { seed: seeds.bob, timeout: 200, defaultTimeout: 2000 } }) tape('delay startup', function (t) { setTimeout(t.end, 500) From f9feaa1a2c9a85ea36d489612d1c52f143cea008 Mon Sep 17 00:00:00 2001 From: Andre Staltz Date: Fri, 29 Dec 2023 10:25:58 +0200 Subject: [PATCH 2/3] addressed code review comments --- CHANGELOG.md | 2 +- README.md | 2 +- lib/api.js | 70 +++++++++++++++++++++++++++------------------------- 3 files changed, 39 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0df3ae6..c369021 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # 8.0.0 -- **Breaking change**: +- **Breaking change**: the config object now has the `config.global` namespace, and `config[pluginName]` namespace. A plugin can only access its own config, plus the global config. This is to prevent plugins from accessing each other's config, for preventive security and better code organization. # 7.0.0 diff --git a/README.md b/README.md index 937455e..51ccb2f 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ Start the app and returns an EventEmitter with methods (core and plugin) attache `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[pluginName]` - an object containing data only available to the plugin with name `pluginName`. Note that `pluginName` is the camelCase of `plugin.name`. `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. diff --git a/lib/api.js b/lib/api.js index e1625b8..e7a80fe 100644 --- a/lib/api.js +++ b/lib/api.js @@ -37,15 +37,20 @@ function merge (a, b, mapper) { } /** - * @param {Record} obj + * @param {Record} fullConfig * @param {{name?: string}} plugin */ -function pluckOpts (obj, plugin) { +function buildPluginConfig (fullConfig, plugin) { if (plugin.name) { const camelCaseName = /** @type {string} */ (u.toCamelCase(plugin.name)) - return { [camelCaseName]: obj[camelCaseName], global: obj.global ?? {} } + return { + [camelCaseName]: fullConfig[camelCaseName], + global: fullConfig.global ?? {} + } } else { - return { global: obj.global ?? {} } + return { + global: fullConfig.global ?? {} + } } } @@ -55,23 +60,23 @@ function pluckOpts (obj, plugin) { */ function Api (plugins, defaultConfig) { /** - * @param {any} inputOpts + * @param {any} inputConfig */ - function create (inputOpts) { - const opts = merge(merge({}, defaultConfig), inputOpts) + function create (inputConfig) { + const config = merge(merge({}, defaultConfig), inputConfig) // change event emitter to something with more rigorous security? let api = new EventEmitter() - for (const plug of create.plugins) { - const subOpts = pluckOpts(opts, plug) - let _api = plug.init.call( + for (const plugin of create.plugins) { + const pluginConfig = buildPluginConfig(config, plugin) + let _api = plugin.init.call( {}, api, - subOpts, + pluginConfig, create.permissions, create.manifest ) - if (plug.name) { - const camelCaseName = u.toCamelCase(plug.name) + if (plugin.name) { + const camelCaseName = u.toCamelCase(plugin.name) if (camelCaseName) { /** @type {Record} */ const o = {} @@ -89,7 +94,7 @@ function Api (plugins, defaultConfig) { (val, key) => { if (typeof val === 'function') { val = Hookable(val) - if (plug.manifest && plug.manifest[key] === 'sync') { + if (plugin.manifest && plugin.manifest[key] === 'sync') { u.hookOptionalCB(val) } } @@ -106,52 +111,51 @@ function Api (plugins, defaultConfig) { create.use = /** - * @param {any} plug + * @param {any} plugin */ - function use (plug) { - if (Array.isArray(plug)) { - plug.forEach(create.use) + function use (plugin) { + if (Array.isArray(plugin)) { + plugin.forEach(create.use) return create } - if (!plug.init) { - if (typeof plug === 'function') { - create.plugins.push({ init: plug }) + if (!plugin.init) { + if (typeof plugin === 'function') { + create.plugins.push({ init: plugin }) return create } else { throw new Error('plugins *must* have "init" method') } } - if (plug.name && typeof plug.name === 'string') { - if (plug.name === 'global') { - console.error('plugin named "global" is reserved, skipping') - return create + if (plugin.name && typeof plugin.name === 'string') { + if (plugin.name === 'global') { + throw new Error('plugin named "global" is reserved') } - const found = create.plugins.some((p) => p.name === plug.name) + const found = create.plugins.some((p) => p.name === plugin.name) if (found) { // prettier-ignore - console.error('plugin named:' + plug.name + ' is already loaded, skipping') + console.error('plugin named:' + plugin.name + ' is already loaded, skipping') return create } } - const name = plug.name - if (plug.manifest) { + const name = plugin.name + if (plugin.manifest) { create.manifest = u.merge.manifest( create.manifest, - plug.manifest, + plugin.manifest, u.toCamelCase(name) ) } - if (plug.permissions) { + if (plugin.permissions) { create.permissions = u.merge.permissions( create.permissions, - plug.permissions, + plugin.permissions, u.toCamelCase(name) ) } - create.plugins.push(plug) + create.plugins.push(plugin) return create } From ccde8f94e164f3a30b4b37801544c059fe0b0bec Mon Sep 17 00:00:00 2001 From: Andre Staltz Date: Fri, 29 Dec 2023 10:30:44 +0200 Subject: [PATCH 3/3] fix test --- test/api.js | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/test/api.js b/test/api.js index 0d0a0b8..6de3bb1 100644 --- a/test/api.js +++ b/test/api.js @@ -130,18 +130,12 @@ tape('plugin cannot be named global', function (t) { } }]) - const consoleError = console.error - let called = false - console.error = function (msg) { - t.equal(msg, 'plugin named "global" is reserved, skipping', 'global warn') - called = true - } - Create.use({ - name: 'global', - init: function () { } - }) - t.equal(called, true, 'console.error was called') - console.error = consoleError + t.throws(() => { + Create.use({ + name: 'global', + init: function () { } + }) + }, 'throws on global plugin') t.end() })