Projektstart

This commit is contained in:
2026-01-22 15:49:12 +01:00
parent 7212eb6f7a
commit 57e5f652f8
10637 changed files with 2598792 additions and 64 deletions

2
backend/node_modules/@fastify/helmet/.gitattributes generated vendored Normal file
View File

@@ -0,0 +1,2 @@
# Set default behavior to automatically convert line endings
* text=auto eol=lf

View File

@@ -0,0 +1,13 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"
open-pull-requests-limit: 10
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "monthly"
open-pull-requests-limit: 10

21
backend/node_modules/@fastify/helmet/.github/stale.yml generated vendored Normal file
View File

@@ -0,0 +1,21 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 15
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 7
# Issues with these labels will never be considered stale
exemptLabels:
- "discussion"
- "feature request"
- "bug"
- "help wanted"
- "plugin suggestion"
- "good first issue"
# Label to use when marking an issue as stale
staleLabel: stale
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false

View File

@@ -0,0 +1,28 @@
name: CI
on:
push:
branches:
- main
- next
- 'v*'
paths-ignore:
- 'docs/**'
- '*.md'
pull_request:
paths-ignore:
- 'docs/**'
- '*.md'
permissions:
contents: read
jobs:
test:
permissions:
contents: write
pull-requests: write
uses: fastify/workflows/.github/workflows/plugins-ci.yml@v5
with:
license-check: true
lint: true

49
backend/node_modules/@fastify/helmet/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,49 @@
MIT License
Copyright (c) 2017-present The Fastify team
The Fastify team members are listed at https://github.com/fastify/fastify#team.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
--> Original helmet license
(The MIT License)
Copyright (c) 2012-2017 Evan Hahn, Adam Baldwin
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

262
backend/node_modules/@fastify/helmet/README.md generated vendored Normal file
View File

@@ -0,0 +1,262 @@
# @fastify/helmet
[![CI](https://github.com/fastify/fastify-helmet/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/fastify/fastify-helmet/actions/workflows/ci.yml)
[![NPM version](https://img.shields.io/npm/v/@fastify/helmet)](https://www.npmjs.com/package/@fastify/helmet)
[![neostandard javascript style](https://img.shields.io/badge/code_style-neostandard-brightgreen?style=flat)](https://github.com/neostandard/neostandard)
Important security headers for Fastify, using [helmet](https://npm.im/helmet).
## Install
```
npm i @fastify/helmet
```
### Compatibility
| Plugin version | Fastify version |
| ---------------|-----------------|
| `>=12.x` | `^5.x` |
| `>=9.x <12.x` | `^4.x` |
| `>=7.x <9.x` | `^3.x` |
| `>=1.x <7.x` | `^2.x` |
| `>=1.x <7.x` | `^1.x` |
Please note that if a Fastify version is out of support, then so are the corresponding versions of this plugin
in the table above.
See [Fastify's LTS policy](https://github.com/fastify/fastify/blob/main/docs/Reference/LTS.md) for more details.
## Usage
Simply require this plugin to set basic security headers.
```js
const fastify = require('fastify')()
const helmet = require('@fastify/helmet')
fastify.register(
helmet,
// Example disables the `contentSecurityPolicy` middleware but keeps the rest.
{ contentSecurityPolicy: false }
)
fastify.listen({ port: 3000 }, err => {
if (err) throw err
})
```
## How it works
`@fastify/helmet` is a wrapper around `helmet` that adds an `'onRequest'` hook
and a `reply.helmet` decorator.
It accepts the same options as `helmet`. See [helmet documentation](https://helmetjs.github.io/).
### Apply Helmet to all routes
Pass `{ global: true }` to register Helmet for all routes.
For granular control, pass `{ global: false }` to disable it at a global scope.
Default is `true`.
#### Example - enable `@fastify/helmet` globally
```js
fastify.register(helmet)
// or
fastify.register(helmet, { global: true })
```
#### Example - disable `@fastify/helmet` globally
```js
// register the package with the `{ global: false }` option
fastify.register(helmet, { global: false })
fastify.get('/route-with-disabled-helmet', async (request, reply) => {
return { message: 'helmet is not enabled here' }
})
fastify.get('/route-with-enabled-helmet', {
// We enable and configure helmet for this route only
helmet: {
dnsPrefetchControl: {
allow: true
},
frameguard: {
action: 'foo'
},
referrerPolicy: false
}
}, async (request, reply) => {
return { message: 'helmet is enabled here' }
})
// helmet is disabled on this route but we have access to `reply.helmet` decorator
// that allows us to apply helmet conditionally
fastify.get('/here-we-use-helmet-reply-decorator', async (request, reply) => {
if (condition) {
// we apply the default options
await reply.helmet()
} else {
// we apply customized options
await reply.helmet({ frameguard: false })
}
return {
message: 'we use the helmet reply decorator to conditionally apply helmet middlewares'
}
})
```
### `helmet` route option
`@fastify/helmet` allows enabling, disabling, and customizing `helmet` for each route using the `helmet` shorthand option
when registering routes.
To disable `helmet` for a specific endpoint, pass `{ helmet: false }` to the route options.
To enable or customize `helmet` for a specific endpoint, pass a configuration object to route options, e.g., `{ helmet: { frameguard: false } }`.
#### Example - `@fastify/helmet` configuration using the `helmet` shorthand route option
```js
// register the package with the `{ global: true }` option
fastify.register(helmet, { global: true })
fastify.get('/route-with-disabled-helmet', { helmet: false }, async (request, reply) => {
return { message: 'helmet is not enabled here' }
})
fastify.get('/route-with-enabled-helmet', async (request, reply) => {
return { message: 'helmet is enabled by default here' }
})
fastify.get('/route-with-custom-helmet-configuration', {
// We change the helmet configuration for this route only
helmet: {
enableCSPNonces: true,
contentSecurityPolicy: {
directives: {
'directive-1': ['foo', 'bar']
},
reportOnly: true
},
dnsPrefetchControl: {
allow: true
},
frameguard: {
action: 'foo'
},
hsts: {
maxAge: 1,
includeSubDomains: true,
preload: true
},
permittedCrossDomainPolicies: {
permittedPolicies: 'foo'
},
referrerPolicy: false
}
}, async (request, reply) => {
return { message: 'helmet is enabled with a custom configuration on this route' }
})
```
### Content-Security-Policy Nonce
`@fastify/helmet` also allows CSP nonce generation, which can be enabled by passing `{ enableCSPNonces: true }` into the options.
Retrieve the `nonces` through `reply.cspNonce`.
> Note: This feature is implemented by this module and is not supported by `helmet`.
> For using `helmet` only for csp nonces, see [example](#example---generate-by-helmet).
#### Example - Generate by options
```js
fastify.register(
helmet,
// enable csp nonces generation with default content-security-policy option
{ enableCSPNonces: true }
)
fastify.register(
helmet,
// customize content security policy with nonce generation
{
enableCSPNonces: true,
contentSecurityPolicy: {
directives: {
...
}
}
}
)
fastify.get('/', function(request, reply) {
// retrieve script nonce
reply.cspNonce.script
// retrieve style nonce
reply.cspNonce.style
})
```
#### Example - Generate by helmet
```js
fastify.register(
helmet,
{
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: [
function (req, res) {
// "res" here is actually "reply.raw" in fastify
res.scriptNonce = crypto.randomBytes(16).toString('hex')
// make sure to return nonce-... directive to helmet, so it can be sent in the headers
return `'nonce-${res.scriptNonce}'`
}
],
styleSrc: [
function (req, res) {
// "res" here is actually "reply.raw" in fastify
res.styleNonce = crypto.randomBytes(16).toString('hex')
// make sure to return nonce-... directive to helmet, so it can be sent in the headers
return `'nonce-${res.styleNonce}'`
}
]
}
}
}
)
fastify.get('/', function(request, reply) {
// access the generated nonce by "reply.raw"
reply.raw.scriptNonce
reply.raw.styleNonce
})
```
### Disable Default `helmet` Directives
By default, `helmet` adds [a default set of CSP directives](https://github.com/helmetjs/helmet/tree/main/middlewares/content-security-policy#content-security-policy-middleware) to the response.
Disable this by setting `useDefaults: false` in the `contentSecurityPolicy` configuration.
```js
fastify.register(
helmet,
{
contentSecurityPolicy: {
useDefaults: false,
directives: {
'default-src': ["'self'"]
}
}
}
)
```
## License
Licensed under [MIT](./LICENSE).

View File

@@ -0,0 +1,6 @@
'use strict'
module.exports = require('neostandard')({
ignores: require('neostandard').resolveIgnoresFromGitignore(),
ts: true
})

View File

@@ -0,0 +1,46 @@
'use strict'
const Fastify = require('fastify')
const helmet = require('..')
const fastify = Fastify({
logger: {
level: 'info'
}
})
fastify.register(helmet)
const opts = {
schema: {
response: {
200: {
type: 'object',
properties: {
hello: {
type: 'string'
}
}
}
}
}
}
fastify.get('/', opts, function (_request, reply) {
reply
.header('Content-Type', 'application/json')
.code(200)
.send({ hello: 'world' })
})
fastify.get('/route-with-disabled-helmet', { ...opts, helmet: false }, function (_request, reply) {
reply
.header('Content-Type', 'application/json')
.code(200)
.send({ hello: 'world' })
})
fastify.listen({ port: 3000 }, err => {
if (err) throw err
fastify.log.info(`Server listening on ${fastify.server.address().address}:${fastify.server.address().port}`)
})

151
backend/node_modules/@fastify/helmet/index.js generated vendored Normal file
View File

@@ -0,0 +1,151 @@
'use strict'
const { randomBytes } = require('node:crypto')
const fp = require('fastify-plugin')
const helmet = require('helmet')
async function fastifyHelmet (fastify, options) {
// helmet will throw when any option is explicitly set to "true"
// using ECMAScript destructuring is a clean workaround as we do not need to alter options
const { enableCSPNonces, global, ...globalConfiguration } = options
const isGlobal = typeof global === 'boolean' ? global : true
// We initialize the `helmet` reply decorator only if it does not already exists
if (!fastify.hasReplyDecorator('helmet')) {
fastify.decorateReply('helmet', null)
}
// We initialize the `cspNonce` reply decorator only if it does not already exists
if (!fastify.hasReplyDecorator('cspNonce')) {
fastify.decorateReply('cspNonce', null)
}
fastify.addHook('onRoute', (routeOptions) => {
if (routeOptions.helmet !== undefined) {
if (typeof routeOptions.helmet === 'object') {
routeOptions.config = Object.assign(routeOptions.config || Object.create(null), { helmet: routeOptions.helmet })
} else if (routeOptions.helmet === false) {
routeOptions.config = Object.assign(routeOptions.config || Object.create(null), { helmet: { skipRoute: true } })
} else {
throw new Error('Unknown value for route helmet configuration')
}
}
})
fastify.addHook('onRequest', async function helmetConfigureReply (request, reply) {
const { helmet: routeOptions } = request.routeOptions?.config
if (routeOptions !== undefined) {
const { enableCSPNonces: enableRouteCSPNonces, skipRoute, ...helmetRouteConfiguration } = routeOptions
// If route helmet options are set they overwrite the global helmet configuration
const mergedHelmetConfiguration = Object.assign(Object.create(null), globalConfiguration, helmetRouteConfiguration)
// We decorate the reply with a fallback to the route scoped helmet options
return replyDecorators(request, reply, mergedHelmetConfiguration, enableRouteCSPNonces)
}
// We decorate the reply with a fallback to the global helmet options
return replyDecorators(request, reply, globalConfiguration, enableCSPNonces)
})
fastify.addHook('onRequest', function helmetApplyHeaders (request, reply, next) {
const { helmet: routeOptions } = request.routeOptions?.config
if (routeOptions !== undefined) {
const { enableCSPNonces: enableRouteCSPNonces, skipRoute, ...helmetRouteConfiguration } = routeOptions
if (skipRoute === true) {
// If helmet route option is set to `false` we skip the route
} else {
// If route helmet options are set they overwrite the global helmet configuration
const mergedHelmetConfiguration = Object.assign(Object.create(null), globalConfiguration, helmetRouteConfiguration)
return buildHelmetOnRoutes(request, reply, mergedHelmetConfiguration, enableRouteCSPNonces)
}
return next()
}
if (isGlobal) {
// if the plugin is set globally (meaning that all the routes will be decorated)
// As the endpoint, does not have a custom helmet configuration, use the global one.
return buildHelmetOnRoutes(request, reply, globalConfiguration, enableCSPNonces)
}
// if the plugin is not global we can skip the route
return next()
})
}
async function replyDecorators (request, reply, configuration, enableCSP) {
if (enableCSP) {
reply.cspNonce = {
script: randomBytes(16).toString('hex'),
style: randomBytes(16).toString('hex')
}
}
reply.helmet = function (opts) {
const helmetConfiguration = opts
? Object.assign(Object.create(null), configuration, opts)
: configuration
return helmet(helmetConfiguration)(request.raw, reply.raw, done)
}
}
async function buildHelmetOnRoutes (request, reply, configuration, enableCSP) {
if (enableCSP === true && configuration.contentSecurityPolicy !== false) {
const cspDirectives = configuration.contentSecurityPolicy
? configuration.contentSecurityPolicy.directives
: helmet.contentSecurityPolicy.getDefaultDirectives()
const cspReportOnly = configuration.contentSecurityPolicy
? configuration.contentSecurityPolicy.reportOnly
: undefined
const cspUseDefaults = configuration.contentSecurityPolicy
? configuration.contentSecurityPolicy.useDefaults
: undefined
// We get the csp nonce from the reply
const { script: scriptCSPNonce, style: styleCSPNonce } = reply.cspNonce
// We prevent object reference: https://github.com/fastify/fastify-helmet/issues/118
const directives = { ...cspDirectives }
// We push nonce to csp
// We allow both 'script-src' or 'scriptSrc' syntax
const scriptKey = Array.isArray(directives['script-src']) ? 'script-src' : 'scriptSrc'
directives[scriptKey] = Array.isArray(directives[scriptKey]) ? [...directives[scriptKey]] : []
directives[scriptKey].push(`'nonce-${scriptCSPNonce}'`)
// allow both style-src or styleSrc syntax
const styleKey = Array.isArray(directives['style-src']) ? 'style-src' : 'styleSrc'
directives[styleKey] = Array.isArray(directives[styleKey]) ? [...directives[styleKey]] : []
directives[styleKey].push(`'nonce-${styleCSPNonce}'`)
const contentSecurityPolicy = { directives, reportOnly: cspReportOnly, useDefaults: cspUseDefaults }
const mergedHelmetConfiguration = Object.assign(Object.create(null), configuration, { contentSecurityPolicy })
helmet(mergedHelmetConfiguration)(request.raw, reply.raw, done)
} else {
helmet(configuration)(request.raw, reply.raw, done)
}
}
function done (error) {
// Helmet used to forward an Error object, so we could just rethrow it.
// Since Helmet v8.1.0 (see PR https://github.com/helmetjs/helmet/pull/485 ),
// errors are thrown directly instead of being passed to a callback.
// We keep the argument for compatibility, as v8.1.0 still accepts it
// (see https://github.com/helmetjs/helmet/blob/v8.1.0/index.ts#L109 ).
/* c8 ignore next */
if (error) throw error
}
module.exports = fp(fastifyHelmet, {
fastify: '5.x',
name: '@fastify/helmet'
})
module.exports.default = fastifyHelmet
module.exports.fastifyHelmet = fastifyHelmet
module.exports.contentSecurityPolicy = helmet.contentSecurityPolicy

View File

@@ -0,0 +1,2 @@
# Set default behavior to automatically convert line endings
* text=auto eol=lf

View File

@@ -0,0 +1,13 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"
open-pull-requests-limit: 10
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "monthly"
open-pull-requests-limit: 10

View File

@@ -0,0 +1,21 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 15
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 7
# Issues with these labels will never be considered stale
exemptLabels:
- "discussion"
- "feature request"
- "bug"
- "help wanted"
- "plugin suggestion"
- "good first issue"
# Label to use when marking an issue as stale
staleLabel: stale
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false

View File

@@ -0,0 +1,28 @@
name: CI
on:
push:
branches:
- main
- next
- 'v*'
paths-ignore:
- 'docs/**'
- '*.md'
pull_request:
paths-ignore:
- 'docs/**'
- '*.md'
permissions:
contents: read
jobs:
test:
permissions:
contents: write
pull-requests: write
uses: fastify/workflows/.github/workflows/plugins-ci.yml@v5
with:
license-check: true
lint: true

View File

@@ -0,0 +1,23 @@
MIT License
Copyright (c) 2017-present The Fastify team
The Fastify team members are listed at https://github.com/fastify/fastify#team.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,188 @@
# fastify-plugin
[![CI](https://github.com/fastify/fastify-plugin/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/fastify/fastify-plugin/actions/workflows/ci.yml)
[![NPM version](https://img.shields.io/npm/v/fastify-plugin.svg?style=flat)](https://www.npmjs.com/package/fastify-plugin)
[![neostandard javascript style](https://img.shields.io/badge/code_style-neostandard-brightgreen?style=flat)](https://github.com/neostandard/neostandard)
`fastify-plugin` is a plugin helper for [Fastify](https://github.com/fastify/fastify).
When you build plugins for Fastify and you want them to be accessible in the same context where you require them, you have two ways:
1. Use the `skip-override` hidden property
2. Use this module
__Note: the v4.x series of this module covers Fastify v4__
__Note: the v2.x & v3.x series of this module covers Fastify v3. For Fastify v2 support, refer to the v1.x series.__
## Install
```sh
npm i fastify-plugin
```
## Usage
`fastify-plugin` can do three things for you:
- Add the `skip-override` hidden property
- Check the bare-minimum version of Fastify
- Pass some custom metadata of the plugin to Fastify
Example using a callback:
```js
const fp = require('fastify-plugin')
module.exports = fp(function (fastify, opts, done) {
// your plugin code
done()
})
```
Example using an [async](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function) function:
```js
const fp = require('fastify-plugin')
// A callback function param is not required for async functions
module.exports = fp(async function (fastify, opts) {
// Wait for an async function to fulfill promise before proceeding
await exampleAsyncFunction()
})
```
## Metadata
In addition, if you use this module when creating new plugins, you can declare the dependencies, the name, and the expected Fastify version that your plugin needs.
#### Fastify version
If you need to set a bare-minimum version of Fastify for your plugin, just add the [semver](https://semver.org/) range that you need:
```js
const fp = require('fastify-plugin')
module.exports = fp(function (fastify, opts, done) {
// your plugin code
done()
}, { fastify: '5.x' })
```
If you need to check the Fastify version only, you can pass just the version string.
You can check [here](https://github.com/npm/node-semver#ranges) how to define a `semver` range.
#### Name
Fastify uses this option to validate the dependency graph, allowing it to ensure that no name collisions occur and making it possible to perform [dependency checks](https://github.com/fastify/fastify-plugin#dependencies).
```js
const fp = require('fastify-plugin')
function plugin (fastify, opts, done) {
// your plugin code
done()
}
module.exports = fp(plugin, {
fastify: '5.x',
name: 'your-plugin-name'
})
```
#### Dependencies
You can also check if the `plugins` and `decorators` that your plugin intend to use are present in the dependency graph.
> *Note:* This is the point where registering `name` of the plugins become important, because you can reference `plugin` dependencies by their [name](https://github.com/fastify/fastify-plugin#name).
```js
const fp = require('fastify-plugin')
function plugin (fastify, opts, done) {
// your plugin code
done()
}
module.exports = fp(plugin, {
fastify: '5.x',
decorators: {
fastify: ['plugin1', 'plugin2'],
reply: ['compress']
},
dependencies: ['plugin1-name', 'plugin2-name']
})
```
#### Encapsulate
By default, `fastify-plugin` breaks the [encapsulation](https://github.com/fastify/fastify/blob/HEAD/docs/Reference/Encapsulation.md) but you can optionally keep the plugin encapsulated.
This allows you to set the plugin's name and validate its dependencies without making the plugin accessible.
```js
const fp = require('fastify-plugin')
function plugin (fastify, opts, done) {
// the decorator is not accessible outside this plugin
fastify.decorate('util', function() {})
done()
}
module.exports = fp(plugin, {
name: 'my-encapsulated-plugin',
fastify: '5.x',
decorators: {
fastify: ['plugin1', 'plugin2'],
reply: ['compress']
},
dependencies: ['plugin1-name', 'plugin2-name'],
encapsulate: true
})
```
#### Bundlers and Typescript
`fastify-plugin` adds a `.default` and `[name]` property to the passed in function.
The type definition would have to be updated to leverage this.
## Known Issue: TypeScript Contextual Inference
[Documentation Reference](https://www.typescriptlang.org/docs/handbook/functions.html#inferring-the-types)
It is common for developers to inline their plugin with fastify-plugin such as:
```js
fp((fastify, opts, done) => { done() })
fp(async (fastify, opts) => { return })
```
TypeScript can sometimes infer the types of the arguments for these functions. Plugins in Fastify are recommended to be typed using either `FastifyPluginCallback` or `FastifyPluginAsync`. These two definitions only differ in two ways:
1. The third argument `done` (the callback part)
2. The return type `FastifyPluginCallback` or `FastifyPluginAsync`
At this time, TypeScript inference is not smart enough to differentiate by definition argument length alone.
Thus, if you are a TypeScript developer please use on the following patterns instead:
```ts
// Callback
// Assign type directly
const pluginCallback: FastifyPluginCallback = (fastify, options, done) => { }
fp(pluginCallback)
// or define your own function declaration that satisfies the existing definitions
const pluginCallbackWithTypes = (fastify: FastifyInstance, options: FastifyPluginOptions, done: (error?: FastifyError) => void): void => { }
fp(pluginCallbackWithTypes)
// or inline
fp((fastify: FastifyInstance, options: FastifyPluginOptions, done: (error?: FastifyError) => void): void => { })
// Async
// Assign type directly
const pluginAsync: FastifyPluginAsync = async (fastify, options) => { }
fp(pluginAsync)
// or define your own function declaration that satisfies the existing definitions
const pluginAsyncWithTypes = async (fastify: FastifyInstance, options: FastifyPluginOptions): Promise<void> => { }
fp(pluginAsyncWithTypes)
// or inline
fp(async (fastify: FastifyInstance, options: FastifyPluginOptions): Promise<void> => { })
```
## Acknowledgments
This project is kindly sponsored by:
- [nearForm](https://nearform.com)
- [LetzDoIt](https://www.letzdoitapp.com/)
## License
Licensed under [MIT](./LICENSE).

View File

@@ -0,0 +1,6 @@
'use strict'
module.exports = require('neostandard')({
ignores: require('neostandard').resolveIgnoresFromGitignore(),
ts: true
})

View File

@@ -0,0 +1,25 @@
'use strict'
const fpStackTracePattern = /at\s(?:.*\.)?plugin\s.*\n\s*(.*)/
const fileNamePattern = /(\w*(\.\w*)*)\..*/
module.exports = function getPluginName (fn) {
if (fn.name.length > 0) return fn.name
const stackTraceLimit = Error.stackTraceLimit
Error.stackTraceLimit = 10
try {
throw new Error('anonymous function')
} catch (e) {
Error.stackTraceLimit = stackTraceLimit
return extractPluginName(e.stack)
}
}
function extractPluginName (stack) {
const m = stack.match(fpStackTracePattern)
// get last section of path and match for filename
return m ? m[1].split(/[/\\]/).slice(-1)[0].match(fileNamePattern)[1] : 'anonymous'
}
module.exports.extractPluginName = extractPluginName

View File

@@ -0,0 +1,10 @@
'use strict'
module.exports = function toCamelCase (name) {
if (name[0] === '@') {
name = name.slice(1).replace('/', '-')
}
return name.replace(/-(.)/g, function (match, g1) {
return g1.toUpperCase()
})
}

View File

@@ -0,0 +1,70 @@
{
"name": "fastify-plugin",
"version": "5.1.0",
"description": "Plugin helper for Fastify",
"main": "plugin.js",
"type": "commonjs",
"types": "types/plugin.d.ts",
"scripts": {
"lint": "eslint",
"lint:fix": "eslint --fix",
"test": "npm run test:unit && npm run test:typescript",
"test:unit": "c8 --100 node --test",
"test:coverage": "c8 node --test && c8 report --reporter=html",
"test:typescript": "tsd"
},
"repository": {
"type": "git",
"url": "git+https://github.com/fastify/fastify-plugin.git"
},
"keywords": [
"plugin",
"helper",
"fastify"
],
"author": "Tomas Della Vedova - @delvedor (http://delved.org)",
"contributors": [
{
"name": "Matteo Collina",
"email": "hello@matteocollina.com"
},
{
"name": "Manuel Spigolon",
"email": "behemoth89@gmail.com"
},
{
"name": "Aras Abbasi",
"email": "aras.abbasi@gmail.com"
},
{
"name": "Frazer Smith",
"email": "frazer.dev@icloud.com",
"url": "https://github.com/fdawgs"
}
],
"license": "MIT",
"bugs": {
"url": "https://github.com/fastify/fastify-plugin/issues"
},
"homepage": "https://github.com/fastify/fastify-plugin#readme",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"devDependencies": {
"@fastify/type-provider-typebox": "^5.1.0",
"@types/node": "^24.0.8",
"c8": "^10.1.2",
"eslint": "^9.17.0",
"fastify": "^5.0.0",
"neostandard": "^0.12.0",
"proxyquire": "^2.1.3",
"tsd": "^0.33.0"
}
}

View File

@@ -0,0 +1,67 @@
'use strict'
const getPluginName = require('./lib/getPluginName')
const toCamelCase = require('./lib/toCamelCase')
let count = 0
function plugin (fn, options = {}) {
let autoName = false
if (fn.default !== undefined) {
// Support for 'export default' behaviour in transpiled ECMAScript module
fn = fn.default
}
if (typeof fn !== 'function') {
throw new TypeError(
`fastify-plugin expects a function, instead got a '${typeof fn}'`
)
}
if (typeof options === 'string') {
options = {
fastify: options
}
}
if (
typeof options !== 'object' ||
Array.isArray(options) ||
options === null
) {
throw new TypeError('The options object should be an object')
}
if (options.fastify !== undefined && typeof options.fastify !== 'string') {
throw new TypeError(`fastify-plugin expects a version string, instead got '${typeof options.fastify}'`)
}
if (!options.name) {
autoName = true
options.name = getPluginName(fn) + '-auto-' + count++
}
fn[Symbol.for('skip-override')] = options.encapsulate !== true
fn[Symbol.for('fastify.display-name')] = options.name
fn[Symbol.for('plugin-meta')] = options
// Faux modules support
if (!fn.default) {
fn.default = fn
}
// TypeScript support for named imports
// See https://github.com/fastify/fastify/issues/2404 for more details
// The type definitions would have to be update to match this.
const camelCase = toCamelCase(options.name)
if (!autoName && !fn[camelCase]) {
fn[camelCase] = fn
}
return fn
}
module.exports = plugin
module.exports.default = plugin
module.exports.fastifyPlugin = plugin

View File

@@ -0,0 +1,110 @@
'use strict'
const { test } = require('node:test')
const fp = require('../plugin')
test('webpack removes require.main.filename', t => {
const filename = require.main.filename
const info = console.info
t.after(() => {
require.main.filename = filename
console.info = info
})
require.main.filename = null
console.info = function (msg) {
t.assert.fail('logged: ' + msg)
}
fp((_fastify, _opts, next) => {
next()
}, {
fastify: '^5.0.0'
})
})
test('support faux modules', (t) => {
const plugin = fp((_fastify, _opts, next) => {
next()
})
t.assert.strictEqual(plugin.default, plugin)
})
test('support faux modules does not override existing default field in babel module', (t) => {
const module = {
default: (_fastify, _opts, next) => next()
}
module.default.default = 'Existing default field'
const plugin = fp(module)
t.assert.strictEqual(plugin.default, 'Existing default field')
})
test('support ts named imports', (t) => {
const plugin = fp((_fastify, _opts, next) => {
next()
}, {
name: 'hello'
})
t.assert.strictEqual(plugin.hello, plugin)
})
test('from kebab-case to camelCase', (t) => {
const plugin = fp((_fastify, _opts, next) => {
next()
}, {
name: 'hello-world'
})
t.assert.strictEqual(plugin.helloWorld, plugin)
})
test('from @-prefixed named imports', (t) => {
const plugin = fp((_fastify, _opts, next) => {
next()
}, {
name: '@hello/world'
})
t.assert.strictEqual(plugin.helloWorld, plugin)
})
test('from @-prefixed named kebab-case to camelCase', (t) => {
const plugin = fp((_fastify, _opts, next) => {
next()
}, {
name: '@hello/my-world'
})
t.assert.strictEqual(plugin.helloMyWorld, plugin)
})
test('from kebab-case to camelCase multiple words', (t) => {
const plugin = fp((_fastify, _opts, next) => {
next()
}, {
name: 'hello-long-world'
})
t.assert.strictEqual(plugin.helloLongWorld, plugin)
})
test('from kebab-case to camelCase multiple words does not override', (t) => {
const fn = (_fastify, _opts, next) => {
next()
}
const foobar = {}
fn.helloLongWorld = foobar
const plugin = fp(fn, {
name: 'hello-long-world'
})
t.assert.strictEqual(plugin.helloLongWorld, foobar)
})

View File

@@ -0,0 +1,67 @@
'use strict'
const { test } = require('node:test')
const fp = require('../plugin')
test('checkVersion having require.main.filename', (t) => {
const info = console.info
t.assert.ok(require.main.filename)
t.after(() => {
console.info = info
})
console.info = function (msg) {
t.assert.fail('logged: ' + msg)
}
fp((_fastify, _opts, next) => {
next()
}, {
fastify: '^5.0.0'
})
})
test('checkVersion having no require.main.filename but process.argv[1]', (t) => {
const filename = require.main.filename
const info = console.info
t.after(() => {
require.main.filename = filename
console.info = info
})
require.main.filename = null
console.info = function (msg) {
t.assert.fail('logged: ' + msg)
}
fp((_fastify, _opts, next) => {
next()
}, {
fastify: '^5.0.0'
})
})
test('checkVersion having no require.main.filename and no process.argv[1]', (t) => {
const filename = require.main.filename
const argv = process.argv
const info = console.info
t.after(() => {
require.main.filename = filename
process.argv = argv
console.info = info
})
require.main.filename = null
process.argv[1] = null
console.info = function (msg) {
t.assert.fail('logged: ' + msg)
}
fp((_fastify, _opts, next) => {
next()
}, {
fastify: '^5.0.0'
})
})

View File

@@ -0,0 +1,14 @@
'use strict'
const { test } = require('node:test')
const fp = require('../plugin')
test('anonymous function should be named composite.test0', (t) => {
t.plan(2)
const fn = fp((_fastify, _opts, next) => {
next()
})
t.assert.strictEqual(fn[Symbol.for('plugin-meta')].name, 'composite.test-auto-0')
t.assert.strictEqual(fn[Symbol.for('fastify.display-name')], 'composite.test-auto-0')
})

View File

@@ -0,0 +1,11 @@
import { test } from 'node:test'
import fp from '../../plugin.js'
test('esm base support', (t) => {
fp((_fastify, _opts, next) => {
next()
}, {
fastify: '^5.0.0'
})
t.assert.ok(true, 'fp function called without throwing an error')
})

View File

@@ -0,0 +1,11 @@
'use strict'
// Node v8 throw a `SyntaxError: Unexpected token import`
// even if this branch is never touch in the code,
// by using `eval` we can avoid this issue.
// eslint-disable-next-line
new Function('module', 'return import(module)')('./esm.mjs').catch((err) => {
process.nextTick(() => {
throw err
})
})

View File

@@ -0,0 +1,49 @@
'use strict'
const { test } = require('node:test')
const extractPluginName = require('../lib/getPluginName').extractPluginName
const winStack = `Error: anonymous function
at checkName (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\index.js:43:11)
at plugin (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\index.js:24:20)
at Test.test (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\test\\hello.test.js:9:14)
at bound (domain.js:396:14)
at Test.runBound (domain.js:409:12)
at ret (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\node_modules\\tap\\lib\\test.js:278:21)
at Test.main (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\node_modules\\tap\\lib\\test.js:282:7)
at writeSubComment (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\node_modules\\tap\\lib\\test.js:371:13)
at TAP.writeSubComment (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\node_modules\\tap\\lib\\test.js:403:5)
at Test.runBeforeEach (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\node_modules\\tap\\lib\\test.js:370:14)
at loop (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\node_modules\\function-loop\\index.js:35:15)
at TAP.runBeforeEach (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\node_modules\\tap\\lib\\test.js:683:7)
at TAP.processSubtest (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\node_modules\\tap\\lib\\test.js:369:12)
at TAP.process (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\node_modules\\tap\\lib\\test.js:306:14)
at TAP.sub (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\node_modules\\tap\\lib\\test.js:185:10)
at TAP.test (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\node_modules\\tap\\lib\\test.js:209:17)`
const nixStack = `Error: anonymous function
at checkName (/home/leonardo/desktop/fastify-plugin/index.js:43:11)
at plugin (/home/leonardo/desktop/fastify-plugin/index.js:24:20)
at Test.test (/home/leonardo/desktop/fastify-plugin/test/this.is.a.test.js:9:14)
at bound (domain.js:396:14)
at Test.runBound (domain.js:409:12)
at ret (/home/leonardo/desktop/fastify-plugin/node_modules/tap/lib/test.js:278:21)
at Test.main (/home/leonardo/desktop/fastify-plugin/node_modules/tap/lib/test.js:282:7)
at writeSubComment (/home/leonardo/desktop/fastify-plugin/node_modules/tap/lib/test.js:371:13)
at TAP.writeSubComment (/home/leonardo/desktop/fastify-plugin/node_modules/tap/lib/test.js:403:5)
at Test.runBeforeEach (/home/leonardo/desktop/fastify-plugin/node_modules/tap/lib/test.js:370:14)
at loop (/home/leonardo/desktop/fastify-plugin/node_modules/function-loop/index.js:35:15)
at TAP.runBeforeEach (/home/leonardo/desktop/fastify-plugin/node_modules/tap/lib/test.js:683:7)
at TAP.processSubtest (/home/leonardo/desktop/fastify-plugin/node_modules/tap/lib/test.js:369:12)
at TAP.process (/home/leonardo/desktop/fastify-plugin/node_modules/tap/lib/test.js:306:14)
at TAP.sub (/home/leonardo/desktop/fastify-plugin/node_modules/tap/lib/test.js:185:10)
at TAP.test (/home/leonardo/desktop/fastify-plugin/node_modules/tap/lib/test.js:209:17)`
const anonymousStack = 'Unable to parse this'
test('extractPluginName tests', (t) => {
t.plan(3)
t.assert.strictEqual(extractPluginName(winStack), 'hello.test')
t.assert.strictEqual(extractPluginName(nixStack), 'this.is.a.test')
t.assert.strictEqual(extractPluginName(anonymousStack), 'anonymous')
})

View File

@@ -0,0 +1,15 @@
'use strict'
const { test } = require('node:test')
const fp = require('../plugin')
test('anonymous function should be named mu1tip1e.composite.test', (t) => {
t.plan(2)
const fn = fp((_fastify, _opts, next) => {
next()
})
t.assert.strictEqual(fn[Symbol.for('plugin-meta')].name, 'mu1tip1e.composite.test-auto-0')
t.assert.strictEqual(fn[Symbol.for('fastify.display-name')], 'mu1tip1e.composite.test-auto-0')
})

View File

@@ -0,0 +1,396 @@
'use strict'
const { test } = require('node:test')
const proxyquire = require('proxyquire')
const fp = require('../plugin')
const Fastify = require('fastify')
const pkg = require('../package.json')
test('fastify-plugin is a function', (t) => {
t.plan(1)
t.assert.ok(typeof fp === 'function')
})
test('should return the function with the skip-override Symbol', (t) => {
t.plan(1)
function plugin (_fastify, _opts, next) {
next()
}
fp(plugin)
t.assert.ok(plugin[Symbol.for('skip-override')])
})
test('should support "default" function from babel module', (t) => {
t.plan(1)
const plugin = {
default: () => { }
}
try {
fp(plugin)
t.assert.ok(true)
} catch (e) {
t.assert.strictEqual(e.message, 'fastify-plugin expects a function, instead got a \'object\'')
}
})
test('should throw if the plugin is not a function', (t) => {
t.plan(1)
try {
fp('plugin')
t.assert.fail()
} catch (e) {
t.assert.strictEqual(e.message, 'fastify-plugin expects a function, instead got a \'string\'')
}
})
test('should check the fastify version', (t) => {
t.plan(1)
function plugin (_fastify, _opts, next) {
next()
}
try {
fp(plugin, { fastify: '>=0.10.0' })
t.assert.ok(true)
} catch {
t.assert.fail()
}
})
test('should check the fastify version', (t) => {
t.plan(1)
function plugin (_fastify, _opts, next) {
next()
}
try {
fp(plugin, '>=0.10.0')
t.assert.ok(true)
} catch {
t.assert.fail()
}
})
test('the options object should be an object', (t) => {
t.plan(2)
try {
fp(() => { }, null)
t.assert.fail()
} catch (e) {
t.assert.strictEqual(e.message, 'The options object should be an object')
}
try {
fp(() => { }, [])
t.assert.fail()
} catch (e) {
t.assert.strictEqual(e.message, 'The options object should be an object')
}
})
test('should throw if the version number is not a string', (t) => {
t.plan(1)
try {
fp(() => { }, { fastify: 12 })
t.assert.fail()
} catch (e) {
t.assert.strictEqual(e.message, 'fastify-plugin expects a version string, instead got \'number\'')
}
})
test('Should accept an option object', (t) => {
t.plan(2)
const opts = { hello: 'world' }
function plugin (_fastify, _opts, next) {
next()
}
fp(plugin, opts)
t.assert.ok(plugin[Symbol.for('skip-override')], 'skip-override symbol should be present')
t.assert.deepStrictEqual(plugin[Symbol.for('plugin-meta')], opts, 'plugin-meta should match opts')
})
test('Should accept an option object and checks the version', (t) => {
t.plan(2)
const opts = { hello: 'world', fastify: '>=0.10.0' }
function plugin (_fastify, _opts, next) {
next()
}
fp(plugin, opts)
t.assert.ok(plugin[Symbol.for('skip-override')])
t.assert.deepStrictEqual(plugin[Symbol.for('plugin-meta')], opts)
})
test('should set anonymous function name to file it was called from with a counter', (t) => {
const fp = proxyquire('../plugin.js', { stubs: {} })
const fn = fp((_fastify, _opts, next) => {
next()
})
t.assert.strictEqual(fn[Symbol.for('plugin-meta')].name, 'test-auto-0')
t.assert.strictEqual(fn[Symbol.for('fastify.display-name')], 'test-auto-0')
const fn2 = fp((_fastify, _opts, next) => {
next()
})
t.assert.strictEqual(fn2[Symbol.for('plugin-meta')].name, 'test-auto-1')
t.assert.strictEqual(fn2[Symbol.for('fastify.display-name')], 'test-auto-1')
})
test('should set function name if Error.stackTraceLimit is set to 0', (t) => {
const stackTraceLimit = Error.stackTraceLimit = 0
const fp = proxyquire('../plugin.js', { stubs: {} })
const fn = fp((_fastify, _opts, next) => {
next()
})
t.assert.strictEqual(fn[Symbol.for('plugin-meta')].name, 'test-auto-0')
t.assert.strictEqual(fn[Symbol.for('fastify.display-name')], 'test-auto-0')
const fn2 = fp((_fastify, _opts, next) => {
next()
})
t.assert.strictEqual(fn2[Symbol.for('plugin-meta')].name, 'test-auto-1')
t.assert.strictEqual(fn2[Symbol.for('fastify.display-name')], 'test-auto-1')
Error.stackTraceLimit = stackTraceLimit
})
test('should set display-name to meta name', (t) => {
t.plan(2)
const functionName = 'superDuperSpecialFunction'
const fn = fp((_fastify, _opts, next) => next(), {
name: functionName
})
t.assert.strictEqual(fn[Symbol.for('plugin-meta')].name, functionName)
t.assert.strictEqual(fn[Symbol.for('fastify.display-name')], functionName)
})
test('should preserve fastify version in meta', (t) => {
t.plan(1)
const opts = { hello: 'world', fastify: '>=0.10.0' }
const fn = fp((_fastify, _opts, next) => next(), opts)
t.assert.strictEqual(fn[Symbol.for('plugin-meta')].fastify, '>=0.10.0')
})
test('should check fastify dependency graph - plugin', async (t) => {
t.plan(1)
const fastify = Fastify()
fastify.register(fp((_fastify, _opts, next) => next(), {
fastify: '5.x',
name: 'plugin1-name'
}))
fastify.register(fp((_fastify, _opts, next) => next(), {
fastify: '5.x',
name: 'test',
dependencies: ['plugin1-name', 'plugin2-name']
}))
await t.assert.rejects(fastify.ready(), { message: "The dependency 'plugin2-name' of plugin 'test' is not registered" })
})
test('should check fastify dependency graph - decorate', async (t) => {
t.plan(1)
const fastify = Fastify()
fastify.decorate('plugin1', fp((_fastify, _opts, next) => next(), {
fastify: '5.x',
name: 'plugin1-name'
}))
fastify.register(fp((_fastify, _opts, next) => next(), {
fastify: '5.x',
name: 'test',
decorators: { fastify: ['plugin1', 'plugin2'] }
}))
await t.assert.rejects(fastify.ready(), { message: "The decorator 'plugin2' required by 'test' is not present in Fastify" })
})
test('should check fastify dependency graph - decorateReply', async (t) => {
t.plan(1)
const fastify = Fastify()
fastify.decorateReply('plugin1', fp((_fastify, _opts, next) => next(), {
fastify: '5.x',
name: 'plugin1-name'
}))
fastify.register(fp((_fastify, _opts, next) => next(), {
fastify: '5.x',
name: 'test',
decorators: { reply: ['plugin1', 'plugin2'] }
}))
await t.assert.rejects(fastify.ready(), { message: "The decorator 'plugin2' required by 'test' is not present in Reply" })
})
test('should accept an option to encapsulate', async (t) => {
t.plan(3)
const fastify = Fastify()
fastify.register(fp((fastify, _opts, next) => {
fastify.decorate('accessible', true)
next()
}, {
name: 'accessible-plugin'
}))
fastify.register(fp((fastify, _opts, next) => {
fastify.decorate('alsoAccessible', true)
next()
}, {
name: 'accessible-plugin2',
encapsulate: false
}))
fastify.register(fp((fastify, _opts, next) => {
fastify.decorate('encapsulated', true)
next()
}, {
name: 'encapsulated-plugin',
encapsulate: true
}))
await fastify.ready()
t.assert.ok(fastify.hasDecorator('accessible'))
t.assert.ok(fastify.hasDecorator('alsoAccessible'))
t.assert.ok(!fastify.hasDecorator('encapsulated'))
})
test('should check dependencies when encapsulated', async (t) => {
t.plan(1)
const fastify = Fastify()
fastify.register(fp((_fastify, _opts, next) => next(), {
name: 'test',
dependencies: ['missing-dependency-name'],
encapsulate: true
}))
await t.assert.rejects(fastify.ready(), { message: "The dependency 'missing-dependency-name' of plugin 'test' is not registered" })
})
test(
'should check version when encapsulated',
{ skip: /\d-.+/.test(pkg.devDependencies.fastify) },
async (t) => {
t.plan(1)
const fastify = Fastify()
fastify.register(fp((_fastify, _opts, next) => next(), {
name: 'test',
fastify: '<=2.10.0',
encapsulate: true
}))
await t.assert.rejects(fastify.ready(), { message: /fastify-plugin: test - expected '<=2.10.0' fastify version, '\d.\d+.\d+' is installed/ })
}
)
test('should check decorators when encapsulated', async (t) => {
t.plan(1)
const fastify = Fastify()
fastify.decorate('plugin1', 'foo')
fastify.register(fp((_fastify, _opts, next) => next(), {
fastify: '5.x',
name: 'test',
encapsulate: true,
decorators: { fastify: ['plugin1', 'plugin2'] }
}))
await t.assert.rejects(fastify.ready(), { message: "The decorator 'plugin2' required by 'test' is not present in Fastify" })
})
test('plugin name when encapsulated', async (t) => {
t.plan(6)
const fastify = Fastify()
fastify.register(function plugin (_instance, _opts, next) {
next()
})
fastify.register(fp(getFn('hello'), {
fastify: '5.x',
name: 'hello',
encapsulate: true
}))
fastify.register(function plugin (fastify, _opts, next) {
fastify.register(fp(getFn('deep'), {
fastify: '5.x',
name: 'deep',
encapsulate: true
}))
fastify.register(fp(function genericPlugin (fastify, _opts, next) {
t.assert.strictEqual(fastify.pluginName, 'deep-deep', 'should be deep-deep')
fastify.register(fp(getFn('deep-deep-deep'), {
fastify: '5.x',
name: 'deep-deep-deep',
encapsulate: true
}))
fastify.register(fp(getFn('deep-deep -> not-encapsulated-2'), {
fastify: '5.x',
name: 'not-encapsulated-2'
}))
next()
}, {
fastify: '5.x',
name: 'deep-deep',
encapsulate: true
}))
fastify.register(fp(getFn('plugin -> not-encapsulated'), {
fastify: '5.x',
name: 'not-encapsulated'
}))
next()
})
await fastify.ready()
function getFn (expectedName) {
return function genericPlugin (fastify, _opts, next) {
t.assert.strictEqual(fastify.pluginName, expectedName, `should be ${expectedName}`)
next()
}
}
})

View File

@@ -0,0 +1,24 @@
'use strict'
const { test } = require('node:test')
const toCamelCase = require('../lib/toCamelCase')
test('from kebab-case to camelCase', (t) => {
t.plan(1)
t.assert.strictEqual(toCamelCase('hello-world'), 'helloWorld')
})
test('from @-prefixed named imports', (t) => {
t.plan(1)
t.assert.strictEqual(toCamelCase('@hello/world'), 'helloWorld')
})
test('from @-prefixed named kebab-case to camelCase', (t) => {
t.plan(1)
t.assert.strictEqual(toCamelCase('@hello/my-world'), 'helloMyWorld')
})
test('from kebab-case to camelCase multiple words', (t) => {
t.plan(1)
t.assert.strictEqual(toCamelCase('hello-long-world'), 'helloLongWorld')
})

View File

@@ -0,0 +1,19 @@
import { FastifyPluginAsync } from 'fastify'
type FastifyExampleAsync = FastifyPluginAsync<fastifyExampleAsync.FastifyExampleAsyncOptions>
declare namespace fastifyExampleAsync {
export interface FastifyExampleAsyncOptions {
foo?: 'bar'
}
export interface FastifyExampleAsyncPluginOptions extends FastifyExampleAsyncOptions {
}
export const fastifyExampleAsync: FastifyExampleAsync
export { fastifyExampleAsync as default }
}
declare function fastifyExampleAsync (...params: Parameters<FastifyExampleAsync>): ReturnType<FastifyExampleAsync>
export default fastifyExampleAsync

View File

@@ -0,0 +1,19 @@
import { FastifyPluginCallback } from 'fastify'
type FastifyExampleCallback = FastifyPluginCallback<fastifyExampleCallback.FastifyExampleCallbackOptions>
declare namespace fastifyExampleCallback {
export interface FastifyExampleCallbackOptions {
foo?: 'bar'
}
export interface FastifyExampleCallbackPluginOptions extends FastifyExampleCallbackOptions {
}
export const fastifyExampleCallback: FastifyExampleCallback
export { fastifyExampleCallback as default }
}
declare function fastifyExampleCallback (...params: Parameters<FastifyExampleCallback>): ReturnType<FastifyExampleCallback>
export default fastifyExampleCallback

View File

@@ -0,0 +1,61 @@
/// <reference types="fastify" />
import {
FastifyPluginCallback,
FastifyPluginAsync,
FastifyPluginOptions,
RawServerBase,
RawServerDefault,
FastifyTypeProvider,
FastifyTypeProviderDefault,
FastifyBaseLogger,
} from 'fastify'
type FastifyPlugin = typeof fastifyPlugin
declare namespace fastifyPlugin {
export interface PluginMetadata {
/** Bare-minimum version of Fastify for your plugin, just add the semver range that you need. */
fastify?: string,
name?: string,
/** Decorator dependencies for this plugin */
decorators?: {
fastify?: (string | symbol)[],
reply?: (string | symbol)[],
request?: (string | symbol)[]
},
/** The plugin dependencies */
dependencies?: string[],
encapsulate?: boolean
}
// Exporting PluginOptions for backward compatibility after renaming it to PluginMetadata
/**
* @deprecated Use PluginMetadata instead
*/
export interface PluginOptions extends PluginMetadata {}
export const fastifyPlugin: FastifyPlugin
export { fastifyPlugin as default }
}
/**
* This function does three things for you:
* 1. Add the `skip-override` hidden property
* 2. Check bare-minimum version of Fastify
* 3. Pass some custom metadata of the plugin to Fastify
* @param fn Fastify plugin function
* @param options Optional plugin options
*/
declare function fastifyPlugin<
Options extends FastifyPluginOptions = Record<never, never>,
RawServer extends RawServerBase = RawServerDefault,
TypeProvider extends FastifyTypeProvider = FastifyTypeProviderDefault,
Logger extends FastifyBaseLogger = FastifyBaseLogger,
Fn extends FastifyPluginCallback<Options, RawServer, TypeProvider, Logger> | FastifyPluginAsync<Options, RawServer, TypeProvider, Logger> = FastifyPluginCallback<Options, RawServer, TypeProvider, Logger>
> (
fn: Fn extends unknown ? Fn extends (...args: any) => Promise<any> ? FastifyPluginAsync<Options, RawServer, TypeProvider, Logger> : FastifyPluginCallback<Options, RawServer, TypeProvider, Logger> : Fn,
options?: fastifyPlugin.PluginMetadata | string
): Fn
export = fastifyPlugin

View File

@@ -0,0 +1,166 @@
import fastifyPlugin from '..'
import fastify, { FastifyPluginCallback, FastifyPluginAsync, FastifyError, FastifyInstance, FastifyPluginOptions, RawServerDefault, FastifyTypeProviderDefault, FastifyBaseLogger } from 'fastify'
import { expectAssignable, expectError, expectNotType, expectType } from 'tsd'
import { Server } from 'node:https'
import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox'
import fastifyExampleCallback from './example-callback.test-d'
import fastifyExampleAsync from './example-async.test-d'
interface Options {
foo: string
}
const testSymbol = Symbol('foobar')
// Callback
const pluginCallback: FastifyPluginCallback = (_fastify, _options, _next) => { }
expectType<FastifyPluginCallback>(fastifyPlugin(pluginCallback))
const pluginCallbackWithTypes = (_fastify: FastifyInstance, _options: FastifyPluginOptions, _next: (error?: FastifyError) => void): void => { }
expectAssignable<FastifyPluginCallback>(fastifyPlugin(pluginCallbackWithTypes))
expectNotType<any>(fastifyPlugin(pluginCallbackWithTypes))
expectAssignable<FastifyPluginCallback>(fastifyPlugin((_fastify: FastifyInstance, _options: FastifyPluginOptions, _next: (error?: FastifyError) => void): void => { }))
expectNotType<any>(fastifyPlugin((_fastify: FastifyInstance, _options: FastifyPluginOptions, _next: (error?: FastifyError) => void): void => { }))
expectType<FastifyPluginCallback>(fastifyPlugin(pluginCallback, ''))
expectType<FastifyPluginCallback>(fastifyPlugin(pluginCallback, {
fastify: '',
name: '',
decorators: {
fastify: ['', testSymbol],
reply: ['', testSymbol],
request: ['', testSymbol]
},
dependencies: [''],
encapsulate: true
}))
const pluginCallbackWithOptions: FastifyPluginCallback<Options> = (_fastify, options, _next) => {
expectType<string>(options.foo)
}
expectType<FastifyPluginCallback<Options>>(fastifyPlugin(pluginCallbackWithOptions))
const pluginCallbackWithServer: FastifyPluginCallback<Options, Server> = (fastify, _options, _next) => {
expectType<Server>(fastify.server)
}
expectType<FastifyPluginCallback<Options, Server>>(fastifyPlugin(pluginCallbackWithServer))
const pluginCallbackWithTypeProvider: FastifyPluginCallback<Options, Server, TypeBoxTypeProvider> = (_fastify, _options, _next) => { }
expectType<FastifyPluginCallback<Options, Server, TypeBoxTypeProvider>>(fastifyPlugin(pluginCallbackWithTypeProvider))
// Async
const pluginAsync: FastifyPluginAsync = async (_fastify, _options) => { }
expectType<FastifyPluginAsync>(fastifyPlugin(pluginAsync))
const pluginAsyncWithTypes = async (_fastify: FastifyInstance, _options: FastifyPluginOptions): Promise<void> => { }
expectType<FastifyPluginAsync<FastifyPluginOptions, RawServerDefault, FastifyTypeProviderDefault>>(fastifyPlugin(pluginAsyncWithTypes))
expectType<FastifyPluginAsync<FastifyPluginOptions, RawServerDefault, FastifyTypeProviderDefault>>(fastifyPlugin(async (_fastify: FastifyInstance, _options: FastifyPluginOptions): Promise<void> => { }))
expectType<FastifyPluginAsync>(fastifyPlugin(pluginAsync, ''))
expectType<FastifyPluginAsync>(fastifyPlugin(pluginAsync, {
fastify: '',
name: '',
decorators: {
fastify: ['', testSymbol],
reply: ['', testSymbol],
request: ['', testSymbol]
},
dependencies: [''],
encapsulate: true
}))
const pluginAsyncWithOptions: FastifyPluginAsync<Options> = async (_fastify, options) => {
expectType<string>(options.foo)
}
expectType<FastifyPluginAsync<Options>>(fastifyPlugin(pluginAsyncWithOptions))
const pluginAsyncWithServer: FastifyPluginAsync<Options, Server> = async (fastify, _options) => {
expectType<Server>(fastify.server)
}
expectType<FastifyPluginAsync<Options, Server>>(fastifyPlugin(pluginAsyncWithServer))
const pluginAsyncWithTypeProvider: FastifyPluginAsync<Options, Server, TypeBoxTypeProvider> = async (_fastify, _options) => { }
expectType<FastifyPluginAsync<Options, Server, TypeBoxTypeProvider>>(fastifyPlugin(pluginAsyncWithTypeProvider))
// Fastify register
const server = fastify()
server.register(fastifyPlugin(pluginCallback))
server.register(fastifyPlugin(pluginCallbackWithTypes), { foo: 'bar' })
server.register(fastifyPlugin(pluginCallbackWithOptions), { foo: 'bar' })
server.register(fastifyPlugin(pluginCallbackWithServer), { foo: 'bar' })
server.register(fastifyPlugin(pluginCallbackWithTypeProvider), { foo: 'bar' })
server.register(fastifyPlugin(pluginAsync))
server.register(fastifyPlugin(pluginAsyncWithTypes), { foo: 'bar' })
server.register(fastifyPlugin(pluginAsyncWithOptions), { foo: 'bar' })
server.register(fastifyPlugin(pluginAsyncWithServer), { foo: 'bar' })
server.register(fastifyPlugin(pluginAsyncWithTypeProvider), { foo: 'bar' })
// properly handling callback and async
fastifyPlugin(function (fastify, options, next) {
expectType<FastifyInstance>(fastify)
expectType<Record<never, never>>(options)
expectType<(err?: Error) => void>(next)
})
fastifyPlugin<Options>(function (fastify, options, next) {
expectType<FastifyInstance>(fastify)
expectType<Options>(options)
expectType<(err?: Error) => void>(next)
})
fastifyPlugin<Options>(async function (fastify, options) {
expectType<FastifyInstance>(fastify)
expectType<Options>(options)
})
expectAssignable<FastifyPluginAsync<Options, RawServerDefault, FastifyTypeProviderDefault, FastifyBaseLogger>>(fastifyPlugin(async function (_fastify: FastifyInstance, _options: Options) { }))
expectNotType<any>(fastifyPlugin(async function (_fastify: FastifyInstance, _options: Options) { }))
fastifyPlugin(async function (fastify, options: Options) {
expectType<FastifyInstance>(fastify)
expectType<Options>(options)
})
fastifyPlugin(async function (fastify, options) {
expectType<FastifyInstance>(fastify)
expectType<Record<never, never>>(options)
})
expectError(
fastifyPlugin(async function (fastify, options: Options, _next) {
expectType<FastifyInstance>(fastify)
expectType<Options>(options)
})
)
expectAssignable<FastifyPluginCallback<Options>>(fastifyPlugin(function (_fastify, _options, _next) { }))
expectNotType<any>(fastifyPlugin(function (_fastify, _options, _next) { }))
fastifyPlugin(function (fastify, options: Options, next) {
expectType<FastifyInstance>(fastify)
expectType<Options>(options)
expectType<(err?: Error) => void>(next)
})
expectError(
fastifyPlugin(function (fastify, options: Options, _next) {
expectType<FastifyInstance>(fastify)
expectType<Options>(options)
return Promise.resolve()
})
)
server.register(fastifyExampleCallback, { foo: 'bar' })
expectError(server.register(fastifyExampleCallback, { foo: 'baz' }))
server.register(fastifyExampleAsync, { foo: 'bar' })
expectError(server.register(fastifyExampleAsync, { foo: 'baz' }))

82
backend/node_modules/@fastify/helmet/package.json generated vendored Normal file
View File

@@ -0,0 +1,82 @@
{
"name": "@fastify/helmet",
"version": "13.0.2",
"description": "Important security headers for Fastify",
"main": "index.js",
"type": "commonjs",
"types": "types/index.d.ts",
"scripts": {
"lint": "eslint",
"lint:fix": "eslint --fix",
"test": "npm run test:unit && npm run test:typescript",
"test:typescript": "tsd",
"test:unit": "c8 --100 node --test"
},
"repository": {
"type": "git",
"url": "git+https://github.com/fastify/fastify-helmet.git"
},
"keywords": [
"fastify",
"helmet",
"security",
"headers",
"x-frame-options",
"csp",
"hsts",
"clickjack"
],
"author": "Matteo Collina <hello@matteocollina.com>",
"contributors": [
{
"name": "Tomas Della Vedova",
"url": "http://delved.org"
},
{
"name": "Manuel Spigolon",
"email": "behemoth89@gmail.com"
},
{
"name": "Maksim Sinik",
"url": "https://maksim.dev"
},
{
"name": "Frazer Smith",
"email": "frazer.dev@icloud.com",
"url": "https://github.com/fdawgs"
}
],
"license": "MIT",
"bugs": {
"url": "https://github.com/fastify/fastify-helmet/issues"
},
"homepage": "https://github.com/fastify/fastify-helmet#readme",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"devDependencies": {
"@types/node": "^24.3.0",
"c8": "^10.1.2",
"eslint": "^9.17.0",
"fastify": "^5.0.0",
"neostandard": "^0.12.0",
"tsd": "^0.33.0"
},
"dependencies": {
"fastify-plugin": "^5.0.0",
"helmet": "^8.0.0"
},
"tsd": {
"directory": "test/types"
},
"publishConfig": {
"access": "public"
}
}

1028
backend/node_modules/@fastify/helmet/test/global.test.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,499 @@
'use strict'
const { test } = require('node:test')
const Fastify = require('fastify')
const helmet = require('..')
test('It should apply route specific helmet options over the global options', async (t) => {
t.plan(2)
const fastify = Fastify()
await fastify.register(helmet, { global: true })
fastify.get('/', { helmet: { frameguard: false } }, (_request, reply) => {
reply.send({ hello: 'world' })
})
const response = await fastify.inject({
method: 'GET',
path: '/'
})
const notExpected = {
'x-frame-options': 'SAMEORIGIN'
}
const expected = {
'x-dns-prefetch-control': 'off',
'x-download-options': 'noopen',
'x-content-type-options': 'nosniff',
'x-xss-protection': '0'
}
const actualResponseHeaders = {
'x-dns-prefetch-control': response.headers['x-dns-prefetch-control'],
'x-download-options': response.headers['x-download-options'],
'x-content-type-options': response.headers['x-content-type-options'],
'x-xss-protection': response.headers['x-xss-protection']
}
t.assert.notDeepStrictEqual(
response.headers['x-frame-options'],
notExpected['x-frame-options']
)
t.assert.deepStrictEqual(actualResponseHeaders, expected)
})
test('It should disable helmet on specific route when route `helmet` option is set to `false`', async (t) => {
t.plan(2)
const fastify = Fastify()
await fastify.register(helmet, { global: true })
fastify.get('/disabled', { helmet: false }, (_request, reply) => {
reply.send({ hello: 'disabled' })
})
fastify.get('/enabled', (_request, reply) => {
reply.send({ hello: 'enabled' })
})
const helmetHeaders = {
'x-frame-options': 'SAMEORIGIN',
'x-dns-prefetch-control': 'off',
'x-download-options': 'noopen',
'x-content-type-options': 'nosniff',
'x-xss-protection': '0'
}
await fastify
.inject({
method: 'GET',
path: '/disabled'
})
.then((response) => {
const actualResponseHeaders = {
'x-frame-options': response.headers['x-frame-options'],
'x-dns-prefetch-control': response.headers['x-dns-prefetch-control'],
'x-download-options': response.headers['x-download-options'],
'x-content-type-options': response.headers['x-content-type-options'],
'x-xss-protection': response.headers['x-xss-protection']
}
t.assert.notDeepStrictEqual(actualResponseHeaders, helmetHeaders)
})
.catch((err) => {
t.assert.fail(err)
})
await fastify
.inject({
method: 'GET',
path: '/enabled'
})
.then((response) => {
const actualResponseHeaders = {
'x-frame-options': response.headers['x-frame-options'],
'x-dns-prefetch-control': response.headers['x-dns-prefetch-control'],
'x-download-options': response.headers['x-download-options'],
'x-content-type-options': response.headers['x-content-type-options'],
'x-xss-protection': response.headers['x-xss-protection']
}
t.assert.deepStrictEqual(actualResponseHeaders, helmetHeaders)
})
.catch((err) => {
t.assert.fail(err)
})
})
test('It should add CSPNonce decorator and hooks when route `enableCSPNonces` option is set to `true`', async (t) => {
t.plan(4)
const fastify = Fastify()
await fastify.register(helmet, {
global: false,
enableCSPNonces: false,
contentSecurityPolicy: {
directives: {
'script-src': ["'self'", "'unsafe-eval'", "'unsafe-inline'"],
'style-src': ["'self'", "'unsafe-inline'"]
}
}
})
fastify.get(
'/',
{
helmet: {
enableCSPNonces: true
}
},
(_request, reply) => {
t.assert.ok(reply.cspNonce)
reply.send(reply.cspNonce)
}
)
const response = await fastify.inject({ method: 'GET', path: '/' })
const cspCache = response.json()
const expected = {
'content-security-policy': `script-src 'self' 'unsafe-eval' 'unsafe-inline' 'nonce-${cspCache.script}';style-src 'self' 'unsafe-inline' 'nonce-${cspCache.style}';default-src 'self';base-uri 'self';font-src 'self' https: data:;form-action 'self';frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src-attr 'none';upgrade-insecure-requests`
}
const actualResponseHeaders = {
'content-security-policy': response.headers['content-security-policy']
}
t.assert.ok(cspCache.script)
t.assert.ok(cspCache.style)
t.assert.deepStrictEqual(actualResponseHeaders, expected)
})
test('It should add CSPNonce decorator and hooks with default options when route `enableCSPNonces` option is set to `true`', async (t) => {
t.plan(8)
const fastify = Fastify()
await fastify.register(helmet, {
global: false,
enableCSPNonces: false
})
fastify.get('/no-csp', (_request, reply) => {
t.assert.equal(reply.cspNonce, null)
reply.send({ message: 'no csp' })
})
fastify.get(
'/with-csp',
{
helmet: {
enableCSPNonces: true
}
},
(_request, reply) => {
t.assert.ok(reply.cspNonce)
reply.send(reply.cspNonce)
}
)
fastify.inject({
method: 'GET',
path: '/no-csp'
})
let response
response = await fastify.inject({ method: 'GET', path: '/with-csp' })
const cspCache = response.json()
t.assert.ok(cspCache.script)
t.assert.ok(cspCache.style)
response = await fastify.inject({ method: 'GET', path: '/with-csp' })
const newCsp = response.json()
t.assert.notEqual(cspCache, newCsp)
t.assert.ok(cspCache.script)
t.assert.ok(cspCache.style)
})
test('It should not add CSPNonce decorator when route `enableCSPNonces` option is set to `false`', async (t) => {
t.plan(8)
const fastify = Fastify()
await fastify.register(helmet, {
global: true,
enableCSPNonces: true
})
fastify.get('/with-csp', (_request, reply) => {
t.assert.ok(reply.cspNonce)
reply.send(reply.cspNonce)
})
fastify.get(
'/no-csp',
{ helmet: { enableCSPNonces: false } },
(_request, reply) => {
t.assert.equal(reply.cspNonce, null)
reply.send({ message: 'no csp' })
}
)
fastify.inject({
method: 'GET',
path: '/no-csp'
})
let response
response = await fastify.inject({ method: 'GET', path: '/with-csp' })
const cspCache = response.json()
t.assert.ok(cspCache.script)
t.assert.ok(cspCache.style)
response = await fastify.inject({ method: 'GET', path: '/with-csp' })
const newCsp = response.json()
t.assert.notEqual(cspCache, newCsp)
t.assert.ok(cspCache.script)
t.assert.ok(cspCache.style)
})
test('It should not set default directives when route useDefaults is set to `false`', async (t) => {
t.plan(1)
const fastify = Fastify()
await fastify.register(helmet, {
global: false,
enableCSPNonces: false,
contentSecurityPolicy: {
directives: {}
}
})
fastify.get(
'/',
{
helmet: {
contentSecurityPolicy: {
useDefaults: false,
directives: {
'default-src': ["'self'"],
'script-src': ["'self'", "'unsafe-eval'", "'unsafe-inline'"],
'style-src': ["'self'", "'unsafe-inline'"]
}
}
}
},
(_request, reply) => {
reply.send({ hello: 'world' })
}
)
const response = await fastify.inject({ method: 'GET', path: '/' })
const expected = {
'content-security-policy':
"default-src 'self';script-src 'self' 'unsafe-eval' 'unsafe-inline';style-src 'self' 'unsafe-inline'"
}
const actualResponseHeaders = {
'content-security-policy': response.headers['content-security-policy']
}
t.assert.deepStrictEqual(actualResponseHeaders, expected)
})
test('It should not set `content-security-policy` header, if route contentSecurityPolicy is false', async (t) => {
t.plan(1)
const fastify = Fastify()
await fastify.register(helmet, {
global: false,
enableCSPNonces: false,
contentSecurityPolicy: {
directives: {}
}
})
fastify.get(
'/',
{
helmet: {
contentSecurityPolicy: false
}
},
(_request, reply) => {
reply.send({ hello: 'world' })
}
)
const response = await fastify.inject({ method: 'GET', path: '/' })
const expected = {
'content-security-policy': undefined
}
const actualResponseHeaders = {
'content-security-policy': response.headers['content-security-policy']
}
t.assert.deepStrictEqual(actualResponseHeaders, expected)
})
test('It should be able to conditionally apply the middlewares through the `helmet` reply decorator', async (t) => {
t.plan(10)
const fastify = Fastify()
await fastify.register(helmet, { global: false })
fastify.get('/:condition', async (request, reply) => {
const { condition } = request.params
t.assert.ok(reply.helmet)
t.assert.notEqual(reply.helmet, null)
if (condition !== 'frameguard') {
await reply.helmet({ frameguard: false })
} else {
await reply.helmet({ frameguard: true })
}
return { message: 'ok' }
})
const expected = {
'x-dns-prefetch-control': 'off',
'x-download-options': 'noopen',
'x-content-type-options': 'nosniff',
'x-xss-protection': '0'
}
const maybeExpected = {
'x-frame-options': 'SAMEORIGIN'
}
{
const response = await fastify.inject({
method: 'GET',
path: '/no-frameguard'
})
const actualResponseHeaders = {
'x-dns-prefetch-control': response.headers['x-dns-prefetch-control'],
'x-download-options': response.headers['x-download-options'],
'x-content-type-options': response.headers['x-content-type-options'],
'x-xss-protection': response.headers['x-xss-protection']
}
t.assert.equal(response.statusCode, 200)
t.assert.notDeepStrictEqual(
response.headers['x-frame-options'],
maybeExpected['x-frame-options']
)
t.assert.deepStrictEqual(actualResponseHeaders, expected)
}
const response = await fastify.inject({
method: 'GET',
path: '/frameguard'
})
const actualResponseHeaders = {
'x-dns-prefetch-control': response.headers['x-dns-prefetch-control'],
'x-download-options': response.headers['x-download-options'],
'x-content-type-options': response.headers['x-content-type-options'],
'x-xss-protection': response.headers['x-xss-protection']
}
t.assert.equal(response.statusCode, 200)
t.assert.deepStrictEqual(
response.headers['x-frame-options'],
maybeExpected['x-frame-options']
)
t.assert.deepStrictEqual(actualResponseHeaders, expected)
})
test('It should throw an error when route specific helmet options are of an invalid type', async (t) => {
t.plan(2)
const fastify = Fastify()
await fastify.register(helmet)
try {
fastify.get('/', { helmet: 'invalid_options' }, () => {
return { message: 'ok' }
})
} catch (error) {
t.assert.ok(error)
t.assert.equal(
error.message,
'Unknown value for route helmet configuration'
)
}
})
test('It should forward `helmet` reply decorator and route specific errors to `fastify-helmet`', async (t) => {
t.plan(6)
const fastify = Fastify()
await fastify.register(helmet, { global: false })
fastify.get('/helmet-reply-decorator-error', async (_request, reply) => {
await reply.helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'", () => 'bad;value']
}
}
})
return { message: 'ok' }
})
fastify.get(
'/helmet-route-configuration-error',
{
helmet: {
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'", () => 'bad;value']
}
}
}
},
async () => {
return { message: 'ok' }
}
)
const notExpected = {
'x-dns-prefetch-control': 'off',
'x-frame-options': 'SAMEORIGIN',
'x-download-options': 'noopen',
'x-content-type-options': 'nosniff',
'x-xss-protection': '0'
}
{
const response = await fastify.inject({
method: 'GET',
path: '/helmet-reply-decorator-error'
})
const actualResponseHeaders = {
'x-dns-prefetch-control': response.headers['x-dns-prefetch-control'],
'x-download-options': response.headers['x-download-options'],
'x-content-type-options': response.headers['x-content-type-options'],
'x-xss-protection': response.headers['x-xss-protection']
}
t.assert.equal(response.statusCode, 500)
t.assert.equal(
JSON.parse(response.payload).message,
'Content-Security-Policy received an invalid directive value for "default-src"'
)
t.assert.notDeepStrictEqual(actualResponseHeaders, notExpected)
}
const response = await fastify.inject({
method: 'GET',
path: '/helmet-route-configuration-error'
})
const actualResponseHeaders = {
'x-dns-prefetch-control': response.headers['x-dns-prefetch-control'],
'x-download-options': response.headers['x-download-options'],
'x-content-type-options': response.headers['x-content-type-options'],
'x-xss-protection': response.headers['x-xss-protection']
}
t.assert.equal(response.statusCode, 500)
t.assert.equal(
JSON.parse(response.payload).message,
'Content-Security-Policy received an invalid directive value for "default-src"'
)
t.assert.notDeepStrictEqual(actualResponseHeaders, notExpected)
})

41
backend/node_modules/@fastify/helmet/types/index.d.ts generated vendored Normal file
View File

@@ -0,0 +1,41 @@
import { FastifyPluginAsync, RawServerBase, RawServerDefault } from 'fastify'
import helmet, { contentSecurityPolicy, HelmetOptions } from 'helmet'
declare module 'fastify' {
export interface RouteShorthandOptions<
// eslint-disable-next-line @typescript-eslint/no-unused-vars
RawServer extends RawServerBase = RawServerDefault
> extends fastifyHelmet.FastifyHelmetRouteOptions { }
interface FastifyReply {
cspNonce: {
script: string;
style: string;
},
helmet: (opts?: HelmetOptions) => typeof helmet
}
export interface RouteOptions extends fastifyHelmet.FastifyHelmetRouteOptions { }
}
type FastifyHelmet = FastifyPluginAsync<fastifyHelmet.FastifyHelmetOptions> & {
contentSecurityPolicy: typeof contentSecurityPolicy;
}
declare namespace fastifyHelmet {
export interface FastifyHelmetRouteOptions {
helmet?: Omit<FastifyHelmetOptions, 'global'> | false;
}
export type FastifyHelmetOptions = {
enableCSPNonces?: boolean,
global?: boolean;
} & NonNullable<HelmetOptions>
export const fastifyHelmet: FastifyHelmet
export { fastifyHelmet as default }
}
declare function fastifyHelmet (...params: Parameters<FastifyHelmet>): ReturnType<FastifyHelmet>
export = fastifyHelmet

View File

@@ -0,0 +1,184 @@
import fastify, { FastifyPluginAsync } from 'fastify'
import helmet from 'helmet'
import { expectAssignable, expectError, expectType } from 'tsd'
import fastifyHelmet, { FastifyHelmetOptions, FastifyHelmetRouteOptions } from '..'
// Plugin registered with no options
const appOne = fastify()
appOne.register(fastifyHelmet)
// Plugin registered with an empty object option
const appTwo = fastify()
expectAssignable<FastifyHelmetOptions>({})
appTwo.register(fastifyHelmet, {})
// Plugin registered with all helmet middlewares disabled
const appThree = fastify()
const helmetOptions = {
contentSecurityPolicy: false,
dnsPrefetchControl: false,
frameguard: false,
hidePoweredBy: false,
hsts: false,
ieNoOpen: false,
noSniff: false,
permittedCrossDomainPolicies: false,
referrerPolicy: false,
xssFilter: false
}
expectAssignable<FastifyHelmetOptions>(helmetOptions)
appThree.register(fastifyHelmet, helmetOptions)
// Plugin registered with helmet middlewares custom settings
const appFour = fastify()
appFour.register(fastifyHelmet, {
contentSecurityPolicy: {
directives: {
'directive-1': ['foo', 'bar']
},
reportOnly: true,
useDefaults: false
},
dnsPrefetchControl: {
allow: true
},
frameguard: {
action: 'deny'
},
hsts: {
maxAge: 1,
includeSubDomains: true,
preload: true
},
permittedCrossDomainPolicies: {
permittedPolicies: 'master-only'
},
referrerPolicy: {
policy: 'no-referrer'
}
// these options are false or never
// hidePoweredBy: false
// ieNoOpen: false,
// noSniff: false,
// xssFilter: false
})
// Plugin registered with `enableCSPNonces` option and helmet default CSP settings
const appFive = fastify()
appFive.register(fastifyHelmet, { enableCSPNonces: true })
appFive.get('/', function (_request, reply) {
expectType<{
script: string;
style: string;
}>(reply.cspNonce)
})
// Plugin registered with `enableCSPNonces` option and custom CSP settings
const appSix = fastify()
appSix.register(fastifyHelmet, {
enableCSPNonces: true,
contentSecurityPolicy: {
directives: {
'directive-1': ['foo', 'bar']
},
reportOnly: true
}
})
appSix.get('/', function (_request, reply) {
expectType<{
script: string;
style: string;
}>(reply.cspNonce)
})
const csp = fastifyHelmet.contentSecurityPolicy
expectType<typeof helmet.contentSecurityPolicy>(csp)
// Plugin registered with `global` set to `true`
const appSeven = fastify()
appSeven.register(fastifyHelmet, { global: true })
appSeven.get('/route-with-disabled-helmet', { helmet: false }, function (_request, reply) {
expectType<typeof helmet>(reply.helmet())
})
expectError(
appSeven.get('/route-with-disabled-helmet', {
helmet: 'trigger a typescript error'
}, function (_request, reply) {
expectType<typeof helmet>(reply.helmet())
})
)
// Plugin registered with `global` set to `false`
const appEight = fastify()
appEight.register(fastifyHelmet, { global: false })
appEight.get('/disabled-helmet', function (_request, reply) {
expectType<typeof helmet>(reply.helmet(helmetOptions))
})
const routeHelmetOptions = {
helmet: {
enableCSPNonces: true,
contentSecurityPolicy: {
directives: {
'directive-1': ['foo', 'bar']
},
reportOnly: true
},
dnsPrefetchControl: {
allow: true
},
frameguard: {
action: 'deny' as const
},
hsts: {
maxAge: 1,
includeSubDomains: true,
preload: true
},
permittedCrossDomainPolicies: {
permittedPolicies: 'all' as const
},
referrerPolicy: {
policy: 'no-referrer' as const
}
}
}
expectAssignable<FastifyHelmetRouteOptions>(routeHelmetOptions)
appEight.get('/enabled-helmet', routeHelmetOptions, function (_request, reply) {
expectType<typeof helmet>(reply.helmet())
expectType<{
script: string;
style: string;
}>(reply.cspNonce)
})
appEight.get('/enable-framegard', {
helmet: { frameguard: true }
}, function (_request, reply) {
expectType<typeof helmet>(reply.helmet())
expectType<{
script: string;
style: string;
}>(reply.cspNonce)
})
// Plugin registered with an invalid helmet option
const appThatTriggerAnError = fastify()
expectError(
appThatTriggerAnError.register(fastifyHelmet, {
thisOptionDoesNotExist: 'trigger a typescript error'
})
)
// fastify-helmet instance is using the FastifyHelmetOptions options
expectType<
FastifyPluginAsync<FastifyHelmetOptions> & {
contentSecurityPolicy: typeof helmet.contentSecurityPolicy;
}
>(fastifyHelmet)