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

View File

@@ -0,0 +1,22 @@
The MIT License
Copyright (c) 2022 The 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,52 @@
# @fastify/accept-negotiator
[![CI](https://github.com/fastify/accept-negotiator/workflows/CI/badge.svg)](https://github.com/fastify/accept-negotiator/actions/workflows/ci.yml)
[![NPM version](https://img.shields.io/npm/v/@fastify/accept-negotiator.svg?style=flat)](https://www.npmjs.com/package/@fastify/accept-negotiator)
[![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](https://standardjs.com/)
A negotiator for the accept-headers
### Install
```
npm i @fastify/accept-negotiator
```
### Usage
The module exports a function that you can use for negotiating an accept-header, e.g. accept-encoding. It takes 2 parameters:
```
negotiate(header, supportedValues)
```
- `header` (`string`, required) - The accept-header, e.g. accept-encoding
- `supportedValues` (`string[]`, required) - The values, which are supported
```js
const negotiate = require('@fastify/accept-negotiator').negotiate
const encoding = negotiate('gzip, deflate, br', ['br'])
console.log(encoding) // 'br*
```
The module also exports a class that you can use for negotiating an accept-header, e.g. accept-encoding, and use caching for better performance.
```
Negotiate(supportedValues)
```
- `supportedValues` (`string[]`, required) - The values, which are supported
- `cache` (`{ set: Function; get: Function; has: Function }`, optional) - A Cache-Store, e.g. ES6-Map or mnemonist LRUCache
```js
const Negotiator = require('@fastify/accept-negotiator').Negotiator
const encodingNegotiator = new Negotiator({ supportedValues: ['br'], cache: new Map() })
const encoding = encodingNegotiator.negotiate('gzip, deflate, br')
console.log(encoding) // 'br*
```
## License
Licensed under [MIT](./LICENSE).

View File

@@ -0,0 +1,170 @@
'use strict'
function Negotiator (options) {
if (!new.target) {
return new Negotiator(options)
}
const {
supportedValues = [],
cache
} = (options && typeof options === 'object' && options) || {}
this.supportedValues = supportedValues
this.cache = cache
}
Negotiator.prototype.negotiate = function (header) {
if (typeof header !== 'string') {
return null
}
if (!this.cache) {
return negotiate(header, this.supportedValues)
}
if (!this.cache.has(header)) {
this.cache.set(header, negotiate(header, this.supportedValues))
}
return this.cache.get(header)
}
function negotiate (header, supportedValues) {
if (
!header ||
!Array.isArray(supportedValues) ||
supportedValues.length === 0
) {
return null
}
if (header === '*') {
return supportedValues[0]
}
let preferredEncoding = null
let preferredEncodingPriority = Infinity
let preferredEncodingQuality = 0
function processMatch (enc, quality) {
if (quality === 0 || preferredEncodingQuality > quality) {
return false
}
const encoding = (enc === '*' && supportedValues[0]) || enc
const priority = supportedValues.indexOf(encoding)
if (priority === -1) {
return false
}
if (priority === 0 && quality === 1) {
preferredEncoding = encoding
return true
} else if (preferredEncodingQuality < quality) {
preferredEncoding = encoding
preferredEncodingPriority = priority
preferredEncodingQuality = quality
} else if (preferredEncodingPriority > priority) {
preferredEncoding = encoding
preferredEncodingPriority = priority
preferredEncodingQuality = quality
}
return false
}
parse(header, processMatch)
return preferredEncoding
}
const BEGIN = 0
const TOKEN = 1
const QUALITY = 2
const END = 3
function parse (header, processMatch) {
let str = ''
let quality
let state = BEGIN
for (let i = 0, il = header.length; i < il; ++i) {
const char = header[i]
if (char === ' ' || char === '\t') {
continue
} else if (char === ';') {
if (state === TOKEN) {
state = QUALITY
quality = ''
}
continue
} else if (char === ',') {
if (state === TOKEN) {
if (processMatch(str, 1)) {
state = END
break
}
state = BEGIN
str = ''
} else if (state === QUALITY) {
if (processMatch(str, parseFloat(quality) || 0)) {
state = END
break
}
state = BEGIN
str = ''
quality = ''
}
continue
} else if (
state === QUALITY
) {
if (char === 'q' || char === '=') {
continue
} else if (
char === '.' ||
char === '1' ||
char === '0' ||
char === '2' ||
char === '3' ||
char === '4' ||
char === '5' ||
char === '6' ||
char === '7' ||
char === '8' ||
char === '9'
) {
quality += char
continue
}
} else if (state === BEGIN) {
state = TOKEN
str += char
continue
}
if (state === TOKEN) {
const prevChar = header[i - 1]
if (prevChar === ' ' || prevChar === '\t') {
str = ''
}
str += char
continue
}
if (processMatch(str, parseFloat(quality) || 0)) {
state = END
break
}
state = BEGIN
str = char
quality = ''
}
if (state === TOKEN) {
processMatch(str, 1)
} else if (state === QUALITY) {
processMatch(str, parseFloat(quality) || 0)
}
}
module.exports = negotiate
module.exports.default = negotiate
module.exports.negotiate = negotiate
module.exports.Negotiator = Negotiator

View File

@@ -0,0 +1,51 @@
{
"name": "@fastify/accept-negotiator",
"version": "1.1.0",
"description": "a negotiator for the accept-headers",
"type": "commonjs",
"main": "index.js",
"types": "types/index.d.ts",
"scripts": {
"lint": "standard index.js test/* benchmarks/*",
"test": "npm run test:unit && npm run test:typescript",
"test:unit": "tap",
"test:typescript": "tsd"
},
"standard": {
"ignore": [
"index.d.ts"
]
},
"keywords": [
"encoding",
"negotiator",
"accept-encoding",
"accept",
"http",
"header"
],
"files": [
"README.md",
"LICENSE",
"index.js",
"types/index.d.ts"
],
"author": "Aras Abbasi",
"license": "MIT",
"devDependencies": {
"benchmark": "2.1.4",
"standard": "17.0.0",
"tap": "^16.3.0",
"tsd": "^0.24.1"
},
"engines": {
"node": ">=14"
},
"repository": {
"type": "git",
"url": "git+https://github.com/fastify/accept-negotiator.git"
},
"bugs": {
"url": "https://github.com/fastify/accept-negotiator/issues"
}
}

View File

@@ -0,0 +1,17 @@
type CacheStore = { set: (key: string, value: string) => CacheStore, get: (key: string) => string | undefined, has: (key: string) => boolean }
type NegotiateFn = typeof negotiate
declare namespace negotiate {
export class Negotiator<K extends string = string> {
constructor (options: { supportedValues: K[]; cache?: CacheStore })
negotiate(header: string): K | null
}
export const negotiate: NegotiateFn
export { negotiate as default }
}
declare function negotiate<K extends string = string>(header: string, supportedValues: K[]): K | null;
export = negotiate;

View File

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

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,13 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"
open-pull-requests-limit: 10
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10

View File

@@ -0,0 +1,8 @@
comment: |
Hello! Thank you for contributing!
It appears that you have changed the code, but the tests that verify your change are missing. Could you please add them?
fileExtensions:
- '.ts'
- '.js'
testDir: 'test'

View File

@@ -0,0 +1,26 @@
name: Continuous Integration
on:
push:
branches:
- main
- master
- next
- 'v*'
paths-ignore:
- 'docs/**'
- '*.md'
pull_request:
paths-ignore:
- 'docs/**'
- '*.md'
env:
TZ: 'UTC'
jobs:
test:
uses: fastify/workflows/.github/workflows/plugins-ci.yml@v3
with:
lint: true
license-check: true

2
backend/node_modules/@fastify/ajv-compiler/.taprc generated vendored Normal file
View File

@@ -0,0 +1,2 @@
files:
- test/**/*.test.js

24
backend/node_modules/@fastify/ajv-compiler/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,24 @@
MIT License
Copyright (c) The Fastify Team
The Fastify team members are listed at https://github.com/fastify/fastify#team
and in the README file.
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.

236
backend/node_modules/@fastify/ajv-compiler/README.md generated vendored Normal file
View File

@@ -0,0 +1,236 @@
# @fastify/ajv-compiler
[![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](http://standardjs.com/)
[![Continuous Integration](https://github.com/fastify/ajv-compiler/workflows/Continuous%20Integration/badge.svg)](https://github.com/fastify/ajv-compiler/actions/workflows/ci.yml)
This module manages the [`ajv`](https://www.npmjs.com/package/ajv) instances for the Fastify framework.
It isolates the `ajv` dependency so that the AJV version is not tightly coupled to the Fastify version.
This allows the user to decide which version of AJV to use in their Fastify based application.
## Versions
| `@fastify/ajv-compiler` | `ajv` | Default in `fastify` |
|------------------------:|------:|---------------------:|
| v1.x | v6.x | ^3.14 |
| v2.x | v8.x | - |
| v3.x | v8.x | ^4.x |
### AJV Configuration
The Fastify's default [`ajv` options](https://github.com/ajv-validator/ajv/tree/v6#options) are:
```js
{
coerceTypes: 'array',
useDefaults: true,
removeAdditional: true,
uriResolver: require('fast-uri'),
addUsedSchema: false,
// Explicitly set allErrors to `false`.
// When set to `true`, a DoS attack is possible.
allErrors: false
}
```
Moreover, the [`ajv-formats`](https://www.npmjs.com/package/ajv-formats) module is included by default.
If you need to customize it, check the _usage_ section below.
To customize the `ajv`'s options, see how in the [Fastify official docs](https://fastify.dev/docs/latest/Reference/Server/#ajv).
## Usage
This module is already used as default by Fastify.
If you need to provide to your server instance a different version, refer to [the official doc](https://fastify.dev/docs/latest/Reference/Server/#schemacontroller).
### Customize the `ajv-formats` plugin
The `format` keyword is not part of the official `ajv` module since v7. To use it, you need to install the `ajv-formats` module and this module
does it for you with the default configuration.
If you need to configure the `ajv-formats` plugin you can do it using the standard Fastify configuration:
```js
const app = fastify({
ajv: {
plugins: [[require('ajv-formats'), { mode: 'fast' }]]
}
})
```
In this way, your setup will have precendence over the `@fastify/ajv-compiler` default configuration.
### Customize the `ajv` instance
If you need to customize the `ajv` instance and take full control of its configuration, you can do it by
using the `onCreate` option in the Fastify configuration that accepts a syncronous function that receives the `ajv` instance:
```js
const app = fastify({
ajv: {
onCreate: (ajv) => {
// Modify the ajv instance as you need.
ajv.addFormat('myFormat', (data) => typeof data === 'string')
}
}
})
```
### Fastify with JTD
The [JSON Type Definition](https://jsontypedef.com/) feature is supported by AJV v8.x and you can benefit from it in your Fastify application.
With Fastify v3.20.x and higher, you can use the `@fastify/ajv-compiler` module to load JSON Type Definitions like so:
```js
const factory = require('@fastify/ajv-compiler')()
const app = fastify({
jsonShorthand: false,
ajv: {
customOptions: { }, // additional JTD options
mode: 'JTD'
},
schemaController: {
compilersFactory: {
buildValidator: factory
}
}
})
```
The defaults AJV JTD options are the same as the [Fastify's default options](#AJV-Configuration).
#### Fastify with JTD and serialization
You can use JTD Schemas to serialize your response object too:
```js
const factoryValidator = require('@fastify/ajv-compiler')()
const factorySerializer = require('@fastify/ajv-compiler')({ jtdSerializer: true })
const app = fastify({
jsonShorthand: false,
ajv: {
customOptions: { }, // additional JTD options
mode: 'JTD'
},
schemaController: {
compilersFactory: {
buildValidator: factoryValidator,
buildSerializer: factorySerializer
}
}
})
```
### AJV Standalone
AJV v8 introduces the [standalone feature](https://ajv.js.org/standalone.html) that let you to pre-compile your schemas and use them in your application for a faster startup.
To use this feature, you must be aware of the following:
1. You must generate and save the application's compiled schemas.
2. Read the compiled schemas from the file and provide them back to your Fastify application.
#### Generate and save the compiled schemas
Fastify helps you to generate the validation schemas functions and it is your choice to save them where you want.
To accomplish this, you must use a new compiler: `StandaloneValidator`.
You must provide 2 parameters to this compiler:
- `readMode: false`: a boolean to indicate that you want generate the schemas functions string.
- `storeFunction`" a sync function that must store the source code of the schemas functions. You may provide an async function too, but you must manage errors.
When `readMode: false`, **the compiler is meant to be used in development ONLY**.
```js
const { StandaloneValidator } = require('@fastify/ajv-compiler')
const factory = StandaloneValidator({
readMode: false,
storeFunction (routeOpts, schemaValidationCode) {
// routeOpts is like: { schema, method, url, httpPart }
// schemaValidationCode is a string source code that is the compiled schema function
const fileName = generateFileName(routeOpts)
fs.writeFileSync(path.join(__dirname, fileName), schemaValidationCode)
}
})
const app = fastify({
jsonShorthand: false,
schemaController: {
compilersFactory: {
buildValidator: factory
}
}
})
// ... add all your routes with schemas ...
app.ready().then(() => {
// at this stage all your schemas are compiled and stored in the file system
// now it is important to turn off the readMode
})
```
#### Read the compiled schemas functions
At this stage, you should have a file for every route's schema.
To use them, you must use the `StandaloneValidator` with the parameters:
- `readMode: true`: a boolean to indicate that you want read and use the schemas functions string.
- `restoreFunction`" a sync function that must return a function to validate the route.
Important keep away before you continue reading the documentation:
- when you use the `readMode: true`, the application schemas are not compiled (they are ignored). So, if you change your schemas, you must recompile them!
- as you can see, you must relate the route's schema to the file name using the `routeOpts` object. You may use the `routeOpts.schema.$id` field to do so, it is up to you to define a unique schema identifier.
```js
const { StandaloneValidator } = require('@fastify/ajv-compiler')
const factory = StandaloneValidator({
readMode: true,
restoreFunction (routeOpts) {
// routeOpts is like: { schema, method, url, httpPart }
const fileName = generateFileName(routeOpts)
return require(path.join(__dirname, fileName))
}
})
const app = fastify({
jsonShorthand: false,
schemaController: {
compilersFactory: {
buildValidator: factory
}
}
})
// ... add all your routes with schemas as before...
app.listen({ port: 3000 })
```
### How it works
This module provide a factory function to produce [Validator Compilers](https://fastify.dev/docs/latest/Reference/Server/#validatorcompiler) functions.
The Fastify factory function is just one per server instance and it is called for every encapsulated context created by the application through the `fastify.register()` call.
Every Validator Compiler produced, has a dedicated AJV instance, so, this factory will try to produce as less as possible AJV instances to reduce the memory footprint and the startup time.
The variables involved to choose if a Validator Compiler can be reused are:
- the AJV configuration: it is [one per server](https://fastify.dev/docs/latest/Reference/Server/#ajv)
- the external JSON schemas: once a new schema is added to a fastify's context, calling `fastify.addSchema()`, it will cause a new AJV inizialization
## License
Licensed under [MIT](./LICENSE).

View File

@@ -0,0 +1,37 @@
import cronometro from 'cronometro'
import fjs from 'fast-json-stringify'
import AjvCompiler from '../index.js'
const fjsSerialize = buildFJSSerializerFunction({
type: 'object',
properties: {
hello: { type: 'string' },
name: { type: 'string' }
}
})
const ajvSerialize = buildAJVSerializerFunction({
properties: {
hello: { type: 'string' },
name: { type: 'string' }
}
})
await cronometro({
'fast-json-stringify': function () {
fjsSerialize({ hello: 'Ciao', name: 'Manuel' })
},
'ajv serializer': function () {
ajvSerialize({ hello: 'Ciao', name: 'Manuel' })
}
})
function buildFJSSerializerFunction (schema) {
return fjs(schema)
}
function buildAJVSerializerFunction (schema) {
const factory = AjvCompiler({ jtdSerializer: true })
const compiler = factory({}, { customOptions: {} })
return compiler({ schema })
}

53
backend/node_modules/@fastify/ajv-compiler/index.js generated vendored Normal file
View File

@@ -0,0 +1,53 @@
'use strict'
const AjvReference = Symbol.for('fastify.ajv-compiler.reference')
const ValidatorCompiler = require('./lib/validator-compiler')
const SerializerCompiler = require('./lib/serializer-compiler')
function AjvCompiler (opts) {
const validatorPool = new Map()
const serializerPool = new Map()
if (opts && opts.jtdSerializer === true) {
return function buildSerializerFromPool (externalSchemas, serializerOpts) {
const uniqueAjvKey = getPoolKey({}, serializerOpts)
if (serializerPool.has(uniqueAjvKey)) {
return serializerPool.get(uniqueAjvKey)
}
const compiler = new SerializerCompiler(externalSchemas, serializerOpts)
const ret = compiler.buildSerializerFunction.bind(compiler)
serializerPool.set(uniqueAjvKey, ret)
return ret
}
}
return function buildCompilerFromPool (externalSchemas, options) {
const uniqueAjvKey = getPoolKey(externalSchemas, options.customOptions)
if (validatorPool.has(uniqueAjvKey)) {
return validatorPool.get(uniqueAjvKey)
}
const compiler = new ValidatorCompiler(externalSchemas, options)
const ret = compiler.buildValidatorFunction.bind(compiler)
validatorPool.set(uniqueAjvKey, ret)
if (options.customOptions.code !== undefined) {
ret[AjvReference] = compiler
}
return ret
}
}
function getPoolKey (externalSchemas, options) {
const externals = JSON.stringify(externalSchemas)
const ajvConfig = JSON.stringify(options)
return `${externals}${ajvConfig}`
}
module.exports = AjvCompiler
module.exports.default = AjvCompiler
module.exports.AjvCompiler = AjvCompiler
module.exports.AjvReference = AjvReference
module.exports.StandaloneValidator = require('./standalone')

View File

@@ -0,0 +1,14 @@
'use strict'
const fastUri = require('fast-uri')
module.exports = Object.freeze({
coerceTypes: 'array',
useDefaults: true,
removeAdditional: true,
uriResolver: fastUri,
addUsedSchema: false,
// Explicitly set allErrors to `false`.
// When set to `true`, a DoS attack is possible.
allErrors: false
})

View File

@@ -0,0 +1,27 @@
'use strict'
const AjvJTD = require('ajv/dist/jtd')
const defaultAjvOptions = require('./default-ajv-options')
class SerializerCompiler {
constructor (externalSchemas, options) {
this.ajv = new AjvJTD(Object.assign({}, defaultAjvOptions, options))
/**
* https://ajv.js.org/json-type-definition.html#ref-form
* Unlike JSON Schema, JTD does not allow to reference:
* - any schema fragment other than root level definitions member
* - root of the schema - there is another way to define a self-recursive schema (see Example 2)
* - another schema file (but you can still combine schemas from multiple files using JavaScript).
*
* So we ignore the externalSchemas parameter.
*/
}
buildSerializerFunction ({ schema/*, method, url, httpStatus */ }) {
return this.ajv.compileSerializer(schema)
}
}
module.exports = SerializerCompiler

View File

@@ -0,0 +1,58 @@
'use strict'
const Ajv = require('ajv').default
const AjvJTD = require('ajv/dist/jtd')
const defaultAjvOptions = require('./default-ajv-options')
class ValidatorCompiler {
constructor (externalSchemas, options) {
// This instance of Ajv is private
// it should not be customized or used
if (options.mode === 'JTD') {
this.ajv = new AjvJTD(Object.assign({}, defaultAjvOptions, options.customOptions))
} else {
this.ajv = new Ajv(Object.assign({}, defaultAjvOptions, options.customOptions))
}
let addFormatPlugin = true
if (options.plugins && options.plugins.length > 0) {
for (const plugin of options.plugins) {
if (Array.isArray(plugin)) {
addFormatPlugin = addFormatPlugin && plugin[0].name !== 'formatsPlugin'
plugin[0](this.ajv, plugin[1])
} else {
addFormatPlugin = addFormatPlugin && plugin.name !== 'formatsPlugin'
plugin(this.ajv)
}
}
}
if (addFormatPlugin) {
require('ajv-formats')(this.ajv)
}
options.onCreate?.(this.ajv)
const sourceSchemas = Object.values(externalSchemas)
for (const extSchema of sourceSchemas) {
this.ajv.addSchema(extSchema)
}
}
buildValidatorFunction ({ schema/*, method, url, httpPart */ }) {
// Ajv does not support compiling two schemas with the same
// id inside the same instance. Therefore if we have already
// compiled the schema with the given id, we just return it.
if (schema.$id) {
const stored = this.ajv.getSchema(schema.$id)
if (stored) {
return stored
}
}
return this.ajv.compile(schema)
}
}
module.exports = ValidatorCompiler

View File

@@ -0,0 +1,54 @@
{
"name": "@fastify/ajv-compiler",
"version": "3.6.0",
"description": "Build and manage the AJV instances for the fastify framework",
"main": "index.js",
"type": "commonjs",
"types": "types/index.d.ts",
"directories": {
"test": "test"
},
"scripts": {
"lint": "standard",
"lint:fix": "standard --fix",
"unit": "tap",
"test": "npm run unit && npm run test:typescript",
"test:typescript": "tsd",
"ajv:compile": "ajv compile -s test/source.json -o test/validate_schema.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/fastify/ajv-compiler.git"
},
"keywords": [
"ajv",
"validator",
"schema",
"compiler",
"fastify"
],
"author": "Manuel Spigolon <behemoth89@gmail.com> (https://github.com/Eomm)",
"license": "MIT",
"bugs": {
"url": "https://github.com/fastify/ajv-compiler/issues"
},
"homepage": "https://github.com/fastify/ajv-compiler#readme",
"devDependencies": {
"ajv-cli": "^5.0.0",
"ajv-errors": "^3.0.0",
"ajv-i18n": "^4.0.1",
"ajv-merge-patch": "^5.0.1",
"cronometro": "^3.0.1",
"fastify": "^4.0.0",
"require-from-string": "^2.0.2",
"sanitize-filename": "^1.6.3",
"standard": "^17.0.0",
"tap": "^16.2.0",
"tsd": "^0.31.0"
},
"dependencies": {
"ajv": "^8.11.0",
"ajv-formats": "^2.1.1",
"fast-uri": "^2.0.0"
}
}

View File

@@ -0,0 +1,44 @@
'use strict'
const ValidatorSelector = require('./index')
const standaloneCode = require('ajv/dist/standalone').default
function StandaloneValidator (options = { readMode: true }) {
if (options.readMode === true && !options.restoreFunction) {
throw new Error('You must provide a restoreFunction options when readMode ON')
}
if (options.readMode !== true && !options.storeFunction) {
throw new Error('You must provide a storeFunction options when readMode OFF')
}
if (options.readMode === true) {
// READ MODE: it behalf only in the restore function provided by the user
return function wrapper () {
return function (opts) {
return options.restoreFunction(opts)
}
}
}
// WRITE MODE: it behalf on the default ValidatorSelector, wrapping the API to run the Ajv Standalone code generation
const factory = ValidatorSelector()
return function wrapper (externalSchemas, ajvOptions = {}) {
if (!ajvOptions.customOptions || !ajvOptions.customOptions.code) {
// to generate the validation source code, these options are mandatory
ajvOptions.customOptions = Object.assign({}, ajvOptions.customOptions, { code: { source: true } })
}
const compiler = factory(externalSchemas, ajvOptions)
return function (opts) { // { schema/*, method, url, httpPart */ }
const validationFunc = compiler(opts)
const schemaValidationCode = standaloneCode(compiler[ValidatorSelector.AjvReference].ajv, validationFunc)
options.storeFunction(opts, schemaValidationCode)
return validationFunc
}
}
}
module.exports = StandaloneValidator

View File

View File

@@ -0,0 +1,59 @@
'use strict'
const t = require('tap')
const AjvCompiler = require('../index')
const postSchema = Object.freeze({
$schema: 'http://json-schema.org/draft-07/schema#',
type: 'object',
$id: 'http://mydomain.com/user',
title: 'User schema',
description: 'Contains all user fields',
properties: {
username: { type: 'string', minLength: 4 },
firstName: { type: 'string', minLength: 1 },
lastName: { type: 'string', minLength: 1 },
email: { type: 'string' },
password: { type: 'string', minLength: 6 },
bio: { type: 'string' }
},
required: ['username', 'firstName', 'lastName', 'email', 'bio', 'password']
})
const patchSchema = Object.freeze({
$schema: 'http://json-schema.org/draft-07/schema#',
type: 'object',
$id: 'http://mydomain.com/user',
title: 'User schema',
description: 'Contains all user fields',
properties: {
firstName: { type: 'string', minLength: 1 },
lastName: { type: 'string', minLength: 1 },
bio: { type: 'string' }
}
})
const fastifyAjvOptionsDefault = Object.freeze({
customOptions: {}
})
t.test('must not store schema on compile', t => {
t.plan(4)
const factory = AjvCompiler()
const compiler = factory({}, fastifyAjvOptionsDefault)
const postFn = compiler({ schema: postSchema })
const patchFn = compiler({ schema: patchSchema })
const resultForPost = postFn({})
t.equal(resultForPost, false)
t.has(postFn.errors, [
{
keyword: 'required',
message: "must have required property 'username'"
}
])
const resultForPatch = patchFn({})
t.ok(resultForPatch)
t.notOk(patchFn.errors)
})

View File

@@ -0,0 +1,307 @@
'use strict'
const t = require('tap')
const fastify = require('fastify')
const AjvCompiler = require('../index')
const sym = Symbol.for('fastify.ajv-compiler.reference')
const sampleSchema = Object.freeze({
$id: 'example1',
type: 'object',
properties: {
name: { type: 'string' }
}
})
const externalSchemas1 = Object.freeze({})
const externalSchemas2 = Object.freeze({
foo: {
$id: 'foo',
type: 'object',
properties: {
name: { type: 'string' }
}
}
})
const fastifyAjvOptionsDefault = Object.freeze({
customOptions: {}
})
const fastifyJtdDefault = Object.freeze({
customOptions: { },
mode: 'JTD'
})
const fastifyAjvOptionsCustom = Object.freeze({
customOptions: {
allErrors: true,
removeAdditional: false
},
plugins: [
require('ajv-formats'),
[require('ajv-errors'), { singleError: false }]
]
})
t.test('basic usage', t => {
t.plan(1)
const factory = AjvCompiler()
const compiler = factory(externalSchemas1, fastifyAjvOptionsDefault)
const validatorFunc = compiler({ schema: sampleSchema })
const result = validatorFunc({ name: 'hello' })
t.equal(result, true)
})
t.test('array coercion', t => {
t.plan(2)
const factory = AjvCompiler()
const compiler = factory(externalSchemas1, fastifyAjvOptionsDefault)
const arraySchema = {
$id: 'example1',
type: 'object',
properties: {
name: { type: 'array', items: { type: 'string' } }
}
}
const validatorFunc = compiler({ schema: arraySchema })
const inputObj = { name: 'hello' }
t.equal(validatorFunc(inputObj), true)
t.same(inputObj, { name: ['hello'] }, 'the name property should be coerced to an array')
})
t.test('nullable default', t => {
t.plan(2)
const factory = AjvCompiler()
const compiler = factory({}, fastifyAjvOptionsDefault)
const validatorFunc = compiler({
schema: {
type: 'object',
properties: {
nullable: { type: 'string', nullable: true },
notNullable: { type: 'string' }
}
}
})
const input = { nullable: null, notNullable: null }
const result = validatorFunc(input)
t.equal(result, true)
t.same(input, { nullable: null, notNullable: '' }, 'the notNullable field has been coerced')
})
t.test('plugin loading', t => {
t.plan(3)
const factory = AjvCompiler()
const compiler = factory(externalSchemas1, fastifyAjvOptionsCustom)
const validatorFunc = compiler({
schema: {
type: 'object',
properties: {
q: {
type: 'string',
format: 'date',
formatMinimum: '2016-02-06',
formatExclusiveMaximum: '2016-12-27'
}
},
required: ['q'],
errorMessage: 'hello world'
}
})
const result = validatorFunc({ q: '2016-10-02' })
t.equal(result, true)
const resultFail = validatorFunc({})
t.equal(resultFail, false)
t.equal(validatorFunc.errors[0].message, 'hello world')
})
t.test('optimization - cache ajv instance', t => {
t.plan(5)
const factory = AjvCompiler()
const compiler1 = factory(externalSchemas1, fastifyAjvOptionsDefault)
const compiler2 = factory(externalSchemas1, fastifyAjvOptionsDefault)
t.equal(compiler1, compiler2, 'same instance')
t.same(compiler1, compiler2, 'same instance')
const compiler3 = factory(externalSchemas2, fastifyAjvOptionsDefault)
t.not(compiler3, compiler1, 'new ajv instance when externa schema change')
const compiler4 = factory(externalSchemas1, fastifyAjvOptionsCustom)
t.not(compiler4, compiler1, 'new ajv instance when externa schema change')
t.not(compiler4, compiler3, 'new ajv instance when externa schema change')
})
t.test('the onCreate callback can enhance the ajv instance', t => {
t.plan(2)
const factory = AjvCompiler()
const fastifyAjvCustomOptionsFormats = Object.freeze({
onCreate (ajv) {
for (const [formatName, format] of Object.entries(this.customOptions.formats)) {
ajv.addFormat(formatName, format)
}
},
customOptions: {
formats: {
date: /foo/
}
}
})
const compiler1 = factory(externalSchemas1, fastifyAjvCustomOptionsFormats)
const validatorFunc = compiler1({
schema: {
type: 'string',
format: 'date'
}
})
const result = validatorFunc('foo')
t.equal(result, true)
const resultFail = validatorFunc('2016-10-02')
t.equal(resultFail, false)
})
// https://github.com/fastify/fastify/pull/2969
t.test('compile same $id when in external schema', t => {
t.plan(3)
const factory = AjvCompiler()
const base = {
$id: 'urn:schema:base',
definitions: {
hello: { type: 'string' }
},
type: 'object',
properties: {
hello: { $ref: '#/definitions/hello' }
}
}
const refSchema = {
$id: 'urn:schema:ref',
type: 'object',
properties: {
hello: { $ref: 'urn:schema:base#/definitions/hello' }
}
}
const compiler = factory({
[base.$id]: base,
[refSchema.$id]: refSchema
}, fastifyAjvOptionsDefault)
t.notOk(compiler[sym], 'the ajv reference do not exists if code is not activated')
const validatorFunc1 = compiler({
schema: {
$id: 'urn:schema:ref'
}
})
const validatorFunc2 = compiler({
schema: {
$id: 'urn:schema:ref'
}
})
t.pass('the compile does not fail if the schema compiled is already in the external schemas')
t.equal(validatorFunc1, validatorFunc2, 'the returned function is the same')
})
t.test('JTD MODE', t => {
t.plan(2)
t.test('compile jtd schema', t => {
t.plan(4)
const factory = AjvCompiler()
const jtdSchema = {
discriminator: 'version',
mapping: {
1: {
properties: {
foo: { type: 'uint8' }
}
},
2: {
properties: {
foo: { type: 'string' }
}
}
}
}
const compiler = factory({}, fastifyJtdDefault)
const validatorFunc = compiler({ schema: jtdSchema })
t.pass('generated validation function for JTD SCHEMA')
const result = validatorFunc({
version: '2',
foo: []
})
t.notOk(result, 'failed validation')
t.type(validatorFunc.errors, 'Array')
const success = validatorFunc({
version: '1',
foo: 42
})
t.ok(success)
})
t.test('fastify integration', async t => {
const factory = AjvCompiler()
const app = fastify({
jsonShorthand: false,
ajv: {
customOptions: { },
mode: 'JTD'
},
schemaController: {
compilersFactory: {
buildValidator: factory
}
}
})
app.post('/', {
schema: {
body: {
discriminator: 'version',
mapping: {
1: {
properties: {
foo: { type: 'uint8' }
}
},
2: {
properties: {
foo: { type: 'string' }
}
}
}
}
}
}, () => {})
const res = await app.inject({
url: '/',
method: 'POST',
payload: {
version: '1',
foo: 'this is not a number'
}
})
t.equal(res.statusCode, 400)
t.equal(res.json().message, 'body/foo must be uint8')
})
})

View File

@@ -0,0 +1,264 @@
'use strict'
const t = require('tap')
const fastify = require('fastify')
const AjvCompiler = require('../index')
const ajvFormats = require('ajv-formats')
const ajvErrors = require('ajv-errors')
const localize = require('ajv-i18n')
t.test('Format Baseline test', async (t) => {
const app = buildApplication({
customOptions: {
validateFormats: false
}
})
const res = await app.inject({
url: '/hello',
headers: {
'x-foo': 'hello',
'x-date': 'not a date',
'x-email': 'not an email'
},
query: {
foo: 'hello',
date: 'not a date',
email: 'not an email'
}
})
t.equal(res.statusCode, 200, 'format validation does not apply as configured')
t.equal(res.payload, 'hello')
})
t.test('Custom Format plugin loading test', (t) => {
t.plan(6)
const app = buildApplication({
customOptions: {
validateFormats: true
},
plugins: [[ajvFormats, { mode: 'fast' }]]
})
app.inject('/hello', (err, res) => {
t.error(err)
t.equal(res.statusCode, 400, 'format validation applies')
})
app.inject('/2ad0612c-7578-4b18-9a6f-579863f40e0b', (err, res) => {
t.error(err)
t.equal(res.statusCode, 400, 'format validation applies')
})
app.inject({
url: '/2ad0612c-7578-4b18-9a6f-579863f40e0b',
headers: {
'x-foo': 'hello',
'x-date': new Date().toISOString(),
'x-email': 'foo@bar.baz'
},
query: {
foo: 'hello',
date: new Date().toISOString(),
email: 'foo@bar.baz'
}
}, (err, res) => {
t.error(err)
t.equal(res.statusCode, 200)
})
})
t.test('Format plugin set by default test', (t) => {
t.plan(6)
const app = buildApplication({})
app.inject('/hello', (err, res) => {
t.error(err)
t.equal(res.statusCode, 400, 'format validation applies')
})
app.inject('/2ad0612c-7578-4b18-9a6f-579863f40e0b', (err, res) => {
t.error(err)
t.equal(res.statusCode, 400, 'format validation applies')
})
app.inject({
url: '/2ad0612c-7578-4b18-9a6f-579863f40e0b',
headers: {
'x-foo': 'hello',
'x-date': new Date().toISOString(),
'x-email': 'foo@bar.baz'
},
query: {
foo: 'hello',
date: new Date().toISOString(),
email: 'foo@bar.baz'
}
}, (err, res) => {
t.error(err)
t.equal(res.statusCode, 200)
})
})
t.test('Custom error messages', (t) => {
t.plan(9)
const app = buildApplication({
customOptions: {
removeAdditional: false,
allErrors: true
},
plugins: [ajvFormats, ajvErrors]
})
const errorMessage = {
required: 'custom miss',
type: 'custom type', // will not replace internal "type" error for the property "foo"
_: 'custom type', // this prop will do it
additionalProperties: 'custom too many params'
}
app.post('/', {
handler: () => { t.fail('dont call me') },
schema: {
body: {
type: 'object',
required: ['foo'],
properties: {
foo: { type: 'integer' }
},
additionalProperties: false,
errorMessage
}
}
})
app.inject({
url: '/',
method: 'post',
payload: {}
}, (err, res) => {
t.error(err)
t.equal(res.statusCode, 400)
t.match(res.json().message, errorMessage.required)
})
app.inject({
url: '/',
method: 'post',
payload: { foo: 'not a number' }
}, (err, res) => {
t.error(err)
t.equal(res.statusCode, 400)
t.match(res.json().message, errorMessage.type)
})
app.inject({
url: '/',
method: 'post',
payload: { foo: 3, bar: 'ops' }
}, (err, res) => {
t.error(err)
t.equal(res.statusCode, 400)
t.match(res.json().message, errorMessage.additionalProperties)
})
})
t.test('Custom i18n error messages', (t) => {
t.plan(3)
const app = buildApplication({
customOptions: {
allErrors: true,
messages: false
},
plugins: [ajvFormats]
})
app.post('/', {
handler: () => { t.fail('dont call me') },
schema: {
body: {
type: 'object',
required: ['foo'],
properties: {
foo: { type: 'integer' }
}
}
}
})
app.setErrorHandler((error, request, reply) => {
t.pass('Error handler executed')
if (error.validation) {
localize.ru(error.validation)
reply.status(400).send(error.validation)
return
}
t.fail('not other errors')
})
app.inject({
method: 'POST',
url: '/',
payload: {
foo: 'string'
}
}, (err, res) => {
t.error(err)
t.equal(res.json()[0].message, 'должно быть integer')
})
})
function buildApplication (ajvOptions) {
const factory = AjvCompiler()
const app = fastify({
ajv: ajvOptions,
schemaController: {
compilersFactory: {
buildValidator: factory
}
}
})
app.get('/:id', {
schema: {
headers: {
type: 'object',
required: [
'x-foo',
'x-date',
'x-email'
],
properties: {
'x-foo': { type: 'string' },
'x-date': { type: 'string', format: 'date-time' },
'x-email': { type: 'string', format: 'email' }
}
},
query: {
type: 'object',
required: [
'foo',
'date',
'email'
],
properties: {
foo: { type: 'string' },
date: { type: 'string', format: 'date-time' },
email: { type: 'string', format: 'email' }
}
},
params: {
type: 'object',
properties: {
id: { type: 'string', format: 'uuid' }
}
}
}
}, async () => 'hello')
return app
}

View File

@@ -0,0 +1,279 @@
'use strict'
const t = require('tap')
const fastify = require('fastify')
const AjvCompiler = require('../index')
const jtdSchema = {
discriminator: 'version',
mapping: {
1: {
properties: {
foo: { type: 'uint8' }
}
},
2: {
properties: {
foo: { type: 'string' }
}
}
}
}
const externalSchemas1 = Object.freeze({})
const externalSchemas2 = Object.freeze({
foo: {
definitions: {
coordinates: {
properties: {
lat: { type: 'float32' },
lng: { type: 'float32' }
}
}
}
}
})
const fastifyAjvOptionsDefault = Object.freeze({
customOptions: {}
})
t.test('basic serializer usage', t => {
t.plan(4)
const factory = AjvCompiler({ jtdSerializer: true })
const compiler = factory(externalSchemas1, fastifyAjvOptionsDefault)
const serializeFunc = compiler({ schema: jtdSchema })
t.equal(serializeFunc({ version: '1', foo: 42 }), '{"version":"1","foo":42}')
t.equal(serializeFunc({ version: '2', foo: 'hello' }), '{"version":"2","foo":"hello"}')
t.equal(serializeFunc({ version: '3', foo: 'hello' }), '{"version":"3"}')
t.equal(serializeFunc({ version: '2', foo: ['not', 1, { string: 'string' }] }), '{"version":"2","foo":"not,1,[object Object]"}')
})
t.test('external schemas are ignored', t => {
t.plan(1)
const factory = AjvCompiler({ jtdSerializer: true })
const compiler = factory(externalSchemas2, fastifyAjvOptionsDefault)
const serializeFunc = compiler({
schema: {
definitions: {
coordinates: {
properties: {
lat: { type: 'float32' },
lng: { type: 'float32' }
}
}
},
properties: {
userLoc: { ref: 'coordinates' },
serverLoc: { ref: 'coordinates' }
}
}
})
t.equal(serializeFunc(
{ userLoc: { lat: 50, lng: -90 }, serverLoc: { lat: -15, lng: 50 } }),
'{"userLoc":{"lat":50,"lng":-90},"serverLoc":{"lat":-15,"lng":50}}'
)
})
t.test('fastify integration within JTD serializer', async t => {
const factoryValidator = AjvCompiler()
const factorySerializer = AjvCompiler({ jtdSerializer: true })
const app = fastify({
jsonShorthand: false,
ajv: {
customOptions: { },
mode: 'JTD'
},
schemaController: {
compilersFactory: {
buildValidator: factoryValidator,
buildSerializer: factorySerializer
}
}
})
app.post('/', {
schema: {
body: jtdSchema,
response: {
200: {
properties: {
id: { type: 'string' },
createdAt: { type: 'timestamp' },
karma: { type: 'int32' },
isAdmin: { type: 'boolean' }
}
},
400: jtdSchema
}
}
}, async () => {
return {
id: '123',
createdAt: new Date('1999-01-31T23:00:00.000Z'),
karma: 42,
isAdmin: true,
remove: 'me'
}
})
{
const res = await app.inject({
url: '/',
method: 'POST',
payload: {
version: '1',
foo: 'not a number'
}
})
t.equal(res.statusCode, 400)
t.same(res.json(), { version: 'undefined' })
}
{
const res = await app.inject({
url: '/',
method: 'POST',
payload: {
version: '1',
foo: 32
}
})
t.equal(res.statusCode, 200)
t.same(res.json(), {
id: '123',
createdAt: '1999-01-31T23:00:00.000Z',
karma: 42,
isAdmin: true
})
}
})
t.test('fastify integration and cached serializer', async t => {
const factoryValidator = AjvCompiler()
const factorySerializer = AjvCompiler({ jtdSerializer: true })
const app = fastify({
jsonShorthand: false,
ajv: {
customOptions: { },
mode: 'JTD'
},
schemaController: {
compilersFactory: {
buildValidator: factoryValidator,
buildSerializer: factorySerializer
}
}
})
app.register(async function plugin (app, opts) {
app.post('/', {
schema: {
body: jtdSchema,
response: {
200: {
properties: {
id: { type: 'string' },
createdAt: { type: 'timestamp' },
karma: { type: 'int32' },
isAdmin: { type: 'boolean' }
}
},
400: jtdSchema
}
}
}, async () => {
return {
id: '123',
createdAt: new Date('1999-01-31T23:00:00.000Z'),
karma: 42,
isAdmin: true,
remove: 'me'
}
})
})
app.register(async function plugin (app, opts) {
app.post('/two', {
schema: {
body: jtdSchema,
response: {
400: jtdSchema
}
}
}, () => {})
})
{
const res = await app.inject({
url: '/',
method: 'POST',
payload: {
version: '1',
foo: 'not a number'
}
})
t.equal(res.statusCode, 400)
t.same(res.json(), { version: 'undefined' })
}
{
const res = await app.inject({
url: '/',
method: 'POST',
payload: {
version: '1',
foo: 32
}
})
t.equal(res.statusCode, 200)
t.same(res.json(), {
id: '123',
createdAt: '1999-01-31T23:00:00.000Z',
karma: 42,
isAdmin: true
})
}
})
t.test('fastify integration within JTD serializer and custom options', async t => {
const factorySerializer = AjvCompiler({ jtdSerializer: true })
const app = fastify({
jsonShorthand: false,
serializerOpts: {
allErrors: true,
logger: 'wrong-value'
},
schemaController: {
compilersFactory: {
buildSerializer: factorySerializer
}
}
})
app.post('/', {
schema: {
response: {
200: {
properties: {
test: { type: 'boolean' }
}
}
}
}
}, async () => { })
try {
await app.ready()
t.fail('should throw')
} catch (error) {
t.equal(error.message, 'logger must implement log, warn and error methods', 'the wrong setting is forwarded to ajv/jtd')
}
})

View File

@@ -0,0 +1,203 @@
'use strict'
const fs = require('node:fs')
const path = require('node:path')
const t = require('tap')
const fastify = require('fastify')
const sanitize = require('sanitize-filename')
const { StandaloneValidator: AjvStandaloneValidator } = require('../')
function generateFileName (routeOpts) {
return `/ajv-generated-${sanitize(routeOpts.schema.$id)}-${routeOpts.method}-${routeOpts.httpPart}-${sanitize(routeOpts.url)}.js`
}
const generatedFileNames = []
t.test('standalone', t => {
t.plan(4)
t.teardown(async () => {
for (const fileName of generatedFileNames) {
await fs.promises.unlink(path.join(__dirname, fileName))
}
})
t.test('errors', t => {
t.plan(2)
t.throws(() => {
AjvStandaloneValidator()
}, 'missing restoreFunction')
t.throws(() => {
AjvStandaloneValidator({ readMode: false })
}, 'missing storeFunction')
})
t.test('generate standalone code', t => {
t.plan(5)
const base = {
$id: 'urn:schema:base',
definitions: {
hello: { type: 'string' }
},
type: 'object',
properties: {
hello: { $ref: '#/definitions/hello' }
}
}
const refSchema = {
$id: 'urn:schema:ref',
type: 'object',
properties: {
hello: { $ref: 'urn:schema:base#/definitions/hello' }
}
}
const endpointSchema = {
schema: {
$id: 'urn:schema:endpoint',
$ref: 'urn:schema:ref'
}
}
const schemaMap = {
[base.$id]: base,
[refSchema.$id]: refSchema
}
const factory = AjvStandaloneValidator({
readMode: false,
storeFunction (routeOpts, schemaValidationCode) {
t.same(routeOpts, endpointSchema)
t.type(schemaValidationCode, 'string')
fs.writeFileSync(path.join(__dirname, '/ajv-generated.js'), schemaValidationCode)
generatedFileNames.push('/ajv-generated.js')
t.pass('stored the validation function')
}
})
const compiler = factory(schemaMap)
compiler(endpointSchema)
t.pass('compiled the endpoint schema')
t.test('usage standalone code', t => {
t.plan(3)
const standaloneValidate = require('./ajv-generated')
const valid = standaloneValidate({ hello: 'world' })
t.ok(valid)
const invalid = standaloneValidate({ hello: [] })
t.notOk(invalid)
t.ok(standaloneValidate)
})
})
t.test('fastify integration - writeMode', async t => {
t.plan(6)
const factory = AjvStandaloneValidator({
readMode: false,
storeFunction (routeOpts, schemaValidationCode) {
const fileName = generateFileName(routeOpts)
t.ok(routeOpts)
fs.writeFileSync(path.join(__dirname, fileName), schemaValidationCode)
t.pass('stored the validation function')
generatedFileNames.push(fileName)
},
restoreFunction () {
t.fail('write mode ON')
}
})
const app = buildApp(factory)
await app.ready()
})
t.test('fastify integration - readMode', async t => {
t.plan(6)
const factory = AjvStandaloneValidator({
readMode: true,
storeFunction () {
t.fail('read mode ON')
},
restoreFunction (routeOpts) {
t.pass('restore the validation function')
const fileName = generateFileName(routeOpts)
return require(path.join(__dirname, fileName))
}
})
const app = buildApp(factory)
await app.ready()
let res = await app.inject({
url: '/foo',
method: 'POST',
payload: { hello: [] }
})
t.equal(res.statusCode, 400)
res = await app.inject({
url: '/bar?lang=invalid',
method: 'GET'
})
t.equal(res.statusCode, 400)
res = await app.inject({
url: '/bar?lang=it',
method: 'GET'
})
t.equal(res.statusCode, 200)
})
function buildApp (factory) {
const app = fastify({
jsonShorthand: false,
schemaController: {
compilersFactory: {
buildValidator: factory
}
}
})
app.addSchema({
$id: 'urn:schema:foo',
type: 'object',
properties: {
name: { type: 'string' },
id: { type: 'integer' }
}
})
app.post('/foo', {
schema: {
body: {
$id: 'urn:schema:body',
type: 'object',
properties: {
hello: { $ref: 'urn:schema:foo#/properties/name' }
}
}
}
}, () => { return 'ok' })
app.get('/bar', {
schema: {
query: {
$id: 'urn:schema:query',
type: 'object',
properties: {
lang: { type: 'string', enum: ['it', 'en'] }
}
}
}
}, () => { return 'ok' })
return app
}
})

View File

@@ -0,0 +1,72 @@
import { AnySchema, default as _ajv, Options as AjvOptions, ValidateFunction } from "ajv";
import { default as AjvJTD, JTDOptions } from "ajv/dist/jtd";
import type { Options, ErrorObject } from "ajv";
import { AnyValidateFunction } from "ajv/dist/core";
type Ajv = _ajv;
type AjvSerializerGenerator = typeof AjvCompiler
type AjvJTDCompile = AjvJTD['compileSerializer']
type AjvCompile = (schema: AnySchema, _meta?: boolean) => AnyValidateFunction
declare namespace AjvCompiler {
export type { Options, ErrorObject }
export { Ajv };
export type BuildSerializerFromPool = typeof buildSerializerFromPool
export type BuildCompilerFromPool = typeof buildCompilerFromPool
export const AjvReference: Symbol
export enum HttpParts {
Body = "body",
Headers = "headers",
Params = "params",
Query = "querystring",
}
export type RouteDefinition = {
method: string,
url: string,
httpPart: HttpParts,
schema?: unknown,
}
export type StandaloneRestoreFunction = (opts: RouteDefinition) => ValidateFunction
export type StandaloneStoreFunction = (opts: RouteDefinition, schemaValidationCode: string) => void
export type StandaloneOptionsReadModeOn = {
readMode: true;
restoreFunction?: StandaloneRestoreFunction
}
export type StandaloneOptionsReadModeOff = {
readMode?: false | undefined;
storeFunction?: StandaloneStoreFunction;
}
export type StandaloneOptions = StandaloneOptionsReadModeOn | StandaloneOptionsReadModeOff
export type ValidatorFactory = BuildCompilerFromPool | BuildSerializerFromPool
export type ValidatorCompiler = ReturnType<ValidatorFactory>
export { StandaloneValidator }
export const AjvCompiler: AjvSerializerGenerator
export { AjvCompiler as default }
}
declare function buildCompilerFromPool(externalSchemas: { [key: string]: AnySchema | AnySchema[] }, options?: { mode: 'JTD'; customOptions?: JTDOptions; onCreate?: (ajvInstance: Ajv) => void }): AjvCompile
declare function buildCompilerFromPool(externalSchemas: { [key: string]: AnySchema | AnySchema[] }, options?: { mode?: never; customOptions?: AjvOptions; onCreate?: (ajvInstance: Ajv) => void }): AjvCompile
declare function buildSerializerFromPool(externalSchemas: any, serializerOpts?: { mode?: never; } & JTDOptions): AjvJTDCompile
declare function AjvCompiler(opts: { jtdSerializer: true }): AjvCompiler.BuildSerializerFromPool
declare function AjvCompiler(opts?: { jtdSerializer?: false }): AjvCompiler.BuildCompilerFromPool
declare function StandaloneValidator(options: AjvCompiler.StandaloneOptions): AjvCompiler.BuildCompilerFromPool;
export = AjvCompiler

View File

@@ -0,0 +1,227 @@
import { AnySchemaObject, ValidateFunction } from "ajv";
import { AnyValidateFunction } from "ajv/dist/core";
import { expectAssignable, expectType } from "tsd";
import AjvCompiler, { AjvReference, ValidatorFactory, StandaloneValidator, RouteDefinition, ErrorObject, BuildCompilerFromPool, BuildSerializerFromPool, ValidatorCompiler } from "..";
{
const compiler = AjvCompiler({});
expectType<BuildCompilerFromPool>(compiler);
}
{
const compiler = AjvCompiler();
expectType<BuildCompilerFromPool>(compiler);
}
{
const compiler = AjvCompiler({ jtdSerializer: false});
expectType<BuildCompilerFromPool>(compiler);
}
{
const factory = AjvCompiler({ jtdSerializer: false });
expectType<BuildCompilerFromPool>(factory);
factory({}, {
onCreate(ajv) {
expectType<import("ajv").default>(ajv)
}
});
}
{
const compiler = AjvCompiler({ jtdSerializer: true});
expectType<BuildSerializerFromPool>(compiler);
}
const reader = StandaloneValidator({
readMode: true,
restoreFunction: (route: RouteDefinition) => {
expectAssignable<RouteDefinition>(route)
return {} as ValidateFunction
},
});
expectAssignable<ValidatorFactory>(reader);
const writer = StandaloneValidator({
readMode: false,
storeFunction: (route: RouteDefinition, code: string) => {
expectAssignable<RouteDefinition>(route)
expectAssignable<string>(code)
},
});
expectAssignable<ValidatorFactory>(writer);
expectType<unknown>(({} as ErrorObject).data)
expectType<string>(({} as ErrorObject).instancePath)
expectType<string>(({} as ErrorObject).keyword)
expectType<string | undefined>(({} as ErrorObject).message)
expectType<Record<string, any>>(({} as ErrorObject).params)
expectType<AnySchemaObject | undefined>(({} as ErrorObject).parentSchema)
expectType<string | undefined>(({} as ErrorObject).propertyName)
expectType<unknown>(({} as ErrorObject).schema)
expectType<string>(({} as ErrorObject).schemaPath)
expectType<Symbol>(AjvReference)
{
const jtdSchema = {
discriminator: 'version',
mapping: {
1: {
properties: {
foo: { type: 'uint8' }
}
},
2: {
properties: {
foo: { type: 'string' }
}
}
}
}
const externalSchemas1 = {
foo: {
definitions: {
coordinates: {
properties: {
lat: { type: 'float32' },
lng: { type: 'float32' }
}
}
}
}
}
const factory = AjvCompiler({ jtdSerializer: true })
expectType<BuildSerializerFromPool>(factory)
const compiler = factory(externalSchemas1, {})
expectAssignable<Function>(compiler)
const serializeFunc = compiler({ schema: jtdSchema })
expectType<(data: unknown) => string>(serializeFunc)
expectType<string>(serializeFunc({ version: '1', foo: 42 }))
}
// JTD
{
const factory = AjvCompiler()
expectType<BuildCompilerFromPool>(factory)
const jtdSchema = {
discriminator: 'version',
mapping: {
1: {
properties: {
foo: { type: 'uint8' }
}
},
2: {
properties: {
foo: { type: 'string' }
}
}
}
}
const compiler = factory({}, {
customOptions: {},
mode: 'JTD'
})
expectAssignable<ValidatorCompiler>(compiler)
const validatorFunc = compiler({ schema: jtdSchema })
expectAssignable<ValidateFunction>(validatorFunc)
expectType<boolean | Promise<any>>(validatorFunc({
version: '2',
foo: []
}))
}
// generate standalone code
{
const base = {
$id: 'urn:schema:base',
definitions: {
hello: { type: 'string' }
},
type: 'object',
properties: {
hello: { $ref: '#/definitions/hello' }
}
}
const refSchema = {
$id: 'urn:schema:ref',
type: 'object',
properties: {
hello: { $ref: 'urn:schema:base#/definitions/hello' }
}
}
const endpointSchema = {
schema: {
$id: 'urn:schema:endpoint',
$ref: 'urn:schema:ref'
}
}
const schemaMap = {
[base.$id]: base,
[refSchema.$id]: refSchema
}
const factory = StandaloneValidator({
readMode: false,
storeFunction(routeOpts, schemaValidationCode) {
expectType<RouteDefinition>(routeOpts)
expectType<string>(schemaValidationCode)
}
})
expectAssignable<ValidatorFactory>(factory)
const compiler = factory(schemaMap)
expectAssignable<ValidatorCompiler>(compiler)
expectAssignable<Function>(compiler(endpointSchema))
}
{
const base = {
$id: 'urn:schema:base',
definitions: {
hello: { type: 'string' }
},
type: 'object',
properties: {
hello: { $ref: '#/definitions/hello' }
}
}
const refSchema = {
$id: 'urn:schema:ref',
type: 'object',
properties: {
hello: { $ref: 'urn:schema:base#/definitions/hello' }
}
}
const endpointSchema = {
schema: {
$id: 'urn:schema:endpoint',
$ref: 'urn:schema:ref'
}
}
const schemaMap = {
[base.$id]: base,
[refSchema.$id]: refSchema
}
const factory = StandaloneValidator({
readMode: true,
restoreFunction(routeOpts) {
expectType<RouteDefinition>(routeOpts)
return {} as ValidateFunction
}
})
expectAssignable<ValidatorFactory>(factory)
const compiler = factory(schemaMap)
expectAssignable<ValidatorCompiler>(compiler)
expectType<AnyValidateFunction<any>>(compiler(endpointSchema))
}

8
backend/node_modules/@fastify/cors/.editorconfig generated vendored Normal file
View File

@@ -0,0 +1,8 @@
root = true
[*]
indent_style = space
indent_size = 2
charset = utf-8
trim_trailing_whitespace = false
insert_final_newline = false

5
backend/node_modules/@fastify/cors/.gitattributes generated vendored Normal file
View File

@@ -0,0 +1,5 @@
# Set the default behavior, in case people don't have core.autocrlf set
* text=auto
# Require Unix line endings
* text eol=lf

View File

@@ -0,0 +1,16 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"
open-pull-requests-limit: 10
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
ignore:
- dependency-name: tap
update-types: ["version-update:semver-major"]

21
backend/node_modules/@fastify/cors/.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,23 @@
name: CI
on:
push:
branches:
- main
- master
- next
- 'v*'
paths-ignore:
- 'docs/**'
- '*.md'
pull_request:
paths-ignore:
- 'docs/**'
- '*.md'
jobs:
test:
uses: fastify/workflows/.github/workflows/plugins-ci.yml@v3
with:
lint: true
license-check: true

4
backend/node_modules/@fastify/cors/.taprc generated vendored Normal file
View File

@@ -0,0 +1,4 @@
ts: false
jsx: false
coverage: true
flow: true

21
backend/node_modules/@fastify/cors/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 Fastify
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.

169
backend/node_modules/@fastify/cors/README.md generated vendored Normal file
View File

@@ -0,0 +1,169 @@
# @fastify/cors
![CI](https://github.com/fastify/fastify-cors/workflows/CI/badge.svg)
[![NPM version](https://img.shields.io/npm/v/@fastify/cors.svg?style=flat)](https://www.npmjs.com/package/@fastify/cors)
[![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](https://standardjs.com/)
`@fastify/cors` enables the use of [CORS](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing) in a Fastify application.
## Install
```
npm i @fastify/cors
```
### Compatibility
| Plugin version | Fastify version |
| -------------- |---------------- |
| `^8.0.0` | `^4.0.0` |
| `^7.0.0` | `^3.0.0` |
| `^3.0.0` | `^2.0.0` |
| `^1.0.0` | `^1.0.0` |
Please note that if a Fastify version is out of support, then so are the corresponding version(s) 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
Require `@fastify/cors` and register it as any other plugin, it will add an `onRequest` hook and a [wildcard options route](https://github.com/fastify/fastify/issues/326#issuecomment-411360862).
```js
import Fastify from 'fastify'
import cors from '@fastify/cors'
const fastify = Fastify()
await fastify.register(cors, {
// put your options here
})
fastify.get('/', (req, reply) => {
reply.send({ hello: 'world' })
})
await fastify.listen({ port: 3000 })
```
You can use it as is without passing any option or you can configure it as explained below.
### Options
* `origin`: Configures the **Access-Control-Allow-Origin** CORS header. The value of origin could be of different types:
- `Boolean` - set `origin` to `true` to reflect the [request origin](http://tools.ietf.org/html/draft-abarth-origin-09), or set it to `false` to disable CORS.
- `String` - set `origin` to a specific origin. For example if you set it to `"http://example.com"` only requests from "http://example.com" will be allowed. The special `*` value (default) allows any origin.
- `RegExp` - set `origin` to a regular expression pattern that will be used to test the request origin. If it is a match, the request origin will be reflected. For example, the pattern `/example\.com$/` will reflect any request that is coming from an origin ending with "example.com".
- `Array` - set `origin` to an array of valid origins. Each origin can be a `String` or a `RegExp`. For example `["http://example1.com", /\.example2\.com$/]` will accept any request from "http://example1.com" or from a subdomain of "example2.com".
- `Function` - set `origin` to a function implementing some custom logic. The function takes the request origin as the first parameter and a callback as a second (which expects the signature `err [Error | null], origin`), where `origin` is a non-function value of the origin option. *Async-await* and promises are supported as well. The Fastify instance is bound to function call and you may access via `this`. For example:
```js
origin: (origin, cb) => {
const hostname = new URL(origin).hostname
if(hostname === "localhost"){
// Request from localhost will pass
cb(null, true)
return
}
// Generate an error on other origins, disabling access
cb(new Error("Not allowed"), false)
}
```
* `methods`: Configures the **Access-Control-Allow-Methods** CORS header. Expects a comma-delimited string (ex: 'GET,PUT,POST') or an array (ex: `['GET', 'PUT', 'POST']`).
* `hook`: See the section `Custom Fastify hook name` (default: `onRequest`)
* `allowedHeaders`: Configures the **Access-Control-Allow-Headers** CORS header. Expects a comma-delimited string (ex: `'Content-Type,Authorization'`) or an array (ex: `['Content-Type', 'Authorization']`). If not specified, defaults to reflecting the headers specified in the request's **Access-Control-Request-Headers** header.
* `exposedHeaders`: Configures the **Access-Control-Expose-Headers** CORS header. Expects a comma-delimited string (ex: `'Content-Range,X-Content-Range'`) or an array (ex: `['Content-Range', 'X-Content-Range']`). If not specified, no custom headers are exposed.
* `credentials`: Configures the **Access-Control-Allow-Credentials** CORS header. Set to `true` to pass the header, otherwise it is omitted.
* `maxAge`: Configures the **Access-Control-Max-Age** CORS header. In seconds. Set to an integer to pass the header, otherwise it is omitted.
* `cacheControl`: Configures the **Cache-Control** header for CORS preflight responses. Set to an integer to pass the header as `Cache-Control: max-age=${cacheControl}`, or set to a string to pass the header as `Cache-Control: ${cacheControl}` (fully define the header value), otherwise the header is omitted.
* `preflightContinue`: Pass the CORS preflight response to the route handler (default: `false`).
* `optionsSuccessStatus`: Provides a status code to use for successful `OPTIONS` requests, since some legacy browsers (IE11, various SmartTVs) choke on `204`.
* `preflight`: if needed you can entirely disable preflight by passing `false` here (default: `true`).
* `strictPreflight`: Enforces strict requirement of the CORS preflight request headers (**Access-Control-Request-Method** and **Origin**) as defined by the [W3C CORS specification](https://www.w3.org/TR/2020/SPSD-cors-20200602/#resource-preflight-requests) (the current [fetch living specification](https://fetch.spec.whatwg.org/) does not define server behavior for missing headers). Preflight requests without the required headers will result in 400 errors when set to `true` (default: `true`).
* `hideOptionsRoute`: hide options route from the documentation built using [@fastify/swagger](https://github.com/fastify/fastify-swagger) (default: `true`).
### Configuring CORS Asynchronously
```js
const fastify = require('fastify')()
fastify.register(require('@fastify/cors'), (instance) => {
return (req, callback) => {
const corsOptions = {
// This is NOT recommended for production as it enables reflection exploits
origin: true
};
// do not include CORS headers for requests from localhost
if (/^localhost$/m.test(req.headers.origin)) {
corsOptions.origin = false
}
// callback expects two parameters: error and options
callback(null, corsOptions)
}
})
fastify.register(async function (fastify) {
fastify.get('/', (req, reply) => {
reply.send({ hello: 'world' })
})
})
fastify.listen({ port: 3000 })
```
### Custom Fastify hook name
By default, `@fastify/cors` adds a `onRequest` hook where the validation and header injection are executed. This can be customized by passing `hook` in the options. Valid values are `onRequest`, `preParsing`, `preValidation`, `preHandler`, `preSerialization`, and `onSend`.
```js
import Fastify from 'fastify'
import cors from '@fastify/cors'
const fastify = Fastify()
await fastify.register(cors, {
hook: 'preHandler',
})
fastify.get('/', (req, reply) => {
reply.send({ hello: 'world' })
})
await fastify.listen({ port: 3000 })
```
When configuring CORS asynchronously, an object with `delegator` key is expected:
```js
const fastify = require('fastify')()
fastify.register(require('@fastify/cors'), {
hook: 'preHandler',
delegator: (req, callback) => {
const corsOptions = {
// This is NOT recommended for production as it enables reflection exploits
origin: true
};
// do not include CORS headers for requests from localhost
if (/^localhost$/m.test(req.headers.origin)) {
corsOptions.origin = false
}
// callback expects two parameters: error and options
callback(null, corsOptions)
},
})
fastify.register(async function (fastify) {
fastify.get('/', (req, reply) => {
reply.send({ hello: 'world' })
})
})
fastify.listen({ port: 3000 })
```
## Acknowledgements
The code is a port for Fastify of [`expressjs/cors`](https://github.com/expressjs/cors).
## License
Licensed under [MIT](./LICENSE).<br/>
[`expressjs/cors` license](https://github.com/expressjs/cors/blob/master/LICENSE)

17
backend/node_modules/@fastify/cors/bench.js generated vendored Normal file
View File

@@ -0,0 +1,17 @@
'use strict'
const fastify = require('fastify')()
fastify.register((instance, opts, next) => {
instance.register(require('./index'))
instance.get('/fastify', (req, reply) => reply.send('ok'))
next()
})
fastify.register((instance, opts, next) => {
instance.use(require('cors')())
instance.get('/express', (req, reply) => reply.send('ok'))
next()
})
fastify.listen({ port: 3000 })

View File

@@ -0,0 +1,15 @@
{
"name": "benchmark",
"version": "1.0.0",
"description": "",
"main": "vary.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"benchmark": "^2.1.4",
"vary": "^1.1.2"
}
}

34
backend/node_modules/@fastify/cors/benchmark/vary.js generated vendored Normal file
View File

@@ -0,0 +1,34 @@
'use strict'
const benchmark = require('benchmark')
const vary = require('vary')
const createAddFieldnameToVary = require('../vary').createAddFieldnameToVary
const replyMock = (header) => ({
getHeader () { return header },
setHeader () { },
header () { }
})
const addAcceptToVary = createAddFieldnameToVary('Accept')
const addWildcardToVary = createAddFieldnameToVary('*')
const addAcceptEncodingToVary = createAddFieldnameToVary('Accept-Encoding')
const addXFooToVary = createAddFieldnameToVary('X-Foo')
new benchmark.Suite()
.add('vary - field to undefined', function () { vary(replyMock(undefined), 'Accept-Encoding') }, { minSamples: 100 })
.add('vary - field to *', function () { vary(replyMock('*'), 'Accept-Encoding') }, { minSamples: 100 })
.add('vary - * to field', function () { vary(replyMock('Accept-Encoding'), '*') }, { minSamples: 100 })
.add('vary - field to empty', function () { vary(replyMock(''), 'Accept-Encoding') }, { minSamples: 100 })
.add('vary - fields string to empty', function () { vary(replyMock(''), 'Accept') }, { minSamples: 100 })
.add('vary - field to fields', function () { vary(replyMock('Accept, Accept-Encoding, Accept-Language'), 'X-Foo') }, { minSamples: 100 })
.add('cors - field to undefined', function () { addAcceptEncodingToVary(replyMock(undefined)) }, { minSamples: 100 })
.add('cors - field to *', function () { addAcceptEncodingToVary(replyMock('*')) }, { minSamples: 100 })
.add('cors - * to field', function () { addWildcardToVary(replyMock('Accept-Encoding')) }, { minSamples: 100 })
.add('cors - field to empty', function () { addAcceptEncodingToVary(replyMock('')) }, { minSamples: 100 })
.add('cors - fields string to empty', function () { addAcceptToVary(replyMock('')) }, { minSamples: 100 })
.add('cors - field to fields', function () { addXFooToVary(replyMock('Accept, Accept-Encoding, Accept-Language')) }, { minSamples: 100 })
.on('cycle', function onCycle (event) { console.log(String(event.target)) })
.run({ async: false })

307
backend/node_modules/@fastify/cors/index.js generated vendored Normal file
View File

@@ -0,0 +1,307 @@
'use strict'
const fp = require('fastify-plugin')
const {
addAccessControlRequestHeadersToVaryHeader,
addOriginToVaryHeader
} = require('./vary')
const defaultOptions = {
origin: '*',
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
hook: 'onRequest',
preflightContinue: false,
optionsSuccessStatus: 204,
credentials: false,
exposedHeaders: null,
allowedHeaders: null,
maxAge: null,
preflight: true,
strictPreflight: true
}
const validHooks = [
'onRequest',
'preParsing',
'preValidation',
'preHandler',
'preSerialization',
'onSend'
]
const hookWithPayload = [
'preSerialization',
'preParsing',
'onSend'
]
function validateHook (value, next) {
if (validHooks.indexOf(value) !== -1) {
return
}
next(new TypeError('@fastify/cors: Invalid hook option provided.'))
}
function fastifyCors (fastify, opts, next) {
fastify.decorateRequest('corsPreflightEnabled', false)
let hideOptionsRoute = true
if (typeof opts === 'function') {
handleCorsOptionsDelegator(opts, fastify, { hook: defaultOptions.hook }, next)
} else if (opts.delegator) {
const { delegator, ...options } = opts
handleCorsOptionsDelegator(delegator, fastify, options, next)
} else {
if (opts.hideOptionsRoute !== undefined) hideOptionsRoute = opts.hideOptionsRoute
const corsOptions = normalizeCorsOptions(opts)
validateHook(corsOptions.hook, next)
if (hookWithPayload.indexOf(corsOptions.hook) !== -1) {
fastify.addHook(corsOptions.hook, function handleCors (req, reply, payload, next) {
addCorsHeadersHandler(fastify, corsOptions, req, reply, next)
})
} else {
fastify.addHook(corsOptions.hook, function handleCors (req, reply, next) {
addCorsHeadersHandler(fastify, corsOptions, req, reply, next)
})
}
}
// The preflight reply must occur in the hook. This allows fastify-cors to reply to
// preflight requests BEFORE possible authentication plugins. If the preflight reply
// occurred in this handler, other plugins may deny the request since the browser will
// remove most headers (such as the Authentication header).
//
// This route simply enables fastify to accept preflight requests.
fastify.options('*', { schema: { hide: hideOptionsRoute } }, (req, reply) => {
if (!req.corsPreflightEnabled) {
// Do not handle preflight requests if the origin option disabled CORS
reply.callNotFound()
return
}
reply.send()
})
next()
}
function handleCorsOptionsDelegator (optionsResolver, fastify, opts, next) {
const hook = (opts && opts.hook) || defaultOptions.hook
validateHook(hook, next)
if (optionsResolver.length === 2) {
if (hookWithPayload.indexOf(hook) !== -1) {
fastify.addHook(hook, function handleCors (req, reply, payload, next) {
handleCorsOptionsCallbackDelegator(optionsResolver, fastify, req, reply, next)
})
} else {
fastify.addHook(hook, function handleCors (req, reply, next) {
handleCorsOptionsCallbackDelegator(optionsResolver, fastify, req, reply, next)
})
}
} else {
if (hookWithPayload.indexOf(hook) !== -1) {
// handle delegator based on Promise
fastify.addHook(hook, function handleCors (req, reply, payload, next) {
const ret = optionsResolver(req)
if (ret && typeof ret.then === 'function') {
ret.then(options => addCorsHeadersHandler(fastify, normalizeCorsOptions(options, true), req, reply, next)).catch(next)
return
}
next(new Error('Invalid CORS origin option'))
})
} else {
// handle delegator based on Promise
fastify.addHook(hook, function handleCors (req, reply, next) {
const ret = optionsResolver(req)
if (ret && typeof ret.then === 'function') {
ret.then(options => addCorsHeadersHandler(fastify, normalizeCorsOptions(options, true), req, reply, next)).catch(next)
return
}
next(new Error('Invalid CORS origin option'))
})
}
}
}
function handleCorsOptionsCallbackDelegator (optionsResolver, fastify, req, reply, next) {
optionsResolver(req, (err, options) => {
if (err) {
next(err)
} else {
addCorsHeadersHandler(fastify, normalizeCorsOptions(options, true), req, reply, next)
}
})
}
/**
* @param {import('./types').FastifyCorsOptions} opts
*/
function normalizeCorsOptions (opts, dynamic) {
const corsOptions = { ...defaultOptions, ...opts }
if (Array.isArray(opts.origin) && opts.origin.indexOf('*') !== -1) {
corsOptions.origin = '*'
}
if (Number.isInteger(corsOptions.cacheControl)) {
// integer numbers are formatted this way
corsOptions.cacheControl = `max-age=${corsOptions.cacheControl}`
} else if (typeof corsOptions.cacheControl !== 'string') {
// strings are applied directly and any other value is ignored
corsOptions.cacheControl = null
}
corsOptions.dynamic = dynamic || false
return corsOptions
}
function addCorsHeadersHandler (fastify, options, req, reply, next) {
if ((typeof options.origin !== 'string' && options.origin !== false) || options.dynamic) {
// Always set Vary header for non-static origin option
// https://fetch.spec.whatwg.org/#cors-protocol-and-http-caches
addOriginToVaryHeader(reply)
}
const resolveOriginOption = typeof options.origin === 'function' ? resolveOriginWrapper(fastify, options.origin) : (_, cb) => cb(null, options.origin)
resolveOriginOption(req, (error, resolvedOriginOption) => {
if (error !== null) {
return next(error)
}
// Disable CORS and preflight if false
if (resolvedOriginOption === false) {
return next()
}
// Falsy values are invalid
if (!resolvedOriginOption) {
return next(new Error('Invalid CORS origin option'))
}
addCorsHeaders(req, reply, resolvedOriginOption, options)
if (req.raw.method === 'OPTIONS' && options.preflight === true) {
// Strict mode enforces the required headers for preflight
if (options.strictPreflight === true && (!req.headers.origin || !req.headers['access-control-request-method'])) {
reply.status(400).type('text/plain').send('Invalid Preflight Request')
return
}
req.corsPreflightEnabled = true
addPreflightHeaders(req, reply, options)
if (!options.preflightContinue) {
// Do not call the hook callback and terminate the request
// Safari (and potentially other browsers) need content-length 0,
// for 204 or they just hang waiting for a body
reply
.code(options.optionsSuccessStatus)
.header('Content-Length', '0')
.send()
return
}
}
return next()
})
}
function addCorsHeaders (req, reply, originOption, corsOptions) {
const origin = getAccessControlAllowOriginHeader(req.headers.origin, originOption)
// In the case of origin not allowed the header is not
// written in the response.
// https://github.com/fastify/fastify-cors/issues/127
if (origin) {
reply.header('Access-Control-Allow-Origin', origin)
}
if (corsOptions.credentials) {
reply.header('Access-Control-Allow-Credentials', 'true')
}
if (corsOptions.exposedHeaders !== null) {
reply.header(
'Access-Control-Expose-Headers',
Array.isArray(corsOptions.exposedHeaders) ? corsOptions.exposedHeaders.join(', ') : corsOptions.exposedHeaders
)
}
}
function addPreflightHeaders (req, reply, corsOptions) {
reply.header(
'Access-Control-Allow-Methods',
Array.isArray(corsOptions.methods) ? corsOptions.methods.join(', ') : corsOptions.methods
)
if (corsOptions.allowedHeaders === null) {
addAccessControlRequestHeadersToVaryHeader(reply)
const reqAllowedHeaders = req.headers['access-control-request-headers']
if (reqAllowedHeaders !== undefined) {
reply.header('Access-Control-Allow-Headers', reqAllowedHeaders)
}
} else {
reply.header(
'Access-Control-Allow-Headers',
Array.isArray(corsOptions.allowedHeaders) ? corsOptions.allowedHeaders.join(', ') : corsOptions.allowedHeaders
)
}
if (corsOptions.maxAge !== null) {
reply.header('Access-Control-Max-Age', String(corsOptions.maxAge))
}
if (corsOptions.cacheControl) {
reply.header('Cache-Control', corsOptions.cacheControl)
}
}
function resolveOriginWrapper (fastify, origin) {
return function (req, cb) {
const result = origin.call(fastify, req.headers.origin, cb)
// Allow for promises
if (result && typeof result.then === 'function') {
result.then(res => cb(null, res), cb)
}
}
}
function getAccessControlAllowOriginHeader (reqOrigin, originOption) {
if (typeof originOption === 'string') {
// fixed or any origin ('*')
return originOption
}
// reflect origin
return isRequestOriginAllowed(reqOrigin, originOption) ? reqOrigin : false
}
function isRequestOriginAllowed (reqOrigin, allowedOrigin) {
if (Array.isArray(allowedOrigin)) {
for (let i = 0; i < allowedOrigin.length; ++i) {
if (isRequestOriginAllowed(reqOrigin, allowedOrigin[i])) {
return true
}
}
return false
} else if (typeof allowedOrigin === 'string') {
return reqOrigin === allowedOrigin
} else if (allowedOrigin instanceof RegExp) {
allowedOrigin.lastIndex = 0
return allowedOrigin.test(reqOrigin)
} else {
return !!allowedOrigin
}
}
const _fastifyCors = fp(fastifyCors, {
fastify: '4.x',
name: '@fastify/cors'
})
/**
* These export configurations enable JS and TS developers
* to consumer fastify in whatever way best suits their needs.
*/
module.exports = _fastifyCors
module.exports.fastifyCors = _fastifyCors
module.exports.default = _fastifyCors

59
backend/node_modules/@fastify/cors/package.json generated vendored Normal file
View File

@@ -0,0 +1,59 @@
{
"name": "@fastify/cors",
"version": "9.0.1",
"description": "Fastify CORS",
"main": "index.js",
"type": "commonjs",
"types": "types/index.d.ts",
"scripts": {
"coverage": "tap --cov --coverage-report=html test",
"lint": "standard",
"lint:fix": "standard --fix",
"test": "npm run test:unit && npm run test:typescript",
"test:typescript": "tsd",
"test:unit": "tap test/*.test.js"
},
"keywords": [
"fastify",
"cors",
"headers",
"access",
"control"
],
"author": "Tomas Della Vedova - @delvedor (http://delved.org)",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/fastify/fastify-cors.git"
},
"bugs": {
"url": "https://github.com/fastify/fastify-cors/issues"
},
"homepage": "https://github.com/fastify/fastify-cors#readme",
"devDependencies": {
"@fastify/pre-commit": "^2.0.2",
"@types/node": "^20.1.0",
"@typescript-eslint/eslint-plugin": "^6.4.1",
"@typescript-eslint/parser": "^6.4.0",
"cors": "^2.8.5",
"fastify": "^4.0.0-rc.2",
"standard": "^17.0.0",
"tap": "16.3.9",
"tsd": "^0.30.0",
"typescript": "^5.0.2"
},
"dependencies": {
"fastify-plugin": "^4.0.0",
"mnemonist": "0.39.6"
},
"tsd": {
"directory": "test"
},
"publishConfig": {
"access": "public"
},
"pre-commit": [
"lint",
"test"
]
}

941
backend/node_modules/@fastify/cors/test/cors.test.js generated vendored Normal file
View File

@@ -0,0 +1,941 @@
'use strict'
const { test } = require('tap')
const { createReadStream, statSync, readFileSync } = require('node:fs')
const Fastify = require('fastify')
const cors = require('../')
const { resolve } = require('node:path')
test('Should add cors headers', t => {
t.plan(4)
const fastify = Fastify()
fastify.register(cors)
fastify.get('/', (req, reply) => {
reply.send('ok')
})
fastify.inject({
method: 'GET',
url: '/'
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 200)
t.equal(res.payload, 'ok')
t.match(res.headers, {
'access-control-allow-origin': '*'
})
})
})
test('Should add cors headers when payload is a stream', t => {
t.plan(4)
const fastify = Fastify()
fastify.register(cors)
const filePath = resolve(__dirname, __filename)
fastify.get('/', (req, reply) => {
const stream = createReadStream(filePath)
reply
.type('application/json')
.header('Content-Length', statSync(filePath).size)
.send(stream)
})
const fileContent = readFileSync(filePath, 'utf-8')
fastify.inject({
method: 'GET',
url: '/'
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 200)
t.equal(res.payload, fileContent)
t.match(res.headers, {
'access-control-allow-origin': '*',
'content-length': statSync(filePath).size
})
})
})
test('Should add cors headers (custom values)', t => {
t.plan(10)
const fastify = Fastify()
fastify.register(cors, {
origin: 'example.com',
methods: 'GET',
credentials: true,
exposedHeaders: ['foo', 'bar'],
allowedHeaders: ['baz', 'woo'],
maxAge: 123,
cacheControl: 321
})
fastify.get('/', (req, reply) => {
reply.send('ok')
})
fastify.inject({
method: 'OPTIONS',
url: '/',
headers: {
'access-control-request-method': 'GET',
origin: 'example.com'
}
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 204)
t.equal(res.payload, '')
t.match(res.headers, {
'access-control-allow-origin': 'example.com',
'access-control-allow-credentials': 'true',
'access-control-expose-headers': 'foo, bar',
'access-control-allow-methods': 'GET',
'access-control-allow-headers': 'baz, woo',
'access-control-max-age': '123',
'cache-control': 'max-age=321',
'content-length': '0'
})
t.notMatch(res.headers, { vary: 'Origin' })
})
fastify.inject({
method: 'GET',
url: '/'
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 200)
t.equal(res.payload, 'ok')
t.match(res.headers, {
'access-control-allow-origin': 'example.com',
'access-control-allow-credentials': 'true',
'access-control-expose-headers': 'foo, bar',
'content-length': '2'
})
t.notMatch(res.headers, { vary: 'Origin' })
})
})
test('Should support dynamic config (callback)', t => {
t.plan(16)
const configs = [{
origin: 'example.com',
methods: 'GET',
credentials: true,
exposedHeaders: ['foo', 'bar'],
allowedHeaders: ['baz', 'woo'],
maxAge: 123,
cacheControl: 456
}, {
origin: 'sample.com',
methods: 'GET',
credentials: true,
exposedHeaders: ['zoo', 'bar'],
allowedHeaders: ['baz', 'foo'],
maxAge: 321,
cacheControl: '456'
}]
const fastify = Fastify()
let requestId = 0
const configDelegation = function (req, cb) {
// request should have id
t.ok(req.id)
// request should not have send
t.notOk(req.send)
const config = configs[requestId]
requestId++
if (config) {
cb(null, config)
} else {
cb(new Error('ouch'))
}
}
fastify.register(cors, () => configDelegation)
fastify.get('/', (req, reply) => {
reply.send('ok')
})
fastify.inject({
method: 'GET',
url: '/'
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 200)
t.equal(res.payload, 'ok')
t.match(res.headers, {
'access-control-allow-origin': 'example.com',
'access-control-allow-credentials': 'true',
'access-control-expose-headers': 'foo, bar',
'content-length': '2',
vary: 'Origin'
})
})
fastify.inject({
method: 'OPTIONS',
url: '/',
headers: {
'access-control-request-method': 'GET',
origin: 'example.com'
}
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 204)
t.equal(res.payload, '')
t.match(res.headers, {
'access-control-allow-origin': 'sample.com',
'access-control-allow-credentials': 'true',
'access-control-expose-headers': 'zoo, bar',
'access-control-allow-methods': 'GET',
'access-control-allow-headers': 'baz, foo',
'access-control-max-age': '321',
'cache-control': '456',
'content-length': '0',
vary: 'Origin'
})
})
fastify.inject({
method: 'GET',
url: '/',
headers: {
'access-control-request-method': 'GET',
origin: 'example.com'
}
}, (err, res) => {
t.error(err)
t.equal(res.statusCode, 500)
})
})
test('Should support dynamic config (Promise)', t => {
t.plan(23)
const configs = [{
origin: 'example.com',
methods: 'GET',
credentials: true,
exposedHeaders: ['foo', 'bar'],
allowedHeaders: ['baz', 'woo'],
maxAge: 123,
cacheControl: 456
}, {
origin: 'sample.com',
methods: 'GET',
credentials: true,
exposedHeaders: ['zoo', 'bar'],
allowedHeaders: ['baz', 'foo'],
maxAge: 321,
cacheControl: true // Invalid value should be ignored
}, {
origin: 'sample.com',
methods: 'GET',
credentials: true,
exposedHeaders: ['zoo', 'bar'],
allowedHeaders: ['baz', 'foo'],
maxAge: 321,
cacheControl: 'public, max-age=456'
}]
const fastify = Fastify()
let requestId = 0
const configDelegation = function (req) {
// request should have id
t.ok(req.id)
// request should not have send
t.notOk(req.send)
const config = configs[requestId]
requestId++
if (config) {
return Promise.resolve(config)
} else {
return Promise.reject(new Error('ouch'))
}
}
fastify.register(cors, () => configDelegation)
fastify.get('/', (req, reply) => {
reply.send('ok')
})
fastify.inject({
method: 'GET',
url: '/'
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 200)
t.equal(res.payload, 'ok')
t.match(res.headers, {
'access-control-allow-origin': 'example.com',
'access-control-allow-credentials': 'true',
'access-control-expose-headers': 'foo, bar',
'content-length': '2',
vary: 'Origin'
})
})
fastify.inject({
method: 'OPTIONS',
url: '/',
headers: {
'access-control-request-method': 'GET',
origin: 'sample.com'
}
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 204)
t.equal(res.payload, '')
t.match(res.headers, {
'access-control-allow-origin': 'sample.com',
'access-control-allow-credentials': 'true',
'access-control-expose-headers': 'zoo, bar',
'access-control-allow-methods': 'GET',
'access-control-allow-headers': 'baz, foo',
'access-control-max-age': '321',
'content-length': '0',
vary: 'Origin'
})
t.equal(res.headers['cache-control'], undefined, 'cache-control omitted (invalid value)')
})
fastify.inject({
method: 'OPTIONS',
url: '/',
headers: {
'access-control-request-method': 'GET',
origin: 'example.com'
}
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 204)
t.equal(res.payload, '')
t.match(res.headers, {
'access-control-allow-origin': 'sample.com',
'access-control-allow-credentials': 'true',
'access-control-expose-headers': 'zoo, bar',
'access-control-allow-methods': 'GET',
'access-control-allow-headers': 'baz, foo',
'access-control-max-age': '321',
'cache-control': 'public, max-age=456', // cache-control included (custom string)
'content-length': '0',
vary: 'Origin'
})
})
fastify.inject({
method: 'GET',
url: '/',
headers: {
'access-control-request-method': 'GET',
origin: 'example.com'
}
}, (err, res) => {
t.error(err)
t.equal(res.statusCode, 500)
})
})
test('Should support dynamic config. (Invalid function)', t => {
t.plan(2)
const fastify = Fastify()
fastify.register(cors, () => (a, b, c) => {})
fastify.get('/', (req, reply) => {
reply.send('ok')
})
fastify.inject({
method: 'GET',
url: '/',
headers: {
'access-control-request-method': 'GET',
origin: 'example.com'
}
}, (err, res) => {
t.error(err)
t.equal(res.statusCode, 500)
})
})
test('Dynamic origin resolution (valid origin)', t => {
t.plan(6)
const fastify = Fastify()
const origin = function (header, cb) {
t.equal(header, 'example.com')
t.same(this, fastify)
cb(null, true)
}
fastify.register(cors, { origin })
fastify.get('/', (req, reply) => {
reply.send('ok')
})
fastify.inject({
method: 'GET',
url: '/',
headers: { origin: 'example.com' }
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 200)
t.equal(res.payload, 'ok')
t.match(res.headers, {
'access-control-allow-origin': 'example.com',
vary: 'Origin'
})
})
})
test('Dynamic origin resolution (not valid origin)', t => {
t.plan(5)
const fastify = Fastify()
const origin = (header, cb) => {
t.equal(header, 'example.com')
cb(null, false)
}
fastify.register(cors, { origin })
fastify.get('/', (req, reply) => {
reply.send('ok')
})
fastify.inject({
method: 'GET',
url: '/',
headers: { origin: 'example.com' }
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 200)
t.equal(res.payload, 'ok')
t.same(res.headers, {
'content-length': '2',
'content-type': 'text/plain; charset=utf-8',
connection: 'keep-alive',
vary: 'Origin'
})
})
})
test('Dynamic origin resolution (errored)', t => {
t.plan(3)
const fastify = Fastify()
const origin = (header, cb) => {
t.equal(header, 'example.com')
cb(new Error('ouch'))
}
fastify.register(cors, { origin })
fastify.inject({
method: 'GET',
url: '/',
headers: { origin: 'example.com' }
}, (err, res) => {
t.error(err)
t.equal(res.statusCode, 500)
})
})
test('Dynamic origin resolution (invalid result)', t => {
t.plan(3)
const fastify = Fastify()
const origin = (header, cb) => {
t.equal(header, 'example.com')
cb(null, undefined)
}
fastify.register(cors, { origin })
fastify.inject({
method: 'GET',
url: '/',
headers: { origin: 'example.com' }
}, (err, res) => {
t.error(err)
t.equal(res.statusCode, 500)
})
})
test('Dynamic origin resolution (valid origin - promises)', t => {
t.plan(5)
const fastify = Fastify()
const origin = (header, cb) => {
return new Promise((resolve, reject) => {
t.equal(header, 'example.com')
resolve(true)
})
}
fastify.register(cors, { origin })
fastify.get('/', (req, reply) => {
reply.send('ok')
})
fastify.inject({
method: 'GET',
url: '/',
headers: { origin: 'example.com' }
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 200)
t.equal(res.payload, 'ok')
t.match(res.headers, {
'access-control-allow-origin': 'example.com',
vary: 'Origin'
})
})
})
test('Dynamic origin resolution (not valid origin - promises)', t => {
t.plan(5)
const fastify = Fastify()
const origin = (header, cb) => {
return new Promise((resolve, reject) => {
t.equal(header, 'example.com')
resolve(false)
})
}
fastify.register(cors, { origin })
fastify.get('/', (req, reply) => {
reply.send('ok')
})
fastify.inject({
method: 'GET',
url: '/',
headers: { origin: 'example.com' }
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 200)
t.equal(res.payload, 'ok')
t.same(res.headers, {
'content-length': '2',
'content-type': 'text/plain; charset=utf-8',
connection: 'keep-alive',
vary: 'Origin'
})
})
})
test('Dynamic origin resolution (errored - promises)', t => {
t.plan(3)
const fastify = Fastify()
const origin = (header, cb) => {
return new Promise((resolve, reject) => {
t.equal(header, 'example.com')
reject(new Error('ouch'))
})
}
fastify.register(cors, { origin })
fastify.inject({
method: 'GET',
url: '/',
headers: { origin: 'example.com' }
}, (err, res) => {
t.error(err)
t.equal(res.statusCode, 500)
})
})
test('Should reply 404 without cors headers when origin is false', t => {
t.plan(8)
const fastify = Fastify()
fastify.register(cors, {
origin: false,
methods: 'GET',
credentials: true,
exposedHeaders: ['foo', 'bar'],
allowedHeaders: ['baz', 'woo'],
maxAge: 123
})
fastify.get('/', (req, reply) => {
reply.send('ok')
})
fastify.inject({
method: 'OPTIONS',
url: '/'
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 404)
t.equal(res.payload, '{"message":"Route OPTIONS:/ not found","error":"Not Found","statusCode":404}')
t.same(res.headers, {
'content-length': '76',
'content-type': 'application/json; charset=utf-8',
connection: 'keep-alive'
})
})
fastify.inject({
method: 'GET',
url: '/'
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 200)
t.equal(res.payload, 'ok')
t.same(res.headers, {
'content-length': '2',
'content-type': 'text/plain; charset=utf-8',
connection: 'keep-alive'
})
})
})
test('Server error if origin option is falsy but not false', t => {
t.plan(4)
const fastify = Fastify()
fastify.register(cors, { origin: '' })
fastify.inject({
method: 'GET',
url: '/',
headers: { origin: 'example.com' }
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 500)
t.same(res.json(), { statusCode: 500, error: 'Internal Server Error', message: 'Invalid CORS origin option' })
t.same(res.headers, {
'content-length': '89',
'content-type': 'application/json; charset=utf-8',
connection: 'keep-alive'
})
})
})
test('Allow only request from a specific origin', t => {
t.plan(5)
const fastify = Fastify()
fastify.register(cors, { origin: 'other.io' })
fastify.get('/', (req, reply) => {
reply.send('ok')
})
fastify.inject({
method: 'GET',
url: '/',
headers: { origin: 'example.com' }
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 200)
t.equal(res.payload, 'ok')
t.match(res.headers, {
'access-control-allow-origin': 'other.io'
})
t.notMatch(res.headers, { vary: 'Origin' })
})
})
test('Allow only request from multiple specific origin', t => {
t.plan(9)
const fastify = Fastify()
fastify.register(cors, { origin: ['other.io', 'example.com'] })
fastify.get('/', (req, reply) => {
reply.send('ok')
})
fastify.inject({
method: 'GET',
url: '/',
headers: { origin: 'other.io' }
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 200)
t.equal(res.payload, 'ok')
t.match(res.headers, {
'access-control-allow-origin': 'other.io',
vary: 'Origin'
})
})
fastify.inject({
method: 'GET',
url: '/',
headers: { origin: 'foo.com' }
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 200)
t.equal(res.payload, 'ok')
t.match(res.headers, {
vary: 'Origin'
})
t.equal(res.headers['access-control-allow-origin'], undefined)
})
})
test('Allow only request from a specific origin using regex', t => {
t.plan(8)
const fastify = Fastify()
fastify.register(cors, { origin: /(?:example|other)\.com/giu })
fastify.get('/', (req, reply) => {
reply.send('ok')
})
// .test was previously used, which caused 2 consecutive requests to return
// different results with global (e.g. /g) regexes. Therefore, check this
// twice to check consistency
for (let i = 0; i < 2; i++) {
fastify.inject({
method: 'GET',
url: '/',
headers: { origin: 'https://www.example.com/' }
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 200)
t.equal(res.payload, 'ok')
t.match(res.headers, {
'access-control-allow-origin': 'https://www.example.com/',
vary: 'Origin'
})
})
}
})
test('Disable preflight', t => {
t.plan(7)
const fastify = Fastify()
fastify.register(cors, { preflight: false })
fastify.get('/', (req, reply) => {
reply.send('ok')
})
fastify.inject({
method: 'OPTIONS',
url: '/hello'
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 404)
t.match(res.headers, {
'access-control-allow-origin': '*'
})
})
fastify.inject({
method: 'GET',
url: '/'
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 200)
t.equal(res.payload, 'ok')
t.match(res.headers, {
'access-control-allow-origin': '*'
})
})
})
test('Should always add vary header to `Origin` for reflected origin', t => {
t.plan(12)
const fastify = Fastify()
fastify.register(cors, { origin: true })
fastify.get('/', (req, reply) => {
reply.send('ok')
})
// Invalid Preflight
fastify.inject({
method: 'OPTIONS',
url: '/'
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 400)
t.equal(res.payload, 'Invalid Preflight Request')
t.match(res.headers, {
vary: 'Origin'
})
})
// Valid Preflight
fastify.inject({
method: 'OPTIONS',
url: '/',
headers: {
'access-control-request-method': 'GET',
origin: 'example.com'
}
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 204)
t.equal(res.payload, '')
t.match(res.headers, {
vary: 'Origin'
})
})
// Other Route
fastify.inject({
method: 'GET',
url: '/'
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 200)
t.equal(res.payload, 'ok')
t.match(res.headers, {
vary: 'Origin'
})
})
})
test('Should always add vary header to `Origin` for reflected origin (vary is array)', t => {
t.plan(4)
const fastify = Fastify()
// Mock getHeader function
fastify.decorateReply('getHeader', (name) => ['foo', 'bar'])
fastify.register(cors, { origin: true })
fastify.get('/', (req, reply) => {
reply.send('ok')
})
fastify.inject({
method: 'GET',
url: '/'
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 200)
t.equal(res.payload, 'ok')
t.match(res.headers, {
vary: 'foo, bar, Origin'
})
})
})
test('Allow only request from with specific headers', t => {
t.plan(8)
const fastify = Fastify()
fastify.register(cors, {
allowedHeaders: 'foo',
exposedHeaders: 'bar'
})
fastify.get('/', (req, reply) => {
reply.send('ok')
})
fastify.inject({
method: 'OPTIONS',
url: '/',
headers: {
'access-control-request-method': 'GET',
origin: 'example.com'
}
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 204)
t.match(res.headers, {
'access-control-allow-headers': 'foo'
})
t.notMatch(res.headers, { vary: 'Origin' })
})
fastify.inject({
method: 'GET',
url: '/'
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 200)
t.equal(res.payload, 'ok')
t.match(res.headers, {
'access-control-expose-headers': 'bar'
})
})
})
test('Should support wildcard config /1', t => {
t.plan(4)
const fastify = Fastify()
fastify.register(cors, { origin: '*' })
fastify.get('/', (req, reply) => {
reply.send('ok')
})
fastify.inject({
method: 'GET',
url: '/'
}, (err, res) => {
t.error(err)
t.equal(res.statusCode, 200)
t.equal(res.payload, 'ok')
t.equal(res.headers['access-control-allow-origin'], '*')
})
})
test('Should support wildcard config /2', t => {
t.plan(4)
const fastify = Fastify()
fastify.register(cors, { origin: ['*'] })
fastify.get('/', (req, reply) => {
reply.send('ok')
})
fastify.inject({
method: 'GET',
url: '/'
}, (err, res) => {
t.error(err)
t.equal(res.statusCode, 200)
t.equal(res.payload, 'ok')
t.equal(res.headers['access-control-allow-origin'], '*')
})
})

706
backend/node_modules/@fastify/cors/test/hooks.test.js generated vendored Normal file
View File

@@ -0,0 +1,706 @@
'use strict'
const { test } = require('tap')
const Fastify = require('fastify')
const kFastifyContext = require('fastify/lib/symbols').kRouteContext
const cors = require('..')
test('Should error on invalid hook option', async (t) => {
t.plan(1)
const fastify = Fastify()
t.rejects(fastify.register(cors, { hook: 'invalid' }), new TypeError('@fastify/cors: Invalid hook option provided.'))
})
test('Should set hook onRequest if hook option is not set', async (t) => {
t.plan(10)
const fastify = Fastify()
fastify.register(cors)
fastify.addHook('onResponse', (request, reply, done) => {
t.equal(request[kFastifyContext].onError, null)
t.equal(request[kFastifyContext].onRequest.length, 1)
t.equal(request[kFastifyContext].onSend, null)
t.equal(request[kFastifyContext].preHandler, null)
t.equal(request[kFastifyContext].preParsing, null)
t.equal(request[kFastifyContext].preSerialization, null)
t.equal(request[kFastifyContext].preValidation, null)
done()
})
fastify.get('/', (req, reply) => {
reply.send('ok')
})
await fastify.ready()
const res = await fastify.inject({
method: 'GET',
url: '/'
})
delete res.headers.date
t.equal(res.statusCode, 200)
t.equal(res.payload, 'ok')
t.match(res.headers, {
'access-control-allow-origin': '*'
})
})
test('Should set hook onRequest if hook option is set to onRequest', async (t) => {
t.plan(10)
const fastify = Fastify()
fastify.register(cors, {
hook: 'onRequest'
})
fastify.addHook('onResponse', (request, reply, done) => {
t.equal(request[kFastifyContext].onError, null)
t.equal(request[kFastifyContext].onRequest.length, 1)
t.equal(request[kFastifyContext].onSend, null)
t.equal(request[kFastifyContext].preHandler, null)
t.equal(request[kFastifyContext].preParsing, null)
t.equal(request[kFastifyContext].preSerialization, null)
t.equal(request[kFastifyContext].preValidation, null)
done()
})
fastify.get('/', (req, reply) => {
reply.send('ok')
})
await fastify.ready()
const res = await fastify.inject({
method: 'GET',
url: '/'
})
delete res.headers.date
t.equal(res.statusCode, 200)
t.equal(res.payload, 'ok')
t.match(res.headers, {
'access-control-allow-origin': '*'
})
})
test('Should set hook preParsing if hook option is set to preParsing', async (t) => {
t.plan(11)
const fastify = Fastify()
fastify.register(cors, {
hook: 'preParsing'
})
fastify.addHook('onResponse', (request, reply, done) => {
t.equal(request[kFastifyContext].onError, null)
t.equal(request[kFastifyContext].onRequest, null)
t.equal(request[kFastifyContext].onSend, null)
t.equal(request[kFastifyContext].preHandler, null)
t.equal(request[kFastifyContext].preParsing.length, 1)
t.equal(request[kFastifyContext].preSerialization, null)
t.equal(request[kFastifyContext].preValidation, null)
done()
})
fastify.get('/', (req, reply) => {
reply.send('ok')
})
await fastify.ready()
const res = await fastify.inject({
method: 'GET',
url: '/'
})
delete res.headers.date
t.equal(res.statusCode, 200)
t.equal(res.payload, 'ok')
t.match(res.headers, {
'access-control-allow-origin': '*'
})
t.notMatch(res.headers, { vary: 'Origin' })
})
test('Should set hook preValidation if hook option is set to preValidation', async (t) => {
t.plan(11)
const fastify = Fastify()
fastify.register(cors, {
hook: 'preValidation'
})
fastify.addHook('onResponse', (request, reply, done) => {
t.equal(request[kFastifyContext].onError, null)
t.equal(request[kFastifyContext].onRequest, null)
t.equal(request[kFastifyContext].onSend, null)
t.equal(request[kFastifyContext].preHandler, null)
t.equal(request[kFastifyContext].preParsing, null)
t.equal(request[kFastifyContext].preSerialization, null)
t.equal(request[kFastifyContext].preValidation.length, 1)
done()
})
fastify.get('/', (req, reply) => {
reply.send('ok')
})
await fastify.ready()
const res = await fastify.inject({
method: 'GET',
url: '/'
})
delete res.headers.date
t.equal(res.statusCode, 200)
t.equal(res.payload, 'ok')
t.match(res.headers, {
'access-control-allow-origin': '*'
})
t.notMatch(res.headers, { vary: 'Origin' })
})
test('Should set hook preParsing if hook option is set to preParsing', async (t) => {
t.plan(11)
const fastify = Fastify()
fastify.register(cors, {
hook: 'preParsing'
})
fastify.addHook('onResponse', (request, reply, done) => {
t.equal(request[kFastifyContext].onError, null)
t.equal(request[kFastifyContext].onRequest, null)
t.equal(request[kFastifyContext].onSend, null)
t.equal(request[kFastifyContext].preHandler, null)
t.equal(request[kFastifyContext].preParsing.length, 1)
t.equal(request[kFastifyContext].preSerialization, null)
t.equal(request[kFastifyContext].preValidation, null)
done()
})
fastify.get('/', (req, reply) => {
reply.send('ok')
})
await fastify.ready()
const res = await fastify.inject({
method: 'GET',
url: '/'
})
delete res.headers.date
t.equal(res.statusCode, 200)
t.equal(res.payload, 'ok')
t.match(res.headers, {
'access-control-allow-origin': '*'
})
t.notMatch(res.headers, { vary: 'Origin' })
})
test('Should set hook preHandler if hook option is set to preHandler', async (t) => {
t.plan(11)
const fastify = Fastify()
fastify.register(cors, {
hook: 'preHandler'
})
fastify.addHook('onResponse', (request, reply, done) => {
t.equal(request[kFastifyContext].onError, null)
t.equal(request[kFastifyContext].onRequest, null)
t.equal(request[kFastifyContext].onSend, null)
t.equal(request[kFastifyContext].preHandler.length, 1)
t.equal(request[kFastifyContext].preParsing, null)
t.equal(request[kFastifyContext].preSerialization, null)
t.equal(request[kFastifyContext].preValidation, null)
done()
})
fastify.get('/', (req, reply) => {
reply.send('ok')
})
await fastify.ready()
const res = await fastify.inject({
method: 'GET',
url: '/'
})
delete res.headers.date
t.equal(res.statusCode, 200)
t.equal(res.payload, 'ok')
t.match(res.headers, {
'access-control-allow-origin': '*'
})
t.notMatch(res.headers, { vary: 'Origin' })
})
test('Should set hook onSend if hook option is set to onSend', async (t) => {
t.plan(11)
const fastify = Fastify()
fastify.register(cors, {
hook: 'onSend'
})
fastify.addHook('onResponse', (request, reply, done) => {
t.equal(request[kFastifyContext].onError, null)
t.equal(request[kFastifyContext].onRequest, null)
t.equal(request[kFastifyContext].onSend.length, 1)
t.equal(request[kFastifyContext].preHandler, null)
t.equal(request[kFastifyContext].preParsing, null)
t.equal(request[kFastifyContext].preSerialization, null)
t.equal(request[kFastifyContext].preValidation, null)
done()
})
fastify.get('/', (req, reply) => {
reply.send('ok')
})
await fastify.ready()
const res = await fastify.inject({
method: 'GET',
url: '/'
})
delete res.headers.date
t.equal(res.statusCode, 200)
t.equal(res.payload, 'ok')
t.match(res.headers, {
'access-control-allow-origin': '*'
})
t.notMatch(res.headers, { vary: 'Origin' })
})
test('Should set hook preSerialization if hook option is set to preSerialization', async (t) => {
t.plan(11)
const fastify = Fastify()
fastify.register(cors, {
hook: 'preSerialization'
})
fastify.addHook('onResponse', (request, reply, done) => {
t.equal(request[kFastifyContext].onError, null)
t.equal(request[kFastifyContext].onRequest, null)
t.equal(request[kFastifyContext].onSend, null)
t.equal(request[kFastifyContext].preHandler, null)
t.equal(request[kFastifyContext].preParsing, null)
t.equal(request[kFastifyContext].preSerialization.length, 1)
t.equal(request[kFastifyContext].preValidation, null)
done()
})
fastify.get('/', (req, reply) => {
reply.send({ nonString: true })
})
await fastify.ready()
const res = await fastify.inject({
method: 'GET',
url: '/'
})
delete res.headers.date
t.equal(res.statusCode, 200)
t.equal(res.payload, '{"nonString":true}')
t.match(res.headers, {
'access-control-allow-origin': '*'
})
t.notMatch(res.headers, { vary: 'Origin' })
})
test('Should support custom hook with dynamic config', t => {
t.plan(16)
const configs = [{
origin: 'example.com',
methods: 'GET',
credentials: true,
exposedHeaders: ['foo', 'bar'],
allowedHeaders: ['baz', 'woo'],
maxAge: 123
}, {
origin: 'sample.com',
methods: 'GET',
credentials: true,
exposedHeaders: ['zoo', 'bar'],
allowedHeaders: ['baz', 'foo'],
maxAge: 321
}]
const fastify = Fastify()
let requestId = 0
const configDelegation = function (req, cb) {
// request should have id
t.ok(req.id)
// request should not have send
t.notOk(req.send)
const config = configs[requestId]
requestId++
if (config) {
cb(null, config)
} else {
cb(new Error('ouch'))
}
}
fastify.register(cors, {
hook: 'preHandler',
delegator: configDelegation
})
fastify.get('/', (req, reply) => {
reply.send('ok')
})
fastify.inject({
method: 'GET',
url: '/'
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 200)
t.equal(res.payload, 'ok')
t.match(res.headers, {
'access-control-allow-origin': 'example.com',
'access-control-allow-credentials': 'true',
'access-control-expose-headers': 'foo, bar',
'content-length': '2',
vary: 'Origin'
})
})
fastify.inject({
method: 'OPTIONS',
url: '/',
headers: {
'access-control-request-method': 'GET',
origin: 'example.com'
}
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 204)
t.equal(res.payload, '')
t.match(res.headers, {
'access-control-allow-origin': 'sample.com',
'access-control-allow-credentials': 'true',
'access-control-expose-headers': 'zoo, bar',
'access-control-allow-methods': 'GET',
'access-control-allow-headers': 'baz, foo',
'access-control-max-age': '321',
'content-length': '0',
vary: 'Origin'
})
})
fastify.inject({
method: 'GET',
url: '/',
headers: {
'access-control-request-method': 'GET',
origin: 'example.com'
}
}, (err, res) => {
t.error(err)
t.equal(res.statusCode, 500)
})
})
test('Should support custom hook with dynamic config (callback)', t => {
t.plan(16)
const configs = [{
origin: 'example.com',
methods: 'GET',
credentials: true,
exposedHeaders: ['foo', 'bar'],
allowedHeaders: ['baz', 'woo'],
maxAge: 123
}, {
origin: 'sample.com',
methods: 'GET',
credentials: true,
exposedHeaders: ['zoo', 'bar'],
allowedHeaders: ['baz', 'foo'],
maxAge: 321
}]
const fastify = Fastify()
let requestId = 0
const configDelegation = function (req, cb) {
// request should have id
t.ok(req.id)
// request should not have send
t.notOk(req.send)
const config = configs[requestId]
requestId++
if (config) {
cb(null, config)
} else {
cb(new Error('ouch'))
}
}
fastify.register(cors, {
hook: 'preParsing',
delegator: configDelegation
})
fastify.get('/', (req, reply) => {
reply.send('ok')
})
fastify.inject({
method: 'GET',
url: '/'
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 200)
t.equal(res.payload, 'ok')
t.match(res.headers, {
'access-control-allow-origin': 'example.com',
'access-control-allow-credentials': 'true',
'access-control-expose-headers': 'foo, bar',
'content-length': '2',
vary: 'Origin'
})
})
fastify.inject({
method: 'OPTIONS',
url: '/',
headers: {
'access-control-request-method': 'GET',
origin: 'example.com'
}
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 204)
t.equal(res.payload, '')
t.match(res.headers, {
'access-control-allow-origin': 'sample.com',
'access-control-allow-credentials': 'true',
'access-control-expose-headers': 'zoo, bar',
'access-control-allow-methods': 'GET',
'access-control-allow-headers': 'baz, foo',
'access-control-max-age': '321',
'content-length': '0',
vary: 'Origin'
})
})
fastify.inject({
method: 'GET',
url: '/',
headers: {
'access-control-request-method': 'GET',
origin: 'example.com'
}
}, (err, res) => {
t.error(err)
t.equal(res.statusCode, 500)
})
})
test('Should support custom hook with dynamic config (Promise)', t => {
t.plan(16)
const configs = [{
origin: 'example.com',
methods: 'GET',
credentials: true,
exposedHeaders: ['foo', 'bar'],
allowedHeaders: ['baz', 'woo'],
maxAge: 123
}, {
origin: 'sample.com',
methods: 'GET',
credentials: true,
exposedHeaders: ['zoo', 'bar'],
allowedHeaders: ['baz', 'foo'],
maxAge: 321
}]
const fastify = Fastify()
let requestId = 0
const configDelegation = function (req) {
// request should have id
t.ok(req.id)
// request should not have send
t.notOk(req.send)
const config = configs[requestId]
requestId++
if (config) {
return Promise.resolve(config)
} else {
return Promise.reject(new Error('ouch'))
}
}
fastify.register(cors, {
hook: 'preParsing',
delegator: configDelegation
})
fastify.get('/', (req, reply) => {
reply.send('ok')
})
fastify.inject({
method: 'GET',
url: '/'
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 200)
t.equal(res.payload, 'ok')
t.match(res.headers, {
'access-control-allow-origin': 'example.com',
'access-control-allow-credentials': 'true',
'access-control-expose-headers': 'foo, bar',
'content-length': '2',
vary: 'Origin'
})
})
fastify.inject({
method: 'OPTIONS',
url: '/',
headers: {
'access-control-request-method': 'GET',
origin: 'example.com'
}
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 204)
t.equal(res.payload, '')
t.match(res.headers, {
'access-control-allow-origin': 'sample.com',
'access-control-allow-credentials': 'true',
'access-control-expose-headers': 'zoo, bar',
'access-control-allow-methods': 'GET',
'access-control-allow-headers': 'baz, foo',
'access-control-max-age': '321',
'content-length': '0',
vary: 'Origin'
})
})
fastify.inject({
method: 'GET',
url: '/',
headers: {
'access-control-request-method': 'GET',
origin: 'example.com'
}
}, (err, res) => {
t.error(err)
t.equal(res.statusCode, 500)
})
})
test('Should support custom hook with dynamic config (Promise), but should error /1', t => {
t.plan(6)
const fastify = Fastify()
const configDelegation = function () {
return false
}
fastify.register(cors, {
hook: 'preParsing',
delegator: configDelegation
})
fastify.get('/', (req, reply) => {
reply.send('ok')
})
fastify.inject({
method: 'OPTIONS',
url: '/',
headers: {
'access-control-request-method': 'GET',
origin: 'example.com'
}
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 500)
t.equal(res.payload, '{"statusCode":500,"error":"Internal Server Error","message":"Invalid CORS origin option"}')
t.match(res.headers, {
'content-length': '89'
})
})
fastify.inject({
method: 'GET',
url: '/',
headers: {
'access-control-request-method': 'GET',
origin: 'example.com'
}
}, (err, res) => {
t.error(err)
t.equal(res.statusCode, 500)
})
})
test('Should support custom hook with dynamic config (Promise), but should error /2', t => {
t.plan(6)
const fastify = Fastify()
const configDelegation = function () {
return false
}
fastify.register(cors, {
delegator: configDelegation
})
fastify.get('/', (req, reply) => {
reply.send('ok')
})
fastify.inject({
method: 'OPTIONS',
url: '/',
headers: {
'access-control-request-method': 'GET',
origin: 'example.com'
}
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 500)
t.equal(res.payload, '{"statusCode":500,"error":"Internal Server Error","message":"Invalid CORS origin option"}')
t.match(res.headers, {
'content-length': '89'
})
})
fastify.inject({
method: 'GET',
url: '/',
headers: {
'access-control-request-method': 'GET',
origin: 'example.com'
}
}, (err, res) => {
t.error(err)
t.equal(res.statusCode, 500)
})
})

View File

@@ -0,0 +1,435 @@
'use strict'
const { test } = require('tap')
const Fastify = require('fastify')
const cors = require('../')
test('Should reply to preflight requests', t => {
t.plan(4)
const fastify = Fastify()
fastify.register(cors)
fastify.inject({
method: 'OPTIONS',
url: '/',
headers: {
'access-control-request-method': 'GET',
origin: 'example.com'
}
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 204)
t.equal(res.payload, '')
t.match(res.headers, {
'access-control-allow-origin': '*',
'access-control-allow-methods': 'GET,HEAD,PUT,PATCH,POST,DELETE',
vary: 'Access-Control-Request-Headers',
'content-length': '0'
})
})
})
test('Should add access-control-allow-headers to response if preflight req has access-control-request-headers', t => {
t.plan(4)
const fastify = Fastify()
fastify.register(cors)
fastify.inject({
method: 'OPTIONS',
url: '/',
headers: {
'access-control-request-headers': 'x-requested-with',
'access-control-request-method': 'GET',
origin: 'example.com'
}
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 204)
t.equal(res.payload, '')
t.match(res.headers, {
'access-control-allow-origin': '*',
'access-control-allow-methods': 'GET,HEAD,PUT,PATCH,POST,DELETE',
'access-control-allow-headers': 'x-requested-with',
vary: 'Access-Control-Request-Headers',
'content-length': '0'
})
})
})
test('Should reply to preflight requests with custom status code', t => {
t.plan(4)
const fastify = Fastify()
fastify.register(cors, { optionsSuccessStatus: 200 })
fastify.inject({
method: 'OPTIONS',
url: '/',
headers: {
'access-control-request-method': 'GET',
origin: 'example.com'
}
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 200)
t.equal(res.payload, '')
t.match(res.headers, {
'access-control-allow-origin': '*',
'access-control-allow-methods': 'GET,HEAD,PUT,PATCH,POST,DELETE',
vary: 'Access-Control-Request-Headers',
'content-length': '0'
})
})
})
test('Should be able to override preflight response with a route', t => {
t.plan(5)
const fastify = Fastify()
fastify.register(cors, { preflightContinue: true })
fastify.options('/', (req, reply) => {
reply.send('ok')
})
fastify.inject({
method: 'OPTIONS',
url: '/',
headers: {
'access-control-request-method': 'GET',
origin: 'example.com'
}
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 200)
t.equal(res.payload, 'ok')
t.match(res.headers, {
// Only the base cors headers and no preflight headers
'access-control-allow-origin': '*'
})
t.notMatch(res.headers, { vary: 'Origin' })
})
})
test('Should reply to all options requests', t => {
t.plan(4)
const fastify = Fastify()
fastify.register(cors)
fastify.inject({
method: 'OPTIONS',
url: '/hello',
headers: {
'access-control-request-method': 'GET',
origin: 'example.com'
}
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 204)
t.equal(res.payload, '')
t.match(res.headers, {
'access-control-allow-origin': '*',
'access-control-allow-methods': 'GET,HEAD,PUT,PATCH,POST,DELETE',
vary: 'Access-Control-Request-Headers',
'content-length': '0'
})
})
})
test('Should support a prefix for preflight requests', t => {
t.plan(6)
const fastify = Fastify()
fastify.register((instance, opts, next) => {
instance.register(cors)
next()
}, { prefix: '/subsystem' })
fastify.inject({
method: 'OPTIONS',
url: '/hello'
}, (err, res) => {
t.error(err)
t.equal(res.statusCode, 404)
})
fastify.inject({
method: 'OPTIONS',
url: '/subsystem/hello',
headers: {
'access-control-request-method': 'GET',
origin: 'example.com'
}
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 204)
t.equal(res.payload, '')
t.match(res.headers, {
'access-control-allow-origin': '*',
'access-control-allow-methods': 'GET,HEAD,PUT,PATCH,POST,DELETE',
vary: 'Access-Control-Request-Headers',
'content-length': '0'
})
})
})
test('hide options route by default', t => {
t.plan(2)
const fastify = Fastify()
fastify.addHook('onRoute', (route) => {
if (route.method === 'OPTIONS' && route.url === '*') {
t.equal(route.schema.hide, true)
}
})
fastify.register(cors)
fastify.ready(err => {
t.error(err)
})
})
test('show options route', t => {
t.plan(2)
const fastify = Fastify()
fastify.addHook('onRoute', (route) => {
if (route.method === 'OPTIONS' && route.url === '*') {
t.equal(route.schema.hide, false)
}
})
fastify.register(cors, { hideOptionsRoute: false })
fastify.ready(err => {
t.error(err)
})
})
test('Allow only request from with specific methods', t => {
t.plan(4)
const fastify = Fastify()
fastify.register(cors, { methods: ['GET', 'POST'] })
fastify.inject({
method: 'OPTIONS',
url: '/',
headers: {
'access-control-request-method': 'GET',
origin: 'example.com'
}
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 204)
t.match(res.headers, {
'access-control-allow-methods': 'GET, POST'
})
t.notMatch(res.headers, { vary: 'Origin' })
})
})
test('Should reply with 400 error to OPTIONS requests missing origin header when default strictPreflight', t => {
t.plan(3)
const fastify = Fastify()
fastify.register(cors)
fastify.inject({
method: 'OPTIONS',
url: '/',
headers: {
'access-control-request-method': 'GET'
}
}, (err, res) => {
t.error(err)
t.equal(res.statusCode, 400)
t.equal(res.payload, 'Invalid Preflight Request')
})
})
test('Should reply with 400 to OPTIONS requests when missing Access-Control-Request-Method header when default strictPreflight', t => {
t.plan(3)
const fastify = Fastify()
fastify.register(cors, {
strictPreflight: true
})
fastify.inject({
method: 'OPTIONS',
url: '/',
headers: {
origin: 'example.com'
}
}, (err, res) => {
t.error(err)
t.equal(res.statusCode, 400)
t.equal(res.payload, 'Invalid Preflight Request')
})
})
test('Should reply to all preflight requests when strictPreflight is disabled', t => {
t.plan(4)
const fastify = Fastify()
fastify.register(cors, { strictPreflight: false })
fastify.inject({
method: 'OPTIONS',
url: '/'
// No access-control-request-method or origin headers
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 204)
t.equal(res.payload, '')
t.match(res.headers, {
'access-control-allow-origin': '*',
'access-control-allow-methods': 'GET,HEAD,PUT,PATCH,POST,DELETE',
vary: 'Access-Control-Request-Headers',
'content-length': '0'
})
})
})
test('Default empty 200 response with preflightContinue on OPTIONS routes', t => {
t.plan(4)
const fastify = Fastify()
fastify.register(cors, { preflightContinue: true })
fastify.inject({
method: 'OPTIONS',
url: '/doesnotexist',
headers: {
'access-control-request-method': 'GET',
origin: 'example.com'
}
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 200)
t.equal(res.payload, '')
t.match(res.headers, {
'access-control-allow-origin': '*',
'access-control-allow-methods': 'GET,HEAD,PUT,PATCH,POST,DELETE',
vary: 'Access-Control-Request-Headers'
})
})
})
test('Can override preflight response with preflightContinue', t => {
t.plan(4)
const fastify = Fastify()
fastify.register(cors, { preflightContinue: true })
fastify.options('/', (req, reply) => {
reply.send('ok')
})
fastify.inject({
method: 'OPTIONS',
url: '/',
headers: {
'access-control-request-method': 'GET',
origin: 'example.com'
}
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 200)
t.equal(res.payload, 'ok')
t.match(res.headers, {
'access-control-allow-origin': '*',
'access-control-allow-methods': 'GET,HEAD,PUT,PATCH,POST,DELETE',
vary: 'Access-Control-Request-Headers'
})
})
})
test('Should support ongoing prefix ', t => {
t.plan(12)
const fastify = Fastify()
fastify.register(async (instance) => {
instance.register(cors)
}, { prefix: '/prefix' })
// support prefixed route
fastify.inject({
method: 'OPTIONS',
url: '/prefix',
headers: {
'access-control-request-method': 'GET',
origin: 'example.com'
}
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 204)
t.equal(res.payload, '')
t.match(res.headers, {
'access-control-allow-origin': '*',
'access-control-allow-methods': 'GET,HEAD,PUT,PATCH,POST,DELETE',
vary: 'Access-Control-Request-Headers',
'content-length': '0'
})
})
// support prefixed route without / continue
fastify.inject({
method: 'OPTIONS',
url: '/prefixfoo',
headers: {
'access-control-request-method': 'GET',
origin: 'example.com'
}
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 204)
t.equal(res.payload, '')
t.match(res.headers, {
'access-control-allow-origin': '*',
'access-control-allow-methods': 'GET,HEAD,PUT,PATCH,POST,DELETE',
vary: 'Access-Control-Request-Headers',
'content-length': '0'
})
})
// support prefixed route with / continue
fastify.inject({
method: 'OPTIONS',
url: '/prefix/foo',
headers: {
'access-control-request-method': 'GET',
origin: 'example.com'
}
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 204)
t.equal(res.payload, '')
t.match(res.headers, {
'access-control-allow-origin': '*',
'access-control-allow-methods': 'GET,HEAD,PUT,PATCH,POST,DELETE',
vary: 'Access-Control-Request-Headers',
'content-length': '0'
})
})
})

229
backend/node_modules/@fastify/cors/test/vary.test.js generated vendored Normal file
View File

@@ -0,0 +1,229 @@
'use strict'
const test = require('tap').test
const createAddFieldnameToVary = require('../vary').createAddFieldnameToVary
const parse = require('../vary').parse
test('Should set * even if we set a specific field', t => {
t.plan(1)
const addOriginToVary = createAddFieldnameToVary('Origin')
const replyMock = {
getHeader (name) {
return '*'
},
header (name, value) {
t.fail('Should not be here')
}
}
addOriginToVary(replyMock)
t.pass()
})
test('Should set * even if we set a specific field', t => {
t.plan(2)
const addWildcardToVary = createAddFieldnameToVary('*')
const replyMock = {
getHeader (name) {
return 'Origin'
},
header (name, value) {
t.same(name, 'Vary')
t.same(value, '*')
}
}
addWildcardToVary(replyMock)
})
test('Should set * when field contains a *', t => {
t.plan(3)
const addOriginToVary = createAddFieldnameToVary('Origin')
const replyMock = {
getHeader (name) {
return ['Origin', '*', 'Access-Control-Request-Headers']
},
header (name, value) {
t.same(name, 'Vary')
t.same(value, '*')
}
}
addOriginToVary(replyMock)
t.pass()
})
test('Should concat vary values', t => {
t.plan(3)
const addOriginToVary = createAddFieldnameToVary('Origin')
const replyMock = {
getHeader (name) {
return 'Access-Control-Request-Headers'
},
header (name, value) {
t.same(name, 'Vary')
t.same(value, 'Access-Control-Request-Headers, Origin')
}
}
addOriginToVary(replyMock)
t.pass()
})
test('Should concat vary values ignoring consecutive commas', t => {
t.plan(3)
const addOriginToVary = createAddFieldnameToVary('Origin')
const replyMock = {
getHeader (name) {
return ' Access-Control-Request-Headers,Access-Control-Request-Method'
},
header (name, value) {
t.same(name, 'Vary')
t.same(value, ' Access-Control-Request-Headers,Access-Control-Request-Method, Origin')
}
}
addOriginToVary(replyMock)
t.pass()
})
test('Should concat vary values ignoring whitespace', t => {
t.plan(3)
const addOriginToVary = createAddFieldnameToVary('Origin')
const replyMock = {
getHeader (name) {
return ' Access-Control-Request-Headers ,Access-Control-Request-Method'
},
header (name, value) {
t.same(name, 'Vary')
t.same(value, ' Access-Control-Request-Headers ,Access-Control-Request-Method, Origin')
}
}
addOriginToVary(replyMock)
t.pass()
})
test('Should set the field as value for vary if no vary is defined', t => {
t.plan(2)
const addOriginToVary = createAddFieldnameToVary('Origin')
const replyMock = {
getHeader (name) {
return undefined
},
header (name, value) {
t.same(name, 'Vary')
t.same(value, 'Origin')
}
}
addOriginToVary(replyMock)
})
test('Should set * as value for vary if vary contains *', t => {
t.plan(2)
const addOriginToVary = createAddFieldnameToVary('Origin')
const replyMock = {
getHeader (name) {
return 'Accept,*'
},
header (name, value) {
t.same(name, 'Vary')
t.same(value, '*')
}
}
addOriginToVary(replyMock)
})
test('Should set Accept-Encoding as value for vary if vary is empty string', t => {
t.plan(2)
const addAcceptEncodingToVary = createAddFieldnameToVary('Accept-Encoding')
const replyMock = {
getHeader (name) {
return ''
},
header (name, value) {
t.same(name, 'Vary')
t.same(value, 'Accept-Encoding')
}
}
addAcceptEncodingToVary(replyMock)
})
test('Should have no issues with values containing dashes', t => {
t.plan(2)
const addXFooToVary = createAddFieldnameToVary('X-Foo')
const replyMock = {
value: 'Accept-Encoding',
getHeader (name) {
return this.value
},
header (name, value) {
t.same(name, 'Vary')
t.same(value, 'Accept-Encoding, X-Foo')
this.value = value
}
}
addXFooToVary(replyMock)
addXFooToVary(replyMock)
})
test('Should ignore the header as value for vary if it is already in vary', t => {
t.plan(1)
const addOriginToVary = createAddFieldnameToVary('Origin')
const replyMock = {
getHeader (name) {
return 'Origin'
},
header (name, value) {
t.fail('Should not be here')
}
}
addOriginToVary(replyMock)
addOriginToVary(replyMock)
t.pass()
})
test('parse', t => {
t.plan(18)
t.same(parse(''), [])
t.same(parse('a'), ['a'])
t.same(parse('a,b'), ['a', 'b'])
t.same(parse(' a,b'), ['a', 'b'])
t.same(parse('a,b '), ['a', 'b'])
t.same(parse('a,b,c'), ['a', 'b', 'c'])
t.same(parse('A,b,c'), ['a', 'b', 'c'])
t.same(parse('a,b,c,'), ['a', 'b', 'c'])
t.same(parse('a,b,c, '), ['a', 'b', 'c'])
t.same(parse(',a,b,c'), ['a', 'b', 'c'])
t.same(parse(' ,a,b,c'), ['a', 'b', 'c'])
t.same(parse('a,,b,c'), ['a', 'b', 'c'])
t.same(parse('a,,,b,,c'), ['a', 'b', 'c'])
t.same(parse('a, b,c'), ['a', 'b', 'c'])
t.same(parse('a, b,c'), ['a', 'b', 'c'])
t.same(parse('a, , b,c'), ['a', 'b', 'c'])
t.same(parse('a, , b,c'), ['a', 'b', 'c'])
// one for the cache
t.same(parse('A,b,c'), ['a', 'b', 'c'])
})
test('createAddFieldnameToVary', t => {
t.plan(2)
t.same(typeof createAddFieldnameToVary('valid-header'), 'function')
t.throws(() => createAddFieldnameToVary('invalid:header'), TypeError, 'Field contains invalid characters.')
})

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

@@ -0,0 +1,116 @@
/// <reference types="node" />
import { FastifyInstance, FastifyPluginCallback, FastifyRequest } from 'fastify';
type OriginCallback = (err: Error | null, origin: ValueOrArray<OriginType>) => void;
type OriginType = string | boolean | RegExp;
type ValueOrArray<T> = T | ArrayOfValueOrArray<T>;
interface ArrayOfValueOrArray<T> extends Array<ValueOrArray<T>> {
}
type FastifyCorsPlugin = FastifyPluginCallback<
NonNullable<fastifyCors.FastifyCorsOptions> | fastifyCors.FastifyCorsOptionsDelegate
>;
type FastifyCorsHook =
| 'onRequest'
| 'preParsing'
| 'preValidation'
| 'preHandler'
| 'preSerialization'
| 'onSend'
declare namespace fastifyCors {
export type OriginFunction = (origin: string | undefined, callback: OriginCallback) => void;
export interface FastifyCorsOptions {
/**
* Configures the Lifecycle Hook.
*/
hook?: FastifyCorsHook;
/**
* Configures the delegate function.
*/
delegator?: FastifyCorsOptionsDelegate;
/**
* Configures the Access-Control-Allow-Origin CORS header.
*/
origin?: ValueOrArray<OriginType> | fastifyCors.OriginFunction;
/**
* Configures the Access-Control-Allow-Credentials CORS header.
* Set to true to pass the header, otherwise it is omitted.
*/
credentials?: boolean;
/**
* Configures the Access-Control-Expose-Headers CORS header.
* Expects a comma-delimited string (ex: 'Content-Range,X-Content-Range')
* or an array (ex: ['Content-Range', 'X-Content-Range']).
* If not specified, no custom headers are exposed.
*/
exposedHeaders?: string | string[];
/**
* Configures the Access-Control-Allow-Headers CORS header.
* Expects a comma-delimited string (ex: 'Content-Type,Authorization')
* or an array (ex: ['Content-Type', 'Authorization']). If not
* specified, defaults to reflecting the headers specified in the
* request's Access-Control-Request-Headers header.
*/
allowedHeaders?: string | string[];
/**
* Configures the Access-Control-Allow-Methods CORS header.
* Expects a comma-delimited string (ex: 'GET,PUT,POST') or an array (ex: ['GET', 'PUT', 'POST']).
*/
methods?: string | string[];
/**
* Configures the Access-Control-Max-Age CORS header.
* Set to an integer to pass the header, otherwise it is omitted.
*/
maxAge?: number;
/**
* Configures the Cache-Control header for CORS preflight responses.
* Set to an integer to pass the header as `Cache-Control: max-age=${cacheControl}`,
* or set to a string to pass the header as `Cache-Control: ${cacheControl}` (fully define
* the header value), otherwise the header is omitted.
*/
cacheControl?: number | string;
/**
* Pass the CORS preflight response to the route handler (default: false).
*/
preflightContinue?: boolean;
/**
* Provides a status code to use for successful OPTIONS requests,
* since some legacy browsers (IE11, various SmartTVs) choke on 204.
*/
optionsSuccessStatus?: number;
/**
* Pass the CORS preflight response to the route handler (default: false).
*/
preflight?: boolean;
/**
* Enforces strict requirement of the CORS preflight request headers (Access-Control-Request-Method and Origin).
* Preflight requests without the required headers will result in 400 errors when set to `true` (default: `true`).
*/
strictPreflight?: boolean;
/**
* Hide options route from the documentation built using fastify-swagger (default: true).
*/
hideOptionsRoute?: boolean;
}
export interface FastifyCorsOptionsDelegateCallback { (req: FastifyRequest, cb: (error: Error | null, corsOptions?: FastifyCorsOptions) => void): void }
export interface FastifyCorsOptionsDelegatePromise { (req: FastifyRequest): Promise<FastifyCorsOptions> }
export type FastifyCorsOptionsDelegate = FastifyCorsOptionsDelegateCallback | FastifyCorsOptionsDelegatePromise
export type FastifyPluginOptionsDelegate<T = FastifyCorsOptionsDelegate> = (instance: FastifyInstance) => T;
export const fastifyCors: FastifyCorsPlugin
export { fastifyCors as default };
}
declare function fastifyCors(
...params: Parameters<FastifyCorsPlugin>
): ReturnType<FastifyCorsPlugin>;
export = fastifyCors;

View File

@@ -0,0 +1,355 @@
import fastify, { FastifyRequest } from 'fastify'
import { expectType } from 'tsd'
import fastifyCors, {
FastifyCorsOptions,
FastifyCorsOptionsDelegate,
FastifyCorsOptionsDelegatePromise,
FastifyPluginOptionsDelegate,
OriginFunction
} from '..'
const app = fastify()
app.register(fastifyCors)
app.register(fastifyCors, {
origin: true,
allowedHeaders: 'authorization,content-type',
methods: 'GET,POST,PUT,PATCH,DELETE,OPTIONS',
credentials: true,
exposedHeaders: 'authorization',
maxAge: 13000,
cacheControl: 13000,
preflightContinue: false,
optionsSuccessStatus: 200,
preflight: false,
strictPreflight: false
})
app.register(fastifyCors, {
origin: true,
allowedHeaders: ['authorization', 'content-type'],
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
credentials: true,
exposedHeaders: ['authorization'],
maxAge: 13000,
cacheControl: 'public, max-age=3500',
preflightContinue: false,
optionsSuccessStatus: 200,
preflight: false,
strictPreflight: false
})
app.register(fastifyCors, {
origin: '*',
allowedHeaders: ['authorization', 'content-type'],
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
credentials: true,
exposedHeaders: ['authorization'],
maxAge: 13000,
cacheControl: 13000,
preflightContinue: false,
optionsSuccessStatus: 200,
preflight: false,
strictPreflight: false
})
app.register(fastifyCors, {
origin: /\*/,
allowedHeaders: ['authorization', 'content-type'],
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
credentials: true,
exposedHeaders: ['authorization'],
maxAge: 13000,
cacheControl: 13000,
preflightContinue: false,
optionsSuccessStatus: 200,
preflight: false,
strictPreflight: false
})
app.register(fastifyCors, {
origin: ['*', 'something'],
allowedHeaders: ['authorization', 'content-type'],
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
credentials: true,
exposedHeaders: ['authorization'],
maxAge: 13000,
cacheControl: 13000,
preflightContinue: false,
optionsSuccessStatus: 200,
preflight: false,
strictPreflight: false
})
app.register(fastifyCors, {
origin: [/\*/, /something/],
allowedHeaders: ['authorization', 'content-type'],
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
credentials: true,
exposedHeaders: ['authorization'],
maxAge: 13000,
cacheControl: 13000,
preflightContinue: false,
optionsSuccessStatus: 200,
preflight: false,
strictPreflight: false
})
const corsDelegate: OriginFunction = (origin, cb) => {
if (origin === undefined || /localhost/.test(origin)) {
cb(null, true)
return
}
cb(new Error(), false)
}
app.register(fastifyCors, {
origin: corsDelegate,
allowedHeaders: ['authorization', 'content-type'],
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
credentials: true,
exposedHeaders: ['authorization'],
maxAge: 13000,
cacheControl: 13000,
optionsSuccessStatus: 200,
preflight: false,
strictPreflight: false
})
app.register(fastifyCors, {
origin: (origin, cb) => cb(null, true)
})
app.register(fastifyCors, {
origin: (origin, cb) => cb(null, '*')
})
app.register(fastifyCors, {
origin: (origin, cb) => cb(null, /\*/)
})
const appHttp2 = fastify({ http2: true })
appHttp2.register(fastifyCors)
appHttp2.register(fastifyCors, {
origin: true,
allowedHeaders: 'authorization,content-type',
methods: 'GET,POST,PUT,PATCH,DELETE,OPTIONS',
credentials: true,
exposedHeaders: 'authorization',
maxAge: 13000,
cacheControl: 13000,
preflightContinue: false,
optionsSuccessStatus: 200,
preflight: false,
strictPreflight: false
})
appHttp2.register(fastifyCors, {
origin: true,
allowedHeaders: ['authorization', 'content-type'],
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
credentials: true,
exposedHeaders: ['authorization'],
maxAge: 13000,
cacheControl: 13000,
preflightContinue: false,
optionsSuccessStatus: 200,
preflight: false,
strictPreflight: false
})
appHttp2.register(fastifyCors, {
origin: '*',
allowedHeaders: ['authorization', 'content-type'],
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
credentials: true,
exposedHeaders: ['authorization'],
maxAge: 13000,
cacheControl: 13000,
preflightContinue: false,
optionsSuccessStatus: 200,
preflight: false,
strictPreflight: false
})
appHttp2.register(fastifyCors, {
origin: /\*/,
allowedHeaders: ['authorization', 'content-type'],
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
credentials: true,
exposedHeaders: ['authorization'],
maxAge: 13000,
cacheControl: 13000,
preflightContinue: false,
optionsSuccessStatus: 200,
preflight: false,
strictPreflight: false
})
appHttp2.register(fastifyCors, {
origin: ['*', 'something'],
allowedHeaders: ['authorization', 'content-type'],
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
credentials: true,
exposedHeaders: ['authorization'],
maxAge: 13000,
cacheControl: 13000,
preflightContinue: false,
optionsSuccessStatus: 200,
preflight: false,
strictPreflight: false
})
appHttp2.register(fastifyCors, {
origin: [/\*/, /something/],
allowedHeaders: ['authorization', 'content-type'],
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
credentials: true,
exposedHeaders: ['authorization'],
maxAge: 13000,
cacheControl: 13000,
preflightContinue: false,
optionsSuccessStatus: 200,
preflight: false,
strictPreflight: false
})
appHttp2.register(fastifyCors, {
origin: (origin: string | undefined, cb: (err: Error | null, allow: boolean) => void) => {
if (origin === undefined || /localhost/.test(origin)) {
cb(null, true)
return
}
cb(new Error(), false)
},
allowedHeaders: ['authorization', 'content-type'],
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
credentials: true,
exposedHeaders: ['authorization'],
maxAge: 13000,
cacheControl: 13000,
preflightContinue: false,
optionsSuccessStatus: 200,
preflight: false,
strictPreflight: false
})
appHttp2.register(fastifyCors, (): FastifyCorsOptionsDelegate => (req, cb) => {
cb(null, {
origin: [/\*/, /something/],
allowedHeaders: ['authorization', 'content-type'],
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
credentials: true,
exposedHeaders: ['authorization'],
maxAge: 13000,
cacheControl: 13000,
preflightContinue: false,
optionsSuccessStatus: 200,
preflight: false,
strictPreflight: false
})
})
appHttp2.register(fastifyCors, (): FastifyCorsOptionsDelegatePromise => (req) => {
return Promise.resolve({
origin: [/\*/, /something/],
allowedHeaders: ['authorization', 'content-type'],
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
credentials: true,
exposedHeaders: ['authorization'],
maxAge: 13000,
cacheControl: 13000,
preflightContinue: false,
optionsSuccessStatus: 200,
preflight: false,
strictPreflight: false
})
})
const delegate: FastifyPluginOptionsDelegate<FastifyCorsOptionsDelegatePromise> = () => async (req) => {
return {
origin: [/\*/, /something/],
allowedHeaders: ['authorization', 'content-type'],
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
credentials: true,
exposedHeaders: ['authorization'],
maxAge: 13000,
cacheControl: 13000,
preflightContinue: false,
optionsSuccessStatus: 200,
preflight: false,
strictPreflight: false
}
}
appHttp2.register(fastifyCors, {
hook: 'onRequest'
})
appHttp2.register(fastifyCors, {
hook: 'preParsing'
})
appHttp2.register(fastifyCors, {
hook: 'preValidation'
})
appHttp2.register(fastifyCors, {
hook: 'preHandler'
})
appHttp2.register(fastifyCors, {
hook: 'preSerialization'
})
appHttp2.register(fastifyCors, {
hook: 'onSend'
})
appHttp2.register(fastifyCors, {
hook: 'preParsing',
delegator: (req, cb) => {
if (req.url.startsWith('/some-value')) {
cb(new Error())
}
cb(null, {
origin: [/\*/, /something/],
allowedHeaders: ['authorization', 'content-type'],
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
credentials: true,
exposedHeaders: ['authorization'],
maxAge: 13000,
cacheControl: 12000,
preflightContinue: false,
optionsSuccessStatus: 200,
preflight: false,
strictPreflight: false
})
}
})
appHttp2.register(fastifyCors, {
hook: 'preParsing',
delegator: async (req: FastifyRequest): Promise<FastifyCorsOptions> => {
return {
origin: [/\*/, /something/],
allowedHeaders: ['authorization', 'content-type'],
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
credentials: true,
exposedHeaders: ['authorization'],
maxAge: 13000,
cacheControl: 'public, max-age=3500',
preflightContinue: false,
optionsSuccessStatus: 200,
preflight: false,
strictPreflight: false
}
}
})
appHttp2.register(fastifyCors, delegate)
appHttp2.register(fastifyCors, {
hook: 'preParsing',
origin: function (origin) {
expectType<string|undefined>(origin)
},
})

116
backend/node_modules/@fastify/cors/vary.js generated vendored Normal file
View File

@@ -0,0 +1,116 @@
'use strict'
const LRUCache = require('mnemonist/lru-cache')
/**
* Field Value Components
* Most HTTP header field values are defined using common syntax
* components (token, quoted-string, and comment) separated by
* whitespace or specific delimiting characters. Delimiters are chosen
* from the set of US-ASCII visual characters not allowed in a token
* (DQUOTE and "(),/:;<=>?@[\]{}").
*
* field-name = token
* token = 1*tchar
* tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*"
* / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
* / DIGIT / ALPHA
* ; any VCHAR, except delimiters
*
* @see https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.6
*/
const validFieldnameRE = /^[!#$%&'*+\-.^\w`|~]+$/u
function validateFieldname (fieldname) {
if (validFieldnameRE.test(fieldname) === false) {
throw new TypeError('Fieldname contains invalid characters.')
}
}
function parse (header) {
header = header.trim().toLowerCase()
const result = []
if (header.length === 0) {
// pass through
} else if (header.indexOf(',') === -1) {
result.push(header)
} else {
const il = header.length
let i = 0
let pos = 0
let char
// tokenize the header
for (i = 0; i < il; ++i) {
char = header[i]
// when we have whitespace set the pos to the next position
if (char === ' ') {
pos = i + 1
// `,` is the separator of vary-values
} else if (char === ',') {
// if pos and current position are not the same we have a valid token
if (pos !== i) {
result.push(header.slice(pos, i))
}
// reset the positions
pos = i + 1
}
}
if (pos !== i) {
result.push(header.slice(pos, i))
}
}
return result
}
function createAddFieldnameToVary (fieldname) {
const headerCache = new LRUCache(1000)
validateFieldname(fieldname)
return function (reply) {
let header = reply.getHeader('Vary')
if (!header) {
reply.header('Vary', fieldname)
return
}
if (header === '*') {
return
}
if (fieldname === '*') {
reply.header('Vary', '*')
return
}
if (Array.isArray(header)) {
header = header.join(', ')
}
if (!headerCache.has(header)) {
const vals = parse(header)
if (vals.indexOf('*') !== -1) {
headerCache.set(header, '*')
} else if (vals.indexOf(fieldname.toLowerCase()) === -1) {
headerCache.set(header, header + ', ' + fieldname)
} else {
headerCache.set(header, null)
}
}
const cached = headerCache.get(header)
if (cached !== null) {
reply.header('Vary', cached)
}
}
}
module.exports.createAddFieldnameToVary = createAddFieldnameToVary
module.exports.addOriginToVaryHeader = createAddFieldnameToVary('Origin')
module.exports.addAccessControlRequestHeadersToVaryHeader = createAddFieldnameToVary('Access-Control-Request-Headers')
module.exports.parse = parse

2
backend/node_modules/@fastify/error/.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

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

21
backend/node_modules/@fastify/error/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Fastify
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.

140
backend/node_modules/@fastify/error/README.md generated vendored Normal file
View File

@@ -0,0 +1,140 @@
# @fastify/error
[![CI](https://github.com/fastify/fastify-error/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/fastify/fastify-error/actions/workflows/ci.yml)
[![NPM version](https://img.shields.io/npm/v/@fastify/error.svg?style=flat)](https://www.npmjs.com/package/@fastify/error)
[![neostandard javascript style](https://img.shields.io/badge/code_style-neostandard-brightgreen?style=flat)](https://github.com/neostandard/neostandard)
A small utility, used by Fastify itself, for generating consistent error objects across your codebase and plugins.
### Install
```
npm i @fastify/error
```
### Usage
The module exports a function that you can use for consistent error objects, it takes 4 parameters:
```js
createError(code, message [, statusCode [, Base [, captureStackTrace]]])
```
- `code` (`string`, required) - The error code, you can access it later with `error.code`. For consistency, we recommend prefixing plugin error codes with `FST_`
- `message` (`string`, required) - The error message. You can also use interpolated strings for formatting the message.
- `statusCode` (`number`, optional) - The status code that Fastify will use if the error is sent via HTTP.
- `Base` (`ErrorConstructor`, optional) - The base error object that will be used. (eg `TypeError`, `RangeError`)
- `captureStackTrace` (`boolean`, optional) - Whether to capture the stack trace or not.
```js
const createError = require('@fastify/error')
const CustomError = createError('ERROR_CODE', 'Hello')
console.log(new CustomError()) // error.message => 'Hello'
```
How to use an interpolated string:
```js
const createError = require('@fastify/error')
const CustomError = createError('ERROR_CODE', 'Hello %s')
console.log(new CustomError('world')) // error.message => 'Hello world'
```
How to add cause:
```js
const createError = require('@fastify/error')
const CustomError = createError('ERROR_CODE', 'Hello %s')
console.log(new CustomError('world', {cause: new Error('cause')}))
// error.message => 'Hello world'
// error.cause => Error('cause')
```
### TypeScript
It is possible to limit your error constructor with a generic type using TypeScript:
```ts
const CustomError = createError<[string]>('ERROR_CODE', 'Hello %s')
new CustomError('world')
//@ts-expect-error
new CustomError(1)
```
### instanceof
All errors created with `createError` will be instances of the base error constructor you provided, or `Error` if none was provided.
```js
const createError = require('@fastify/error')
const CustomError = createError('ERROR_CODE', 'Hello %s', 500, TypeError)
const customError = new CustomError('world')
console.log(customError instanceof CustomError) // true
console.log(customError instanceof TypeError) // true
console.log(customError instanceof Error) // true
```
All instantiated errors are instances of the `FastifyError` class, which can be required directly from the module.
```js
const { createError, FastifyError } = require('@fastify/error')
const CustomError = createError('ERROR_CODE', 'Hello %s', 500, TypeError)
const customError = new CustomError('world')
console.log(customError instanceof FastifyError) // true
```
A `FastifyError` created by `createError` can extend another `FastifyError` while maintaining correct `instanceof` behavior.
```js
const { createError, FastifyError } = require('@fastify/error')
const CustomError = createError('ERROR_CODE', 'Hello %s', 500, TypeError)
const ChildCustomError = createError('CHILD_ERROR_CODE', 'Hello %s', 500, CustomError)
const customError = new ChildCustomError('world')
console.log(customError instanceof ChildCustomError) // true
console.log(customError instanceof CustomError) // true
console.log(customError instanceof FastifyError) // true
console.log(customError instanceof TypeError) // true
console.log(customError instanceof Error) // true
```
If `fastify-error` is installed multiple times directly or as a transitive dependency, `instanceof` checks for errors created by `createError` will still work correctly across these installations, as long as their error codes (e.g., `FST_ERR_CUSTOM_ERROR`) are identical.
```js
const { createError, FastifyError } = require('@fastify/error')
// CustomError from `@fastify/some-plugin` is created with `createError` and
// has its own `@fastify/error` installation as dependency. CustomError has
// FST_ERR_CUSTOM_ERROR as code.
const { CustomError: CustomErrorFromPlugin } = require('@fastify/some-plugin')
const CustomError = createError('FST_ERR_CUSTOM_ERROR', 'Hello %s', 500)
const customError = new CustomError('world')
const customErrorFromPlugin = new CustomErrorFromPlugin('world')
console.log(customError instanceof CustomError) // true
console.log(customError instanceof CustomErrorFromPlugin) // true
console.log(customErrorFromPlugin instanceof CustomError) // true
console.log(customErrorFromPlugin instanceof CustomErrorFromPlugin) // true
```
Changing the code of an instantiated Error will not change the result of the `instanceof` operator.
```js
const { createError, FastifyError } = require('@fastify/error')
const CustomError = createError('ERROR_CODE', 'Hello %s', 500, TypeError)
const AnotherCustomError = createError('ANOTHER_ERROR_CODE', 'Hello %s', 500, CustomError)
const customError = new CustomError('world')
customError.code = 'ANOTHER_ERROR_CODE'
console.log(customError instanceof CustomError) // true
console.log(customError instanceof AnotherCustomError) // false
```
## License
Licensed under [MIT](./LICENSE).

View File

@@ -0,0 +1,9 @@
'use strict'
const benchmark = require('benchmark')
const createError = require('..')
new benchmark.Suite()
.add('create FastifyError', function () { createError('CODE', 'Not available') }, { minSamples: 100 })
.on('cycle', function onCycle (event) { console.log(String(event.target)) })
.run({ async: false })

View File

@@ -0,0 +1,18 @@
'use strict'
const benchmark = require('benchmark')
const createError = require('..')
const FastifyError = createError('CODE', 'Not available')
const FastifyError1 = createError('CODE', 'Not %s available')
const FastifyError2 = createError('CODE', 'Not %s available %s')
const cause = new Error('cause')
new benchmark.Suite()
.add('instantiate Error', function () { new Error() }, { minSamples: 100 }) // eslint-disable-line no-new
.add('instantiate FastifyError', function () { new FastifyError() }, { minSamples: 100 }) // eslint-disable-line no-new
.add('instantiate FastifyError arg 1', function () { new FastifyError1('q') }, { minSamples: 100 }) // eslint-disable-line no-new
.add('instantiate FastifyError arg 2', function () { new FastifyError2('qq', 'ss') }, { minSamples: 100 }) // eslint-disable-line no-new
.add('instantiate FastifyError cause', function () { new FastifyError2({ cause }) }, { minSamples: 100 }) // eslint-disable-line no-new
.on('cycle', function onCycle (event) { console.log(String(event.target)) })
.run({ async: false })

View File

@@ -0,0 +1,13 @@
'use strict'
const benchmark = require('benchmark')
const createError = require('..')
const FastifyError = createError('CODE', 'Not available')
Error.stackTraceLimit = 0
new benchmark.Suite()
.add('no-stack instantiate Error', function () { new Error() }, { minSamples: 100 }) // eslint-disable-line no-new
.add('no-stack instantiate FastifyError', function () { new FastifyError() }, { minSamples: 100 }) // eslint-disable-line no-new
.on('cycle', function onCycle (event) { console.log(String(event.target)) })
.run({ async: false })

View File

@@ -0,0 +1,11 @@
'use strict'
const benchmark = require('benchmark')
const createError = require('..')
const FastifyError = createError('CODE', 'Not available')
new benchmark.Suite()
.add('FastifyError toString', function () { new FastifyError().toString() }, { minSamples: 100 })
.on('cycle', function onCycle (event) { console.log(String(event.target)) })
.run({ async: false })

6
backend/node_modules/@fastify/error/eslint.config.js generated vendored Normal file
View File

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

100
backend/node_modules/@fastify/error/index.js generated vendored Normal file
View File

@@ -0,0 +1,100 @@
'use strict'
const { format } = require('node:util')
function toString () {
return `${this.name} [${this.code}]: ${this.message}`
}
const FastifyGenericErrorSymbol = Symbol.for('fastify-error-generic')
function createError (code, message, statusCode = 500, Base = Error, captureStackTrace = createError.captureStackTrace) {
const shouldCreateFastifyGenericError = code === FastifyGenericErrorSymbol
if (shouldCreateFastifyGenericError) {
code = 'FST_ERR'
}
if (!code) throw new Error('Fastify error code must not be empty')
if (!message) throw new Error('Fastify error message must not be empty')
code = code.toUpperCase()
!statusCode && (statusCode = undefined)
const FastifySpecificErrorSymbol = Symbol.for(`fastify-error ${code}`)
function FastifyError (...args) {
if (!new.target) {
return new FastifyError(...args)
}
this.code = code
this.name = 'FastifyError'
this.statusCode = statusCode
const lastElement = args.length - 1
if (lastElement !== -1 && args[lastElement] && typeof args[lastElement] === 'object' && 'cause' in args[lastElement]) {
this.cause = args.pop().cause
}
this.message = format(message, ...args)
Error.stackTraceLimit && captureStackTrace && Error.captureStackTrace(this, FastifyError)
}
FastifyError.prototype = Object.create(Base.prototype, {
constructor: {
value: FastifyError,
enumerable: false,
writable: true,
configurable: true
},
[FastifyGenericErrorSymbol]: {
value: true,
enumerable: false,
writable: false,
configurable: false
},
[FastifySpecificErrorSymbol]: {
value: true,
enumerable: false,
writable: false,
configurable: false
}
})
if (shouldCreateFastifyGenericError) {
Object.defineProperty(FastifyError, Symbol.hasInstance, {
value (instance) {
return instance && instance[FastifyGenericErrorSymbol]
},
configurable: false,
writable: false,
enumerable: false
})
} else {
Object.defineProperty(FastifyError, Symbol.hasInstance, {
value (instance) {
return instance && instance[FastifySpecificErrorSymbol]
},
configurable: false,
writable: false,
enumerable: false
})
}
FastifyError.prototype[Symbol.toStringTag] = 'Error'
FastifyError.prototype.toString = toString
return FastifyError
}
createError.captureStackTrace = true
const FastifyErrorConstructor = createError(FastifyGenericErrorSymbol, 'Fastify Error', 500, Error)
module.exports = createError
module.exports.FastifyError = FastifyErrorConstructor
module.exports.default = createError
module.exports.createError = createError

75
backend/node_modules/@fastify/error/package.json generated vendored Normal file
View File

@@ -0,0 +1,75 @@
{
"name": "@fastify/error",
"version": "4.2.0",
"description": "A small utility, used by Fastify itself, for generating consistent error objects across your codebase and plugins.",
"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:unit": "c8 --100 node --test",
"test:typescript": "tsd"
},
"repository": {
"type": "git",
"url": "git+https://github.com/fastify/fastify-error.git"
},
"keywords": [
"fastify",
"error",
"utility",
"plugin"
],
"author": "Tomas Della Vedova",
"contributors": [
{
"name": "Matteo Collina",
"email": "hello@matteocollina.com"
},
{
"name": "James Sumners",
"url": "https://james.sumners.info"
},
{
"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-error/issues"
},
"homepage": "https://github.com/fastify/fastify-error#readme",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"devDependencies": {
"benchmark": "^2.1.4",
"c8": "^10.1.2",
"eslint": "^9.17.0",
"neostandard": "^0.12.0",
"tsd": "^0.32.0"
},
"tsd": {
"compilerOptions": {
"esModuleInterop": true
}
},
"publishConfig": {
"access": "public"
}
}

232
backend/node_modules/@fastify/error/test/index.test.js generated vendored Normal file
View File

@@ -0,0 +1,232 @@
'use strict'
const test = require('node:test')
const { createError, FastifyError } = require('..')
test('Create error with zero parameter', (t) => {
t.plan(6)
const NewError = createError('CODE', 'Not available')
const err = new NewError()
t.assert.ok(err instanceof Error)
t.assert.equal(err.name, 'FastifyError')
t.assert.equal(err.message, 'Not available')
t.assert.equal(err.code, 'CODE')
t.assert.equal(err.statusCode, 500)
t.assert.ok(err.stack)
})
test('Create error with 1 parameter', (t) => {
t.plan(6)
const NewError = createError('CODE', 'hey %s')
const err = new NewError('alice')
t.assert.equal(err.name, 'FastifyError')
t.assert.ok(err instanceof Error)
t.assert.equal(err.message, 'hey alice')
t.assert.equal(err.code, 'CODE')
t.assert.equal(err.statusCode, 500)
t.assert.ok(err.stack)
})
test('Create error with 1 parameter set to undefined', (t) => {
t.plan(1)
const NewError = createError('CODE', 'hey %s')
const err = new NewError(undefined)
t.assert.equal(err.message, 'hey undefined')
})
test('Create error with 2 parameters', (t) => {
t.plan(6)
const NewError = createError('CODE', 'hey %s, I like your %s')
const err = new NewError('alice', 'attitude')
t.assert.ok(err instanceof Error)
t.assert.equal(err.name, 'FastifyError')
t.assert.equal(err.message, 'hey alice, I like your attitude')
t.assert.equal(err.code, 'CODE')
t.assert.equal(err.statusCode, 500)
t.assert.ok(err.stack)
})
test('Create error with 2 parameters set to undefined', (t) => {
t.plan(1)
const NewError = createError('CODE', 'hey %s, I like your %s')
const err = new NewError(undefined, undefined)
t.assert.equal(err.message, 'hey undefined, I like your undefined')
})
test('Create error with 3 parameters', (t) => {
t.plan(6)
const NewError = createError('CODE', 'hey %s, I like your %s %s')
const err = new NewError('alice', 'attitude', 'see you')
t.assert.ok(err instanceof Error)
t.assert.equal(err.name, 'FastifyError')
t.assert.equal(err.message, 'hey alice, I like your attitude see you')
t.assert.equal(err.code, 'CODE')
t.assert.equal(err.statusCode, 500)
t.assert.ok(err.stack)
})
test('Create error with 3 parameters set to undefined', (t) => {
t.plan(4)
const NewError = createError('CODE', 'hey %s, I like your %s %s')
const err = new NewError(undefined, undefined, undefined)
t.assert.equal(err.message, 'hey undefined, I like your undefined undefined')
t.assert.equal(err.code, 'CODE')
t.assert.equal(err.statusCode, 500)
t.assert.ok(err.stack)
})
test('Create error with 4 parameters set to undefined', (t) => {
t.plan(4)
const NewError = createError('CODE', 'hey %s, I like your %s %s and %s')
const err = new NewError(undefined, undefined, undefined, undefined)
t.assert.equal(
err.message,
'hey undefined, I like your undefined undefined and undefined'
)
t.assert.equal(err.code, 'CODE')
t.assert.equal(err.statusCode, 500)
t.assert.ok(err.stack)
})
test('Create error with no statusCode property', (t) => {
t.plan(6)
const NewError = createError('CODE', 'hey %s', 0)
const err = new NewError('dude')
t.assert.ok(err instanceof Error)
t.assert.equal(err.name, 'FastifyError')
t.assert.equal(err.message, 'hey dude')
t.assert.equal(err.code, 'CODE')
t.assert.equal(err.statusCode, undefined)
t.assert.ok(err.stack)
})
test('Should throw when error code has no fastify code', (t) => {
t.plan(1)
t.assert.throws(
() => createError(),
new Error('Fastify error code must not be empty')
)
})
test('Should throw when error code has no message', (t) => {
t.assert.throws(
() => createError('code'),
new Error('Fastify error message must not be empty')
)
})
test('Create error with different base', (t) => {
t.plan(7)
const NewError = createError('CODE', 'hey %s', 500, TypeError)
const err = new NewError('dude')
t.assert.ok(err instanceof Error)
t.assert.ok(err instanceof TypeError)
t.assert.equal(err.name, 'FastifyError')
t.assert.equal(err.message, 'hey dude')
t.assert.equal(err.code, 'CODE')
t.assert.equal(err.statusCode, 500)
t.assert.ok(err.stack)
})
test('Create error with different base (no stack) (global)', (t) => {
t.plan(7)
createError.captureStackTrace = false
const NewError = createError('CODE', 'hey %s', 500, TypeError)
const err = new NewError('dude')
t.assert.ok(err instanceof Error)
t.assert.ok(err instanceof TypeError)
t.assert.equal(err.name, 'FastifyError')
t.assert.equal(err.message, 'hey dude')
t.assert.equal(err.code, 'CODE')
t.assert.equal(err.statusCode, 500)
t.assert.equal(err.stack, undefined)
createError.captureStackTrace = true
})
test('Create error with different base (no stack) (parameter)', (t) => {
t.plan(7)
const NewError = createError('CODE', 'hey %s', 500, TypeError, false)
const err = new NewError('dude')
t.assert.ok(err instanceof Error)
t.assert.ok(err instanceof TypeError)
t.assert.equal(err.name, 'FastifyError')
t.assert.equal(err.message, 'hey dude')
t.assert.equal(err.code, 'CODE')
t.assert.equal(err.statusCode, 500)
t.assert.equal(err.stack, undefined)
})
test('FastifyError.toString returns code', (t) => {
t.plan(1)
const NewError = createError('CODE', 'foo')
const err = new NewError()
t.assert.equal(err.toString(), 'FastifyError [CODE]: foo')
})
test('Create the error without the new keyword', (t) => {
t.plan(6)
const NewError = createError('CODE', 'Not available')
const err = NewError()
t.assert.ok(err instanceof Error)
t.assert.equal(err.name, 'FastifyError')
t.assert.equal(err.message, 'Not available')
t.assert.equal(err.code, 'CODE')
t.assert.equal(err.statusCode, 500)
t.assert.ok(err.stack)
})
test('Create an error with cause', (t) => {
t.plan(2)
const cause = new Error('HEY')
const NewError = createError('CODE', 'Not available')
const err = NewError({ cause })
t.assert.ok(err instanceof Error)
t.assert.equal(err.cause, cause)
})
test('Create an error with cause and message', (t) => {
t.plan(2)
const cause = new Error('HEY')
const NewError = createError('CODE', 'Not available: %s')
const err = NewError('foo', { cause })
t.assert.ok(err instanceof Error)
t.assert.equal(err.cause, cause)
})
test('Create an error with last argument null', (t) => {
t.plan(2)
const cause = new Error('HEY')
const NewError = createError('CODE', 'Not available')
const err = NewError({ cause }, null)
t.assert.ok(err instanceof Error)
t.assert.ifError(err.cause)
})
test('check if FastifyError is instantiable', (t) => {
t.plan(2)
const err = new FastifyError()
t.assert.ok(err instanceof FastifyError)
t.assert.ok(err instanceof Error)
})

View File

@@ -0,0 +1,263 @@
'use strict'
const cp = require('node:child_process')
const fs = require('node:fs')
const path = require('node:path')
const os = require('node:os')
const test = require('node:test')
const { createError, FastifyError } = require('..')
test('Readme: All errors created with `createError` will be instances of the base error constructor you provided, or `Error` if none was provided.', (t) => {
t.plan(3)
const CustomError = createError('ERROR_CODE', 'Hello %s', 500, TypeError)
const customError = new CustomError('world')
t.assert.ok(customError instanceof CustomError)
t.assert.ok(customError instanceof TypeError)
t.assert.ok(customError instanceof Error)
})
test('Readme: All instantiated errors will be instances of the `FastifyError` class. The `FastifyError` class can be required from the module directly.', (t) => {
t.plan(1)
const CustomError = createError('ERROR_CODE', 'Hello %s', 500, TypeError)
const customError = new CustomError('world')
t.assert.ok(customError instanceof FastifyError)
})
test('Readme: It is possible to create a `FastifyError` that extends another `FastifyError`, created by `createError`, while instanceof working correctly.', (t) => {
t.plan(5)
const CustomError = createError('ERROR_CODE', 'Hello %s', 500, TypeError)
const ChildCustomError = createError('CHILD_ERROR_CODE', 'Hello %s', 500, CustomError)
const customError = new ChildCustomError('world')
t.assert.ok(customError instanceof ChildCustomError)
t.assert.ok(customError instanceof CustomError)
t.assert.ok(customError instanceof FastifyError)
t.assert.ok(customError instanceof TypeError)
t.assert.ok(customError instanceof Error)
})
test('Readme: Changing the code of an instantiated Error will not change the result of the `instanceof` operator.', (t) => {
t.plan(3)
const CustomError = createError('ERROR_CODE', 'Hello %s', 500, TypeError)
const AnotherCustomError = createError('ANOTHER_ERROR_CODE', 'Hello %s', 500, CustomError)
const customError = new CustomError('world')
customError.code = 'ANOTHER_ERROR_CODE'
t.assert.ok(customError instanceof CustomError)
t.assert.ok(customError instanceof AnotherCustomError === false)
t.assert.ok(customError instanceof FastifyError)
})
test('check if createError creates an Error which is instanceof Error', (t) => {
t.plan(3)
const CustomFastifyError = createError('CODE', 'Not available')
const err = CustomFastifyError()
t.assert.ok(err instanceof Error)
t.assert.ok(err instanceof SyntaxError === false)
t.assert.ok(err instanceof TypeError === false)
})
test('check if createError creates an Error which is instanceof FastifyError', (t) => {
t.plan(4)
const CustomFastifyError = createError('CODE', 'Not available')
const err = CustomFastifyError()
t.assert.ok(err instanceof Error)
t.assert.ok(err instanceof FastifyError)
t.assert.ok(err instanceof SyntaxError === false)
t.assert.ok(err instanceof TypeError === false)
})
test('check if createError creates an Error with the right BaseConstructor', (t) => {
t.plan(2)
const CustomFastifyError = createError('CODE', 'Not available', 500, TypeError)
const err = CustomFastifyError()
t.assert.ok(err instanceof Error)
t.assert.ok(err instanceof TypeError)
})
test('check if createError creates an Error with the right BaseConstructor, which is a FastifyError', (t) => {
t.plan(6)
const BaseFastifyError = createError('CODE', 'Not available', 500, TypeError)
const CustomFastifyError = createError('CODE', 'Not available', 500, BaseFastifyError)
const err = CustomFastifyError()
t.assert.ok(err instanceof Error)
t.assert.ok(err instanceof TypeError)
t.assert.ok(err instanceof FastifyError)
t.assert.ok(err instanceof BaseFastifyError)
t.assert.ok(err instanceof CustomFastifyError)
t.assert.ok(err instanceof SyntaxError === false)
})
// for more information see https://github.com/fastify/fastify-error/pull/86#issuecomment-1301466407
test('ensure that instanceof works accross different installations of the fastify-error module', async (t) => {
const assertsPlanned = 5
t.plan(assertsPlanned)
// We need to create a test environment where fastify-error is installed in two different locations
// and then we will check if the error created in one location is instanceof the error created in the other location
// This is done by creating a test directory with the following structure:
// /
// ├── index.js
// └── node_modules/
// ├── fastify-error/
// │ └── index.js
// └── dep/
// ├── index.js
// └── node_modules/
// └── fastify-error/
// └── index.js
const testDirectoryPrefix = 'fastify-error-instanceof-test-'
const testCwd = path.resolve(os.tmpdir(), `${testDirectoryPrefix}${Math.random().toString(36).substring(2, 15)}`)
fs.mkdirSync(testCwd, { recursive: true })
// Create the index.js. It will be executed as a forked process, so we need to
// use process.send to send messages back to the parent process.
fs.writeFileSync(path.resolve(testCwd, 'index.js'), `
'use strict'
const path = require('node:path')
const { createError, FastifyError } = require('fastify-error')
const { foo } = require('dep')
const actualPathOfFastifyError = require.resolve('fastify-error')
const expectedPathOfFastifyError = path.resolve('node_modules', 'fastify-error', 'index.js')
// Ensure that fastify-error is required from the node_modules directory of the test-project
if (actualPathOfFastifyError !== expectedPathOfFastifyError) {
console.error('actualPathOfFastifyError', actualPathOfFastifyError)
console.error('expectedPathOfFastifyError', expectedPathOfFastifyError)
throw new Error('fastify-error should be required from the node_modules directory of the test-project')
}
const Boom = createError('Boom', 'Boom', 500)
const ChildBoom = createError('ChildBoom', 'Boom', 500, Boom)
const NotChildBoom = createError('NotChildBoom', 'NotChildBoom', 500, Boom)
try {
foo()
} catch (err) {
process.send(err instanceof Error)
process.send(err instanceof FastifyError)
process.send(err instanceof NotChildBoom)
process.send(err instanceof Boom)
process.send(err instanceof ChildBoom)
}
`)
// Create /node_modules/fastify-error directory
// Copy the index.js file to the fastify-error directory
fs.mkdirSync(path.resolve(testCwd, 'node_modules', 'fastify-error'), { recursive: true })
fs.copyFileSync(path.resolve(process.cwd(), 'index.js'), path.resolve(testCwd, 'node_modules', 'fastify-error', 'index.js'))
// Create /node_modules/dep/node_modules/fastify-error directory
// Copy the index.js to the fastify-error directory
fs.mkdirSync(path.resolve(testCwd, 'node_modules', 'dep', 'node_modules', 'fastify-error'), { recursive: true })
fs.copyFileSync(path.resolve(process.cwd(), 'index.js'), path.resolve(testCwd, 'node_modules', 'dep', 'node_modules', 'fastify-error', 'index.js'))
// Create /node_modules/dep/index.js. It will export a function foo which will
// throw an error when called. The error will be an instance of ChildBoom, created
// by the fastify-error module in the node_modules directory of dep.
fs.writeFileSync(path.resolve(testCwd, 'node_modules', 'dep', 'index.js'), `
'use strict'
const path = require('node:path')
const { createError } = require('fastify-error')
const actualPathOfFastifyError = require.resolve('fastify-error')
const expectedPathOfFastifyError = path.resolve('node_modules', 'dep', 'node_modules', 'fastify-error', 'index.js')
// Ensure that fastify-error is required from the node_modules directory of the test-project
if (actualPathOfFastifyError !== expectedPathOfFastifyError) {
console.error('actualPathOfFastifyError', actualPathOfFastifyError)
console.error('expectedPathOfFastifyError', expectedPathOfFastifyError)
throw new Error('fastify-error should be required from the node_modules directory of dep')
}
const Boom = createError('Boom', 'Boom', 500)
const ChildBoom = createError('ChildBoom', 'Boom', 500, Boom)
module.exports.foo = function foo () {
throw new ChildBoom('foo go Boom')
}
`)
const finishedPromise = {
promise: undefined,
reject: undefined,
resolve: undefined,
}
finishedPromise.promise = new Promise((resolve, reject) => {
finishedPromise.resolve = resolve
finishedPromise.reject = reject
})
const child = cp.fork(path.resolve(testCwd, 'index.js'), {
cwd: testCwd,
stdio: 'inherit',
env: {
...process.env,
NODE_OPTIONS: '--no-warnings'
},
})
let messageCount = 0
child.on('message', message => {
try {
switch (messageCount) {
case 0:
t.assert.strictEqual(message, true, 'instanceof Error')
break
case 1:
t.assert.strictEqual(message, true, 'instanceof FastifyError')
break
case 2:
t.assert.strictEqual(message, false, 'instanceof NotChildBoom')
break
case 3:
t.assert.strictEqual(message, true, 'instanceof Boom')
break
case 4:
t.assert.strictEqual(message, true, 'instanceof ChildBoom')
break
}
if (++messageCount === assertsPlanned) {
finishedPromise.resolve()
}
} catch (err) {
finishedPromise.reject(err)
}
})
child.on('error', err => {
finishedPromise.reject(err)
})
await finishedPromise.promise
// Cleanup
// As we are creating the test-setup on the fly in the /tmp directory, we can remove it
// safely when we are done. It is not relevant for the test if the deletion fails.
try {
fs.rmSync(testCwd, { recursive: true, force: true })
} catch {}
})

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

@@ -0,0 +1,49 @@
declare function createError<C extends string, SC extends number, Arg extends unknown[] = [any?, any?, any?]> (
code: C,
message: string,
statusCode: SC,
Base?: ErrorConstructor,
captureStackTrace?: boolean
): createError.FastifyErrorConstructor<{ code: C, statusCode: SC }, Arg>
declare function createError<C extends string, Arg extends unknown[] = [any?, any?, any?]> (
code: C,
message: string,
statusCode?: number,
Base?: ErrorConstructor,
captureStackTrace?: boolean
): createError.FastifyErrorConstructor<{ code: C }, Arg>
declare function createError<Arg extends unknown[] = [any?, any?, any?]> (
code: string,
message: string,
statusCode?: number,
Base?: ErrorConstructor,
captureStackTrace?: boolean
): createError.FastifyErrorConstructor<{ code: string }, Arg>
type CreateError = typeof createError
declare namespace createError {
export interface FastifyError extends Error {
code: string
name: string
statusCode?: number
}
export interface FastifyErrorConstructor<
E extends { code: string, statusCode?: number } = { code: string, statusCode?: number },
T extends unknown[] = [any?, any?, any?]
> {
new(...arg: T): FastifyError & E
(...arg: T): FastifyError & E
readonly prototype: FastifyError & E
}
export const FastifyError: FastifyErrorConstructor
export const createError: CreateError
export { createError as default }
}
export = createError

View File

@@ -0,0 +1,92 @@
import createError, { FastifyError, FastifyErrorConstructor } from '..'
import { expectType, expectError } from 'tsd'
const CustomError = createError('ERROR_CODE', 'message')
expectType<FastifyErrorConstructor<{ code: 'ERROR_CODE' }>>(CustomError)
const err = new CustomError()
expectType<FastifyError & { code: 'ERROR_CODE' }>(err)
expectType<'ERROR_CODE'>(err.code)
expectType<string>(err.message)
expectType<number | undefined>(err.statusCode)
const CustomErrorNoStackTrace = createError('ERROR_CODE', 'message', undefined, undefined, false)
expectType<FastifyErrorConstructor<{ code: 'ERROR_CODE' }>>(CustomErrorNoStackTrace)
const errNoStackTrace = new CustomErrorNoStackTrace()
expectType<FastifyError & { code: 'ERROR_CODE' }>(errNoStackTrace)
expectType<'ERROR_CODE'>(errNoStackTrace.code)
expectType<string>(errNoStackTrace.message)
expectType<number | undefined>(errNoStackTrace.statusCode)
const CustomTypedError = createError('OTHER_CODE', 'message', 400)
expectType<FastifyErrorConstructor<{ code: 'OTHER_CODE', statusCode: 400 }>>(CustomTypedError)
const typed = new CustomTypedError()
expectType<FastifyError & { code: 'OTHER_CODE', statusCode: 400 }>(typed)
expectType<'OTHER_CODE'>(typed.code)
expectType<string>(typed.message)
expectType<400>(typed.statusCode)
/* eslint-disable no-new */
const CustomTypedArgError = createError<[string]>('OTHER_CODE', 'expect %s message', 400)
CustomTypedArgError('a')
expectError(CustomTypedArgError('a', 'b'))
expectError(new CustomTypedArgError('a', 'b'))
expectError(CustomTypedArgError(1))
expectError(new CustomTypedArgError(1))
const CustomTypedArgError2 = createError<string, number, [string]>('OTHER_CODE', 'expect %s message', 400)
CustomTypedArgError2('a')
expectError(CustomTypedArgError2('a', 'b'))
expectError(new CustomTypedArgError2('a', 'b'))
expectError(CustomTypedArgError2(1))
expectError(new CustomTypedArgError2(1))
const CustomTypedArgError3 = createError<string, number, [string, string]>('OTHER_CODE', 'expect %s message but got %s', 400)
expectError(CustomTypedArgError3('a'))
CustomTypedArgError3('a', 'b')
new CustomTypedArgError3('a', 'b')
expectError(CustomTypedArgError3(1))
expectError(new CustomTypedArgError3(1))
expectError(new CustomTypedArgError3(1, 2))
expectError(new CustomTypedArgError3('1', 2))
expectError(new CustomTypedArgError3(1, '2'))
const CustomTypedArgError4 = createError<string, number, [string, string]>('OTHER_CODE', 'expect %s message but got %s', 400)
expectError(CustomTypedArgError4('a'))
CustomTypedArgError4('a', 'b')
new CustomTypedArgError4('a', 'b')
expectError(CustomTypedArgError4(1))
expectError(new CustomTypedArgError4(1))
expectError(new CustomTypedArgError4(1, 2))
expectError(new CustomTypedArgError4('1', 2))
expectError(new CustomTypedArgError4(1, '2'))
const CustomTypedArgError5 = createError<[string, string, string, string]>('OTHER_CODE', 'expect %s message but got %s. Please contact %s by emailing to %s', 400)
expectError(CustomTypedArgError5('a'))
expectError(new CustomTypedArgError5('a', 'b'))
expectError(new CustomTypedArgError5('a', 'b', 'c'))
CustomTypedArgError5('a', 'b', 'c', 'd')
expectError(new CustomTypedArgError5('a', 'b', 'c', 'd', 'e'))
const CustomTypedArgError6 = createError<string, number, [string, string, string, string]>('OTHER_CODE', 'expect %s message but got %s. Please contact %s by emailing to %s', 400)
expectError(CustomTypedArgError6('a'))
expectError(new CustomTypedArgError6('a', 'b'))
expectError(new CustomTypedArgError6('a', 'b', 'c'))
CustomTypedArgError6('a', 'b', 'c', 'd')
expectError(new CustomTypedArgError6('a', 'b', 'c', 'd', 'e'))
const CustomErrorWithErrorConstructor = createError('ERROR_CODE', 'message', 500, TypeError)
expectType<FastifyErrorConstructor<{ code: 'ERROR_CODE', statusCode: 500 }>>(CustomErrorWithErrorConstructor)
CustomErrorWithErrorConstructor({ cause: new Error('Error') })
const customErrorWithErrorConstructor = CustomErrorWithErrorConstructor()
if (customErrorWithErrorConstructor instanceof FastifyError) {
expectType<'ERROR_CODE'>(customErrorWithErrorConstructor.code)
expectType<string>(customErrorWithErrorConstructor.message)
expectType<500>(customErrorWithErrorConstructor.statusCode)
}
const error = new FastifyError('ERROR_CODE', 'message', 500)
if (error instanceof FastifyError) {
expectType<string>(error.code)
expectType<string>(error.message)
expectType<number | undefined>(error.statusCode)
}

View File

@@ -0,0 +1 @@
{"extends": "standard"}

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: "weekly"
open-pull-requests-limit: 10

View File

@@ -0,0 +1,18 @@
name: CI
on:
push:
paths-ignore:
- 'docs/**'
- '*.md'
pull_request:
paths-ignore:
- 'docs/**'
- '*.md'
jobs:
test:
uses: fastify/workflows/.github/workflows/plugins-ci.yml@v3
with:
license-check: true
lint: true

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Fastify
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,127 @@
# @fastify/fast-json-stringify-compiler
Build and manage the [`fast-json-stringify`](https://www.npmjs.com/package/fast-json-stringify) instances for the fastify framework.
This package is responsible for compiling the application's `response` JSON schemas into optimized functions to speed up the response time.
[![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](http://standardjs.com/)
[![Continuous Integration](https://github.com/fastify/fast-json-stringify-compiler/workflows/Continuous%20Integration/badge.svg)](https://github.com/fastify/fast-json-stringify-compiler/actions/workflows/ci.yml)
## Versions
| `@fastify/fast-json-stringify-compiler` | `fast-json-stringify` | Supported `fastify` |
|----------------------------------------:|----------------------:|--------------------:|
| v1.x | v3.x | ^3.x |
| v2.x | v3.x | ^4.x |
| v3.x | v4.x | ^4.x |
| v4.x | v5.x | ^4.x |
### fast-json-stringify Configuration
The `fast-json-stringify` configuration is the default one. You can check it the default settings in the [`fast-json-stringify` option](https://github.com/fastify/fast-json-stringify/#options) documentation.
You can also override the default configuration by passing the [`serializerOpts`](https://www.fastify.io/docs/latest/Reference/Server/#serializeropts) configuration to the Fastify instance.
## Usage
This module is already used as default by Fastify.
If you need to provide to your server instance a different version, refer to [the official doc](https://www.fastify.io/docs/latest/Reference/Server/#schemacontroller).
### fast-json-stringify Standalone
`fast-json-stringify@v4.1.0` introduces the [standalone feature](https://github.com/fastify/fast-json-stringify#standalone) that let you to pre-compile your schemas and use them in your application for a faster startup.
To use this feature, you must be aware of the following:
1. You must generate and save the application's compiled schemas.
2. Read the compiled schemas from the file and provide them back to your Fastify application.
#### Generate and save the compiled schemas
Fastify helps you to generate the serialization schemas functions and it is your choice to save them where you want.
To accomplish this, you must use a new compiler: `@fastify/fast-json-stringify-compiler/standalone`.
You must provide 2 parameters to this compiler:
- `readMode: false`: a boolean to indicate that you want generate the schemas functions string.
- `storeFunction`" a sync function that must store the source code of the schemas functions. You may provide an async function too, but you must manage errors.
When `readMode: false`, **the compiler is meant to be used in development ONLY**.
```js
const { StandaloneSerializer } = require('@fastify/fast-json-stringify-compiler')
const factory = StandaloneSerializer({
readMode: false,
storeFunction (routeOpts, schemaSerializationCode) {
// routeOpts is like: { schema, method, url, httpStatus }
// schemaSerializationCode is a string source code that is the compiled schema function
const fileName = generateFileName(routeOpts)
fs.writeFileSync(path.join(__dirname, fileName), schemaSerializationCode)
}
})
const app = fastify({
jsonShorthand: false,
schemaController: {
compilersFactory: {
buildSerializer: factory
}
}
})
// ... add all your routes with schemas ...
app.ready().then(() => {
// at this stage all your schemas are compiled and stored in the file system
// now it is important to turn off the readMode
})
```
#### Read the compiled schemas functions
At this stage, you should have a file for every route's schema.
To use them, you must use the `@fastify/fast-json-stringify-compiler/standalone` with the parameters:
- `readMode: true`: a boolean to indicate that you want read and use the schemas functions string.
- `restoreFunction`" a sync function that must return a function to serialize the route's payload.
Important keep away before you continue reading the documentation:
- when you use the `readMode: true`, the application schemas are not compiled (they are ignored). So, if you change your schemas, you must recompile them!
- as you can see, you must relate the route's schema to the file name using the `routeOpts` object. You may use the `routeOpts.schema.$id` field to do so, it is up to you to define a unique schema identifier.
```js
const { StandaloneSerializer } = require('@fastify/fast-json-stringify-compiler')
const factory = StandaloneSerializer({
readMode: true,
restoreFunction (routeOpts) {
// routeOpts is like: { schema, method, url, httpStatus }
const fileName = generateFileName(routeOpts)
return require(path.join(__dirname, fileName))
}
})
const app = fastify({
jsonShorthand: false,
schemaController: {
compilersFactory: {
buildSerializer: factory
}
}
})
// ... add all your routes with schemas as before...
app.listen({ port: 3000 })
```
### How it works
This module provide a factory function to produce [Serializer Compilers](https://www.fastify.io/docs/latest/Reference/Server/#serializercompiler) functions.
## License
Licensed under [MIT](./LICENSE).

View File

@@ -0,0 +1,23 @@
'use strict'
const fastJsonStringify = require('fast-json-stringify')
function SerializerSelector () {
return function buildSerializerFactory (externalSchemas, serializerOpts) {
const fjsOpts = Object.assign({}, serializerOpts, { schema: externalSchemas })
return responseSchemaCompiler.bind(null, fjsOpts)
}
}
function responseSchemaCompiler (fjsOpts, { schema /* method, url, httpStatus */ }) {
if (fjsOpts.schema && schema.$id && fjsOpts.schema[schema.$id]) {
fjsOpts.schema = { ...fjsOpts.schema }
delete fjsOpts.schema[schema.$id]
}
return fastJsonStringify(schema, fjsOpts)
}
module.exports = SerializerSelector
module.exports.default = SerializerSelector
module.exports.SerializerSelector = SerializerSelector
module.exports.StandaloneSerializer = require('./standalone')

View File

@@ -0,0 +1,40 @@
{
"name": "@fastify/fast-json-stringify-compiler",
"description": "Build and manage the fast-json-stringify instances for the fastify framework",
"version": "4.3.0",
"main": "index.js",
"types": "types/index.d.ts",
"scripts": {
"lint": "standard",
"lint:fix": "standard --fix",
"unit": "tap test/**/*.test.js",
"test": "npm run unit && npm run test:typescript",
"test:typescript": "tsd"
},
"repository": {
"type": "git",
"url": "git+https://github.com/fastify/fast-json-stringify-compiler.git"
},
"keywords": [],
"author": "Manuel Spigolon <manuel.spigolon@nearform.com> (https://github.com/Eomm)",
"license": "MIT",
"bugs": {
"url": "https://github.com/fastify/fast-json-stringify-compiler/issues"
},
"homepage": "https://github.com/fastify/fast-json-stringify-compiler#readme",
"devDependencies": {
"@fastify/pre-commit": "^2.0.2",
"fastify": "^4.0.0",
"sanitize-filename": "^1.6.3",
"standard": "^17.0.0",
"tap": "^16.0.0",
"tsd": "^0.28.0"
},
"pre-commit": [
"lint",
"test"
],
"dependencies": {
"fast-json-stringify": "^5.7.0"
}
}

View File

@@ -0,0 +1,42 @@
'use strict'
const SerializerSelector = require('./index')
function StandaloneSerializer (options = { readMode: true }) {
if (options.readMode === true && typeof options.restoreFunction !== 'function') {
throw new Error('You must provide a function for the restoreFunction-option when readMode ON')
}
if (options.readMode !== true && typeof options.storeFunction !== 'function') {
throw new Error('You must provide a function for the storeFunction-option when readMode OFF')
}
if (options.readMode === true) {
// READ MODE: it behalf only in the restore function provided by the user
return function wrapper () {
return function (opts) {
return options.restoreFunction(opts)
}
}
}
// WRITE MODE: it behalf on the default SerializerSelector, wrapping the API to run the Ajv Standalone code generation
const factory = SerializerSelector()
return function wrapper (externalSchemas, serializerOpts = {}) {
// to generate the serialization source code, this option is mandatory
serializerOpts.mode = 'standalone'
const compiler = factory(externalSchemas, serializerOpts)
return function (opts) { // { schema/*, method, url, httpPart */ }
const serializeFuncCode = compiler(opts)
options.storeFunction(opts, serializeFuncCode)
// eslint-disable-next-line no-new-func
return new Function(serializeFuncCode)
}
}
}
module.exports = StandaloneSerializer
module.exports.default = StandaloneSerializer

View File

@@ -0,0 +1,26 @@
'use strict'
const t = require('tap')
const FjsCompiler = require('../index')
t.test('Use input schema duplicate in the externalSchemas', async t => {
t.plan(1)
const externalSchemas = {
schema1: {
$id: 'schema1',
type: 'number'
},
schema2: {
$id: 'schema2',
type: 'string'
}
}
const factory = FjsCompiler()
const compiler = factory(externalSchemas)
compiler({ schema: externalSchemas.schema1 })
compiler({ schema: externalSchemas.schema2 })
t.pass()
})

View File

@@ -0,0 +1,78 @@
'use strict'
const t = require('tap')
const fastify = require('fastify')
const FjsCompiler = require('../index')
const echo = async (req, reply) => { return req.body }
const sampleSchema = Object.freeze({
$id: 'example1',
type: 'object',
properties: {
name: { type: 'string' }
}
})
const externalSchemas1 = Object.freeze({})
const externalSchemas2 = Object.freeze({
foo: {
$id: 'foo',
type: 'object',
properties: {
name: { type: 'string' }
}
}
})
const fastifyFjsOptionsDefault = Object.freeze({})
t.test('basic usage', t => {
t.plan(1)
const factory = FjsCompiler()
const compiler = factory(externalSchemas1, fastifyFjsOptionsDefault)
const serializeFunc = compiler({ schema: sampleSchema })
const result = serializeFunc({ name: 'hello' })
t.equal(result, '{"name":"hello"}')
})
t.test('fastify integration', async t => {
const factory = FjsCompiler()
const app = fastify({
serializerOpts: {
rounding: 'ceil'
},
schemaController: {
compilersFactory: {
buildSerializer: factory
}
}
})
app.addSchema(externalSchemas2.foo)
app.post('/', {
handler: echo,
schema: {
response: {
200: {
$ref: 'foo#'
}
}
}
})
const res = await app.inject({
url: '/',
method: 'POST',
payload: {
version: '1',
foo: 'this is not a number',
name: 'serialize me'
}
})
t.equal(res.statusCode, 200)
t.same(res.json(), { name: 'serialize me' })
})

View File

@@ -0,0 +1,230 @@
'use strict'
const fs = require('fs')
const path = require('path')
const t = require('tap')
const fastify = require('fastify')
const sanitize = require('sanitize-filename')
const { StandaloneSerializer: FjsStandaloneCompiler } = require('../')
const generatedFileNames = []
function generateFileName (routeOpts) {
const fileName = `/fjs-generated-${sanitize(routeOpts.schema.$id)}-${routeOpts.method}-${routeOpts.httpPart}-${sanitize(routeOpts.url)}.js`
generatedFileNames.push(fileName)
return fileName
}
t.test('standalone', t => {
t.plan(5)
t.teardown(async () => {
for (const fileName of generatedFileNames) {
try {
await fs.promises.unlink(path.join(__dirname, fileName))
} catch (e) {}
}
})
t.test('errors', t => {
t.plan(2)
t.throws(() => {
FjsStandaloneCompiler()
}, 'missing restoreFunction')
t.throws(() => {
FjsStandaloneCompiler({ readMode: false })
}, 'missing storeFunction')
})
t.test('generate standalone code', t => {
t.plan(5)
const base = {
$id: 'urn:schema:base',
definitions: {
hello: { type: 'string' }
},
type: 'object',
properties: {
hello: { $ref: '#/definitions/hello' }
}
}
const refSchema = {
$id: 'urn:schema:ref',
type: 'object',
properties: {
hello: { $ref: 'urn:schema:base#/definitions/hello' }
}
}
const endpointSchema = {
schema: {
$id: 'urn:schema:endpoint',
$ref: 'urn:schema:ref'
}
}
const schemaMap = {
[base.$id]: base,
[refSchema.$id]: refSchema
}
const factory = FjsStandaloneCompiler({
readMode: false,
storeFunction (routeOpts, schemaSerializerCode) {
t.same(routeOpts, endpointSchema)
t.type(schemaSerializerCode, 'string')
fs.writeFileSync(path.join(__dirname, '/fjs-generated.js'), schemaSerializerCode)
generatedFileNames.push('/fjs-generated.js')
t.pass('stored the serializer function')
}
})
const compiler = factory(schemaMap)
compiler(endpointSchema)
t.pass('compiled the endpoint schema')
t.test('usage standalone code', t => {
t.plan(3)
const standaloneSerializer = require('./fjs-generated')
t.ok(standaloneSerializer)
const valid = standaloneSerializer({ hello: 'world' })
t.same(valid, JSON.stringify({ hello: 'world' }))
const invalid = standaloneSerializer({ hello: [] })
t.same(invalid, '{"hello":""}')
})
})
t.test('fastify integration - writeMode', async t => {
t.plan(4)
const factory = FjsStandaloneCompiler({
readMode: false,
storeFunction (routeOpts, schemaSerializationCode) {
const fileName = generateFileName(routeOpts)
t.ok(routeOpts)
fs.writeFileSync(path.join(__dirname, fileName), schemaSerializationCode)
t.pass(`stored the serializer function ${fileName}`)
},
restoreFunction () {
t.fail('write mode ON')
}
})
const app = buildApp(factory)
await app.ready()
})
t.test('fastify integration - writeMode forces standalone', async t => {
t.plan(4)
const factory = FjsStandaloneCompiler({
readMode: false,
storeFunction (routeOpts, schemaSerializationCode) {
const fileName = generateFileName(routeOpts)
t.ok(routeOpts)
fs.writeFileSync(path.join(__dirname, fileName), schemaSerializationCode)
t.pass(`stored the serializer function ${fileName}`)
},
restoreFunction () {
t.fail('write mode ON')
}
})
const app = buildApp(factory, {
mode: 'not-standalone',
rounding: 'ceil'
})
await app.ready()
})
t.test('fastify integration - readMode', async t => {
t.plan(6)
const factory = FjsStandaloneCompiler({
readMode: true,
storeFunction () {
t.fail('read mode ON')
},
restoreFunction (routeOpts) {
const fileName = generateFileName(routeOpts)
t.pass(`restore the serializer function ${fileName}}`)
return require(path.join(__dirname, fileName))
}
})
const app = buildApp(factory)
await app.ready()
let res = await app.inject({
url: '/foo',
method: 'POST'
})
t.equal(res.statusCode, 200)
t.equal(res.payload, JSON.stringify({ hello: 'world' }))
res = await app.inject({
url: '/bar?lang=it',
method: 'GET'
})
t.equal(res.statusCode, 200)
t.equal(res.payload, JSON.stringify({ lang: 'en' }))
})
function buildApp (factory, serializerOpts) {
const app = fastify({
exposeHeadRoutes: false,
jsonShorthand: false,
schemaController: {
compilersFactory: {
buildSerializer: factory
}
},
serializerOpts
})
app.addSchema({
$id: 'urn:schema:foo',
type: 'object',
properties: {
name: { type: 'string' },
id: { type: 'integer' }
}
})
app.post('/foo', {
schema: {
response: {
200: {
$id: 'urn:schema:response',
type: 'object',
properties: {
hello: { $ref: 'urn:schema:foo#/properties/name' }
}
}
}
}
}, () => { return { hello: 'world' } })
app.get('/bar', {
schema: {
response: {
200: {
$id: 'urn:schema:response:bar',
type: 'object',
properties: {
lang: { type: 'string', enum: ['it', 'en'] }
}
}
}
}
}, () => { return { lang: 'en' } })
return app
}
})

View File

@@ -0,0 +1,45 @@
import { Options } from 'fast-json-stringify'
type FastJsonStringifyFactory = () => SerializerSelector.SerializerFactory
declare namespace SerializerSelector {
export type SerializerFactory = (
externalSchemas?: unknown,
options?: Options
) => SerializerCompiler;
export type SerializerCompiler = (
externalSchemas?: unknown,
options?: Options
) => Serializer;
export type Serializer = (doc: any) => string
export type RouteDefinition = {
method: string;
url: string;
httpStatus: string;
schema?: unknown;
}
export type StandaloneOptions = StandaloneOptionsReadModeOn | StandaloneOptionsReadModeOff
export type StandaloneOptionsReadModeOn = {
readMode: true;
restoreFunction?(opts: RouteDefinition): Serializer;
}
export type StandaloneOptionsReadModeOff = {
readMode?: false | undefined;
storeFunction?(opts: RouteDefinition, schemaSerializationCode: string): void;
}
export type { Options }
export const SerializerSelector: FastJsonStringifyFactory;
export function StandaloneSerializer(options: StandaloneOptions): SerializerFactory;
export { SerializerSelector as default }
}
declare function SerializerSelector(...params: Parameters<FastJsonStringifyFactory>): ReturnType<FastJsonStringifyFactory>
export = SerializerSelector

View File

@@ -0,0 +1,141 @@
import { expectAssignable, expectError, expectType } from "tsd";
import SerializerSelector, {
RouteDefinition,
Serializer,
SerializerCompiler,
SerializerFactory,
SerializerSelector as SerializerSelectorNamed,
StandaloneSerializer,
} from "..";
/**
* SerializerSelector
*/
{
const compiler = SerializerSelector();
expectType<SerializerFactory>(compiler);
}
{
const compiler = SerializerSelectorNamed();
expectType<SerializerFactory>(compiler);
}
{
{
const sampleSchema = {
$id: 'example1',
type: 'object',
properties: {
name: { type: 'string' }
}
}
const externalSchemas1 = {}
const factory = SerializerSelector()
expectType<SerializerFactory>(factory);
const compiler = factory(externalSchemas1, {})
expectType<SerializerCompiler>(compiler);
const serializeFunc = compiler({ schema: sampleSchema })
expectType<Serializer>(serializeFunc);
expectType<string>(serializeFunc({ name: 'hello' }))
}
}
/**
* StandaloneSerializer
*/
const reader = StandaloneSerializer({
readMode: true,
restoreFunction: (route: RouteDefinition) => {
expectAssignable<RouteDefinition>(route)
return {} as Serializer
},
});
expectType<SerializerFactory>(reader);
const writer = StandaloneSerializer({
readMode: false,
storeFunction: (route: RouteDefinition, code: string) => {
expectAssignable<RouteDefinition>(route)
expectAssignable<string>(code)
},
});
expectType<SerializerFactory>(writer);
{
const base = {
$id: 'urn:schema:base',
definitions: {
hello: { type: 'string' }
},
type: 'object',
properties: {
hello: { $ref: '#/definitions/hello' }
}
}
const refSchema = {
$id: 'urn:schema:ref',
type: 'object',
properties: {
hello: { $ref: 'urn:schema:base#/definitions/hello' }
}
}
const endpointSchema = {
schema: {
$id: 'urn:schema:endpoint',
$ref: 'urn:schema:ref'
}
}
const schemaMap = {
[base.$id]: base,
[refSchema.$id]: refSchema
}
expectError(StandaloneSerializer({
readMode: true,
storeFunction () { }
}))
expectError(StandaloneSerializer({
readMode: false,
restoreFunction () {}
}))
expectError(StandaloneSerializer({
restoreFunction () {}
}))
expectType<SerializerFactory>(StandaloneSerializer({
storeFunction (routeOpts, schemaSerializerCode) {
expectType<RouteDefinition>(routeOpts)
expectType<string>(schemaSerializerCode)
}
}))
expectType<SerializerFactory>(StandaloneSerializer({
readMode: true,
restoreFunction (routeOpts) {
expectType<RouteDefinition>(routeOpts)
return {} as Serializer
}
}))
const factory = StandaloneSerializer({
readMode: false,
storeFunction (routeOpts, schemaSerializerCode) {
expectType<RouteDefinition>(routeOpts)
expectType<string>(schemaSerializerCode)
}
})
expectType<SerializerFactory>(factory)
const compiler = factory(schemaMap)
expectType<SerializerCompiler>(compiler)
expectType<Serializer>(compiler(endpointSchema))
}

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')
})

Some files were not shown because too many files have changed in this diff Show More