fastify-secrets-core
Advanced tools
Comparing version 1.1.0 to 1.2.0
@@ -5,2 +5,3 @@ 'use strict' | ||
const pProps = require('p-props') | ||
const pMap = require('p-map') | ||
@@ -27,2 +28,43 @@ const DEFAULT_GET_CONCURRENCY = 5 | ||
async function getSecretsFromClient(client, concurrency, refs) { | ||
return Array.isArray(refs) | ||
? Object.assign( | ||
{}, | ||
...(await pMap(refs, async (value) => ({ [value]: await client.get(value) }), { | ||
concurrency | ||
})) | ||
) | ||
: await pProps(refs, (value) => client.get(value), { concurrency }) | ||
} | ||
function decorateWithSecrets(fastify, namespace, secrets) { | ||
if (namespace) { | ||
if (!fastify.secrets) { | ||
fastify.decorate('secrets', {}) | ||
} | ||
fastify.secrets[namespace] = secrets | ||
} else { | ||
fastify.decorate('secrets', secrets) | ||
} | ||
} | ||
async function refresh(client, fastify, opts, refs) { | ||
const { namespace, concurrency } = opts | ||
const secretsToRefresh = typeof refs === 'string' ? [refs] : refs || opts.secrets | ||
const refreshedSecrets = await getSecretsFromClient(client, concurrency, secretsToRefresh) | ||
const existingSecrets = namespace ? fastify.secrets[namespace] : fastify.secrets | ||
decorateWithSecrets(fastify, namespace, { | ||
...existingSecrets, | ||
...refreshedSecrets | ||
}) | ||
if (client.close) { | ||
await client.close() | ||
} | ||
return refreshedSecrets | ||
} | ||
function buildPlugin(Client, pluginOpts) { | ||
@@ -35,16 +77,14 @@ async function FastifySecretsPlugin(fastify, opts) { | ||
const concurrency = opts.concurrency || DEFAULT_GET_CONCURRENCY | ||
const namespace = opts.namespace | ||
const secrets = await pProps(opts.secrets, (value) => client.get(value), { concurrency }) | ||
const namespace = opts.namespace | ||
if (namespace) { | ||
if (!fastify.secrets) { | ||
fastify.decorate('secrets', {}) | ||
// Register secrets | ||
const secrets = await getSecretsFromClient(client, concurrency, opts.secrets) | ||
decorateWithSecrets(fastify, namespace, { | ||
...secrets, | ||
[opts.refreshAlias || 'refresh']: async (refs) => { | ||
const activeClient = client.close ? new Client(opts.clientOptions) : client | ||
return refresh(activeClient, fastify, opts, refs) | ||
} | ||
}) | ||
fastify.secrets[namespace] = secrets | ||
} else { | ||
fastify.decorate('secrets', secrets) | ||
} | ||
if (client.close) { | ||
@@ -51,0 +91,0 @@ await client.close() |
'use strict' | ||
const buildPlugin = require('./build-plugin') | ||
const { buildPlugin } = require('./build-plugin') | ||
@@ -5,0 +5,0 @@ module.exports = { |
{ | ||
"name": "fastify-secrets-core", | ||
"version": "1.1.0", | ||
"version": "1.2.0", | ||
"description": "Simplify development of fastify-secrets plugins", | ||
@@ -35,19 +35,20 @@ "main": "lib/fastify-secrets-core.js", | ||
"fastify-plugin": "^3.0.0", | ||
"p-map": "^4.0.0", | ||
"p-props": "^4.0.0" | ||
}, | ||
"devDependencies": { | ||
"eslint": "^7.11.0", | ||
"eslint-config-prettier": "^7.0.0", | ||
"eslint": "^7.0.0", | ||
"eslint-config-prettier": "^8.0.0", | ||
"eslint-config-standard": "^16.0.0", | ||
"eslint-plugin-import": "^2.22.1", | ||
"eslint-plugin-node": "^11.1.0", | ||
"eslint-plugin-prettier": "^3.1.4", | ||
"eslint-plugin-promise": "^4.2.1", | ||
"eslint-plugin-prettier": "^4.0.0", | ||
"eslint-plugin-promise": "^5.1.0", | ||
"eslint-plugin-standard": "^5.0.0", | ||
"husky": "^4.3.0", | ||
"lint-staged": "^10.4.0", | ||
"husky": "^7.0.0", | ||
"lint-staged": "^12.0.2", | ||
"prettier": "^2.1.2", | ||
"proxyquire": "^2.1.3", | ||
"sinon": "^9.2.0", | ||
"tap": "^14.10.8" | ||
"sinon": "^13.0.0", | ||
"tap": "^16.0.0" | ||
}, | ||
@@ -54,0 +55,0 @@ "lint-staged": { |
@@ -7,3 +7,3 @@ # Fastify Secrets (core) | ||
> This module is intended for developers implemeting fastify-secrets plugins, not for developers using fastify-plugin in their fastify projects | ||
> This module is intended for developers implementing fastify-secrets plugins, not for developers using fastify-plugin in their fastify projects | ||
@@ -65,3 +65,2 @@ ## Installation | ||
} | ||
``` | ||
@@ -74,5 +73,5 @@ | ||
- `secrets` (required). A non-empty object representing a map of secret keys and references. The plugin will decorate fastify with a `secrets` object with the same keys as this option but will replace the references with the actual values for the secret as fetched with `client.get(reference)` | ||
- `secrets` (required). A non-empty object representing a map of secret keys and references, or an array of strings. The plugin will decorate fastify with a `secrets` object with the same keys as this option (for object values) or with the same keys as the array elements (for array values) but will replace the references with the actual values for the secret as fetched with `client.get(reference)` | ||
- `namespace` (optional). If present, the plugin will add the secret values to `fastify.secrets.namespace` instead of `fastify.secrets`. | ||
- `concurrency` (optional, defaults to 5). How many concurrent call will be made to `client.get`. This is handled by `fastify-secrets-core` and it's transparent to the implementation. | ||
- `concurrency` (optional, defaults to 5). How many concurrent call will be made to `client.get`. This is handled by `fastify-secrets-core` and it's transparent to the implementation. | ||
- `clientOptions` (optional). A value that will be provided to the constructor of the `Client`. Useful to allow plugin users to customize the plugin. | ||
@@ -84,3 +83,3 @@ | ||
Assuming a plugin is built as per the previous examples, it can be used as | ||
Assuming a plugin is built as per the previous examples, it can be used as: | ||
@@ -103,5 +102,72 @@ ```js | ||
console.log(fastify.secrets.db.pass) | ||
``` | ||
Or, with an array `secrets` option: | ||
```js | ||
fastify.register(plugin, { | ||
namespace: 'db', | ||
concurrency: 5, | ||
secrets: ['PG_USER', 'PG_PASS'], | ||
clientOptions: { | ||
optional: 'value' | ||
} | ||
}) | ||
await fastify.ready() | ||
console.log(fastify.secrets.db.PG_PASS) | ||
``` | ||
#### Refreshing Secrets | ||
In the event secrets need to be dynamically refreshed, a refresh method is exposed to allow for the refreshing of single, sets, or all secrets scoped to the provided namespace. The signature of the refresh method is as follows: | ||
`async refresh(refs)` | ||
- `refs` (optional). refs can be a single secret, an array of secrets, or left undefined to refresh all secrets belonging to the namespace. | ||
The most basic example of usage can be seen below, | ||
```js | ||
fastify.register(plugin, { | ||
secrets: ['TOKEN'] | ||
}) | ||
await fastify.ready() | ||
const refreshedSecrets = await fastify.secrets.refresh() // { 'TOKEN': 'refreshed value' } | ||
``` | ||
##### Namespacing | ||
When working with multiple secret providers, it is highly recommended that you scope your secrets by a namespace, this will prevent conflicts with other secret providers and allow for more atomic refreshing if necessary. Note the example below for usage, in particular the `refresh` method is exposed on the registered namespace and not at the root of the secrets object. | ||
```js | ||
fastify.register(plugin, { | ||
namespace: 'aws', | ||
secrets: ['TOKEN'] | ||
}) | ||
await fastify.ready() | ||
const refreshedSecrets = await fastify.secrets.aws.refresh() // { 'TOKEN': 'refreshed value' } | ||
``` | ||
##### Refresh Aliasing | ||
It's possible that a secret name may conflict with the `refresh` method, in the event this happens you can supply an alias for the refresh function in order to avoid conflicts. | ||
```js | ||
fastify.register(plugin, { | ||
secrets: ['TOKEN'], | ||
namespace: 'auth', | ||
refreshAlias: 'update' | ||
}) | ||
await fastify.ready() | ||
const refreshedSecrets = await fastify.secrets.update() // { 'TOKEN': 'refreshed value' } | ||
``` | ||
## Contributing | ||
@@ -108,0 +174,0 @@ |
@@ -26,3 +26,3 @@ 'use strict' | ||
test('builds a fastify plugin', (t) => { | ||
test('builds a fastify plugin', async (t) => { | ||
const plugin = buildPlugin(Client, { | ||
@@ -40,9 +40,5 @@ option: 'option1' | ||
t.equal(plugin.Client, Client, 'also exports client') | ||
t.end() | ||
}) | ||
test('plugin', (t) => { | ||
t.plan(7) | ||
test('plugin', async (t) => { | ||
buildPlugin(Client, { | ||
@@ -54,29 +50,45 @@ option: 'option1' | ||
t.test('no namespace', async (t) => { | ||
t.plan(2) | ||
const decorate = sinon.stub().callsFake((key, value) => { | ||
fastifyMock[key] = value | ||
}) | ||
const fastifyMock = { | ||
decorate | ||
} | ||
const decorate = sinon.spy() | ||
await plugin( | ||
{ decorate }, | ||
{ | ||
secrets: { | ||
secret1: 'secret1-name', | ||
secret2: 'secret2-name' | ||
} | ||
await plugin(fastifyMock, { | ||
secrets: { | ||
secret1: 'secret1-name', | ||
secret2: 'secret2-name' | ||
} | ||
) | ||
}) | ||
t.ok(decorate.called, 'decorates fastify') | ||
t.ok( | ||
decorate.calledWith('secrets', { | ||
secret1: 'content for secret1-name', | ||
secret2: 'content for secret2-name' | ||
}), | ||
'decorates with secrets content' | ||
) | ||
t.ok(typeof fastifyMock.secrets.refresh === 'function', 'refresh is defined as expected') | ||
sinon.assert.calledWith(decorate, 'secrets', { | ||
secret1: 'content for secret1-name', | ||
secret2: 'content for secret2-name', | ||
refresh: fastifyMock.secrets.refresh | ||
}) | ||
}) | ||
t.test('no namespace - secrets array', async (t) => { | ||
const decorate = sinon.stub().callsFake((key, value) => { | ||
fastifyMock[key] = value | ||
}) | ||
const fastifyMock = { | ||
decorate | ||
} | ||
await plugin(fastifyMock, { | ||
secrets: ['secret1-name', 'secret2-name'] | ||
}) | ||
t.ok(typeof fastifyMock.secrets.refresh === 'function', 'refresh is defined as expected') | ||
sinon.assert.calledWith(decorate, 'secrets', { | ||
'secret1-name': 'content for secret1-name', | ||
'secret2-name': 'content for secret2-name', | ||
refresh: fastifyMock.secrets.refresh | ||
}) | ||
}) | ||
t.test('no namespace - secrets exists', async (t) => { | ||
t.plan(2) | ||
const decorate = sinon.spy() | ||
@@ -99,56 +111,54 @@ | ||
t.test('namespace', async (t) => { | ||
t.plan(2) | ||
const decorate = sinon.stub().callsFake((key, value) => { | ||
fastifyMock[key] = value | ||
}) | ||
const fastifyMock = { | ||
decorate | ||
} | ||
// emulate decorate behaviour | ||
const decorate = sinon.stub().callsFake(function decorate(key, obj) { | ||
this[key] = obj | ||
await plugin(fastifyMock, { | ||
namespace: 'namespace1', | ||
secrets: { | ||
secret1: 'secret1-name', | ||
secret2: 'secret2-name' | ||
} | ||
}) | ||
await plugin( | ||
{ decorate }, | ||
{ | ||
namespace: 'namespace1', | ||
secrets: { | ||
secret1: 'secret1-name', | ||
secret2: 'secret2-name' | ||
} | ||
t.ok(typeof fastifyMock.secrets.namespace1.refresh === 'function', 'refresh is defined as expected') | ||
sinon.assert.calledWith(decorate, 'secrets', { | ||
namespace1: { | ||
secret1: 'content for secret1-name', | ||
secret2: 'content for secret2-name', | ||
refresh: fastifyMock.secrets.namespace1.refresh | ||
} | ||
) | ||
t.ok(decorate.called, 'decorates fastify') | ||
t.ok( | ||
decorate.calledWith('secrets', { | ||
namespace1: { | ||
secret1: 'content for secret1-name', | ||
secret2: 'content for secret2-name' | ||
} | ||
}), | ||
'decorates with secrets content' | ||
) | ||
}) | ||
}) | ||
t.test('namespace - secrets exists', async (t) => { | ||
t.plan(2) | ||
const decorate = sinon.stub().callsFake((key, value) => { | ||
fastifyMock[key] = value | ||
}) | ||
const expectedSecrets = {} | ||
const fastifyMock = { | ||
decorate, | ||
secrets: expectedSecrets | ||
} | ||
const decorate = sinon.spy() | ||
const secrets = {} | ||
await plugin( | ||
{ decorate, secrets }, | ||
{ | ||
namespace: 'namespace1', | ||
secrets: { | ||
secret1: 'secret1-name', | ||
secret2: 'secret2-name' | ||
} | ||
await plugin(fastifyMock, { | ||
namespace: 'namespace1', | ||
secrets: { | ||
secret1: 'secret1-name', | ||
secret2: 'secret2-name' | ||
} | ||
) | ||
}) | ||
t.notOk(decorate.called, 'does not decorate fastify') | ||
t.ok(typeof fastifyMock.secrets.namespace1.refresh === 'function', 'refresh is defined as expected') | ||
t.notOk(decorate.calledWith('secrets'), 'does not decorate fastify with secrets') | ||
t.same( | ||
secrets, | ||
expectedSecrets, | ||
{ | ||
namespace1: { | ||
secret1: 'content for secret1-name', | ||
secret2: 'content for secret2-name' | ||
secret2: 'content for secret2-name', | ||
refresh: fastifyMock.secrets.namespace1.refresh | ||
} | ||
@@ -161,4 +171,2 @@ }, | ||
t.test('namespace - namespace exists', async (t) => { | ||
t.plan(3) | ||
const decorate = sinon.spy() | ||
@@ -199,4 +207,2 @@ const secrets = { | ||
t.test('no options', async (t) => { | ||
t.plan(2) | ||
const decorate = sinon.spy() | ||
@@ -210,4 +216,2 @@ const promise = plugin({ decorate }) | ||
t.test('no secrets', async (t) => { | ||
t.plan(2) | ||
const decorate = sinon.spy() | ||
@@ -222,5 +226,3 @@ const emptyOpts = {} | ||
test('client integration', (t) => { | ||
t.plan(3) | ||
test('client integration', async (t) => { | ||
t.test('clientOptions are provided to client when instantiated', async (t) => { | ||
@@ -257,4 +259,2 @@ const constructorStub = sinon.stub() | ||
t.test('client with close method', async (t) => { | ||
t.plan(1) | ||
let closeCalled = false | ||
@@ -291,4 +291,2 @@ | ||
t.test('client without close method', async (t) => { | ||
t.plan(1) | ||
class Client { | ||
@@ -317,1 +315,261 @@ async get(key) { | ||
}) | ||
test('client wrapper', async (t) => { | ||
buildPlugin(Client) | ||
const plugin = fp.firstCall.args[0] | ||
t.test("is exposed as 'refresh' at the root with no namespace", async (t) => { | ||
const decorate = sinon.stub().callsFake((key, value) => { | ||
fastifyMock[key] = value | ||
}) | ||
const fastifyMock = { | ||
decorate | ||
} | ||
await plugin(fastifyMock, { | ||
secrets: { | ||
secret1: 'secret1-name' | ||
} | ||
}) | ||
t.ok(decorate.calledWith('secrets'), 'decorates fastify with secrets') | ||
t.ok(fastifyMock.secrets.refresh, 'populates secrets with a refresh method') | ||
}) | ||
t.test("is exposed as 'refresh' on the namespace scope when provided", async (t) => { | ||
const decorate = sinon.stub().callsFake((key, value) => { | ||
fastifyMock[key] = value | ||
}) | ||
const fastifyMock = { | ||
decorate | ||
} | ||
await plugin(fastifyMock, { | ||
namespace: 'test', | ||
secrets: { | ||
secret1: 'secret1-name' | ||
} | ||
}) | ||
t.ok(decorate.calledWith('secrets'), 'decorates fastify with secrets') | ||
t.ok(fastifyMock.secrets.test.refresh, 'populates secrets namespace with a refresh method') | ||
}) | ||
t.test('can be aliased using the refreshAlias option', async (t) => { | ||
const decorate = sinon.stub().callsFake((key, value) => { | ||
fastifyMock[key] = value | ||
}) | ||
const fastifyMock = { | ||
decorate | ||
} | ||
await plugin(fastifyMock, { | ||
namespace: 'test', | ||
refreshAlias: 'update', | ||
secrets: { | ||
secret1: 'secret1-name' | ||
} | ||
}) | ||
t.ok(decorate.calledWith('secrets'), 'decorates fastify with secrets') | ||
t.ok(fastifyMock.secrets.test.update, 'populates secrets namespace with an "update" method') | ||
}) | ||
t.test('persists across refresh invocations', async (t) => { | ||
const decorate = sinon.stub().callsFake((key, value) => { | ||
fastifyMock[key] = value | ||
}) | ||
const fastifyMock = { | ||
decorate | ||
} | ||
await plugin(fastifyMock, { | ||
namespace: 'test', | ||
secrets: { | ||
secret1: 'secret1-name' | ||
} | ||
}) | ||
t.ok(decorate.calledWith('secrets'), 'decorates fastify with secrets') | ||
t.ok(fastifyMock.secrets.test.refresh, 'populates secrets namespace with a refresh method') | ||
await fastifyMock.secrets.test.refresh() | ||
t.ok(decorate.calledWith('secrets'), 'decorates fastify with secrets') | ||
t.ok(fastifyMock.secrets.test.refresh, 'populates secrets namespace with a refresh method') | ||
}) | ||
class MockClient { | ||
constructor() { | ||
this.invokeCount = {} | ||
} | ||
async get(key) { | ||
if (this.invokeCount[key] === undefined) { | ||
this.invokeCount[key] = 1 | ||
} else { | ||
this.invokeCount[key] += 1 | ||
} | ||
return `value for ${key} - ${this.invokeCount[key]}` | ||
} | ||
} | ||
t.test('allows for specific secrets to be refreshed', async (t) => { | ||
buildPlugin(MockClient) | ||
const plugin = fp.firstCall.args[0] | ||
const decorate = sinon.stub().callsFake((key, value) => { | ||
fastifyMock[key] = value | ||
}) | ||
const fastifyMock = { | ||
decorate | ||
} | ||
await plugin(fastifyMock, { | ||
secrets: ['test', 'test2'] | ||
}) | ||
await fastifyMock.secrets.refresh('test') | ||
t.equal(fastifyMock.secrets.test, 'value for test - 2', 'refreshed secret has been called twice') | ||
t.equal(fastifyMock.secrets.test2, 'value for test2 - 1', 'un-refreshed secret has been called once') | ||
}) | ||
t.test('refreshes all secrets by default', async (t) => { | ||
buildPlugin(MockClient) | ||
const plugin = fp.firstCall.args[0] | ||
const decorate = sinon.stub().callsFake((key, value) => { | ||
fastifyMock[key] = value | ||
}) | ||
const fastifyMock = { | ||
decorate | ||
} | ||
await plugin(fastifyMock, { | ||
secrets: ['test', 'test2'] | ||
}) | ||
await fastifyMock.secrets.refresh() | ||
t.equal(fastifyMock.secrets.test, 'value for test - 2', 'refreshed secret has been called twice') | ||
t.equal(fastifyMock.secrets.test2, 'value for test2 - 2', 'refreshed secret has been called twice') | ||
}) | ||
t.test('refreshes a specified set of secrets with array notation', async (t) => { | ||
buildPlugin(MockClient) | ||
const plugin = fp.firstCall.args[0] | ||
const decorate = sinon.stub().callsFake((key, value) => { | ||
fastifyMock[key] = value | ||
}) | ||
const fastifyMock = { | ||
decorate | ||
} | ||
await plugin(fastifyMock, { | ||
secrets: ['test', 'test2', 'test3'] | ||
}) | ||
await fastifyMock.secrets.refresh(['test', 'test2']) | ||
t.equal(fastifyMock.secrets.test, 'value for test - 2', 'refreshed secret has been called twice') | ||
t.equal(fastifyMock.secrets.test2, 'value for test2 - 2', 'refreshed secret has been called twice') | ||
t.equal(fastifyMock.secrets.test3, 'value for test3 - 1', 'un-refreshed secret has been called once') | ||
}) | ||
t.test('refreshes a specified set of secrets with object notation', async (t) => { | ||
buildPlugin(MockClient) | ||
const plugin = fp.firstCall.args[0] | ||
const decorate = sinon.stub().callsFake((key, value) => { | ||
fastifyMock[key] = value | ||
}) | ||
const fastifyMock = { | ||
decorate | ||
} | ||
const defaultSecrets = { | ||
test: 'secretAlias', | ||
test2: 'secretAlias2', | ||
test3: 'secretAlias3' | ||
} | ||
await plugin(fastifyMock, { | ||
secrets: defaultSecrets | ||
}) | ||
await fastifyMock.secrets.refresh() | ||
t.equal(fastifyMock.secrets.test, 'value for secretAlias - 2', 'refreshed secret has been called twice') | ||
t.equal(fastifyMock.secrets.test2, 'value for secretAlias2 - 2', 'refreshed secret has been called twice') | ||
t.equal(fastifyMock.secrets.test3, 'value for secretAlias3 - 2', 'refreshed secret has been called twice') | ||
}) | ||
t.test('respects namespaces when refreshing', async (t) => { | ||
buildPlugin(MockClient) | ||
const plugin = fp.firstCall.args[0] | ||
const decorate = sinon.stub().callsFake((key, value) => { | ||
fastifyMock[key] = value | ||
}) | ||
const fastifyMock = { | ||
decorate, | ||
secrets: {} | ||
} | ||
await plugin(fastifyMock, { | ||
namespace: 'testns', | ||
secrets: ['test', 'test2'] | ||
}) | ||
await fastifyMock.secrets.testns.refresh('test') | ||
t.equal(fastifyMock.secrets.testns.test, 'value for test - 2', 'refreshed secret has been called twice') | ||
t.equal(fastifyMock.secrets.testns.test2, 'value for test2 - 1', 'un-refreshed secret has been called once') | ||
}) | ||
t.test('will instantiate a fresh client if there is a provided close method', async (t) => { | ||
const constructionStub = sinon.stub() | ||
const closeStub = sinon.stub() | ||
class MockCloseClient { | ||
constructor() { | ||
constructionStub() | ||
} | ||
async get(key) { | ||
return `value for ${key}` | ||
} | ||
async close() { | ||
closeStub() | ||
} | ||
} | ||
buildPlugin(MockCloseClient) | ||
const plugin = fp.firstCall.args[0] | ||
const decorate = sinon.stub().callsFake((key, value) => { | ||
fastifyMock[key] = value | ||
}) | ||
const fastifyMock = { | ||
decorate | ||
} | ||
await plugin(fastifyMock, { | ||
secrets: ['test'] | ||
}) | ||
t.ok(closeStub.calledOnce, 'close is invoked after initial secret setup') | ||
await fastifyMock.secrets.refresh() | ||
t.ok(constructionStub.calledTwice, 'constructor has been called twice') | ||
t.ok(closeStub.calledTwice, 'close method has been called twice') | ||
}) | ||
}) |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
33306
16
548
175
0
3
+ Addedp-map@^4.0.0