Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

fastify-secrets-core

Package Overview
Dependencies
Maintainers
11
Versions
12
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

fastify-secrets-core - npm Package Compare versions

Comparing version 1.1.0 to 1.2.0

.github/workflows/check-linked-issues.yml

62

lib/build-plugin.js

@@ -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()

2

lib/fastify-secrets-core.js
'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

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc