diff --git a/.gitignore b/.gitignore index ebbf247..816692d 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,9 @@ storage/**/*.log packages/**/*.d.ts packages/**/*.js +!packages/hyper-express/**/*.js +!packages/hyper-express/**/*.d.ts + #yarn .pnp.* .yarn/* diff --git a/.husky/pre-commit b/.husky/pre-commit index d6cb288..e69de29 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1 +0,0 @@ -npm run build diff --git a/integrations/sample-app/.env.example b/integrations/sample-app/.env.example deleted file mode 100644 index d9b6322..0000000 --- a/integrations/sample-app/.env.example +++ /dev/null @@ -1,53 +0,0 @@ -APP_NAME="Intent App" -APP_PORT=5001 -APP_ENV=local -APP_URL=http://localhost:5001/ - -LOG_LEVEL=debug - -DB_DEBUG=1 -DB_HOST=127.0.0.1 -DB_PORT=5432 -DB_USER=postgres -DB_PASSWORD= -DB_DATABASE=intent_app -DB_FILENAME=intent.db - -DEFAULT_CACHE=memory -DEFAULT_QUEUE=db -DEFAULT_STORAGE=local -DEFAULT_MAILER=logger - -SENTRY_DSN= - -REDIS_HOST=127.0.0.0 -REDIS_PORT=6379 -REDIS_USERNAME= -REDIS_PASSWORD= - -QUEUE_NAME=intent-queue - -AWS_PROFILE= -AWS_REGION= -AWS_ACCESS_KEY= -AWS_SECRET_KEY= - -S3_BUCKET= - -SQS_PREFIX= -SQS_QUEUE= - -MAIL_HOST= -MAIL_PORT= -MAIL_USER= -MAIL_PASSWORD= -FROM_ADDRESS= - -MAILGUN_DOMAIN= -MAILGUN_USERNAME= -MAILGUN_API_KEY= - -RESEND_API_KEY= - - - diff --git a/integrations/sample-app/app/boot/sp/app.ts b/integrations/sample-app/app/boot/sp/app.ts index 06f914d..5587b26 100644 --- a/integrations/sample-app/app/boot/sp/app.ts +++ b/integrations/sample-app/app/boot/sp/app.ts @@ -1,9 +1,6 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ -import { - IntentApplication, - IntentApplicationContext, - ServiceProvider, -} from '@intentjs/core'; +import { IntentApplicationContext, ServiceProvider } from '@intentjs/core'; +import { IntentController } from 'app/http/controllers/icon'; import { QueueJobs } from 'app/jobs/job'; import { UserDbRepository } from 'app/repositories/userDbRepository'; import { UserService } from 'app/services'; @@ -35,5 +32,5 @@ export class AppServiceProvider extends ServiceProvider { /** * Bootstrap any application service here. */ - boot(app: IntentApplication | IntentApplicationContext) {} + boot(app: IntentApplicationContext) {} } diff --git a/integrations/sample-app/app/boot/sp/console.ts b/integrations/sample-app/app/boot/sp/console.ts index 8c9c4ac..d29b5f6 100644 --- a/integrations/sample-app/app/boot/sp/console.ts +++ b/integrations/sample-app/app/boot/sp/console.ts @@ -1,8 +1,4 @@ -import { - IntentApplication, - IntentApplicationContext, - ServiceProvider, -} from '@intentjs/core'; +import { IntentApplicationContext, ServiceProvider } from '@intentjs/core'; import { TestCacheConsoleCommand } from 'app/console/cache'; import { GreetingCommand } from 'app/console/greeting'; import { TestLogConsoleCommand } from 'app/console/log'; @@ -27,5 +23,5 @@ export class ConsoleServiceProvider extends ServiceProvider { * */ // eslint-disable-next-line @typescript-eslint/no-unused-vars - boot(app: IntentApplication | IntentApplicationContext) {} + boot(app: IntentApplicationContext) {} } diff --git a/integrations/sample-app/app/errors/filter.ts b/integrations/sample-app/app/errors/filter.ts index 274db54..2fb89f1 100644 --- a/integrations/sample-app/app/errors/filter.ts +++ b/integrations/sample-app/app/errors/filter.ts @@ -1,16 +1,16 @@ import { - Catch, + ConfigService, + ExecutionContext, HttpException, - HttpStatus, IntentExceptionFilter, - Request, - Response, Type, - ValidationFailed, } from '@intentjs/core'; -@Catch() export class ApplicationExceptionFilter extends IntentExceptionFilter { + constructor(private config: ConfigService) { + super(); + } + doNotReport(): Array> { return []; } @@ -19,19 +19,7 @@ export class ApplicationExceptionFilter extends IntentExceptionFilter { return '*'; } - handleHttp(exception: any, req: Request, res: Response) { - if (exception instanceof ValidationFailed) { - return res - .status(422) - .json({ message: 'validation failed', errors: exception.getErrors() }); - } - - return res.status(this.getStatus(exception)).json(exception); - } - - getStatus(exception: any): HttpStatus { - return exception instanceof HttpException - ? exception.getStatus() - : HttpStatus.INTERNAL_SERVER_ERROR; + handleHttp(context: ExecutionContext, exception: any) { + return super.handleHttp(context, exception); } } diff --git a/integrations/sample-app/app/http/controllers/app.ts b/integrations/sample-app/app/http/controllers/app.ts index 7d784b8..aa7910a 100644 --- a/integrations/sample-app/app/http/controllers/app.ts +++ b/integrations/sample-app/app/http/controllers/app.ts @@ -1,4 +1,4 @@ -import { Controller, Get, Req, Request } from '@intentjs/core'; +import { Controller, Get, Req } from '@intentjs/core'; import { UserService } from 'app/services'; @Controller() @@ -7,7 +7,11 @@ export class UserController { @Get() async getHello(@Req() req: Request) { - console.log(req.all()); - return this.service.getHello(); + return { hello: 'Intent' }; + } + + @Get('hello') + async getHello2(@Req() req: Request) { + return { hello: 'Intent' }; } } diff --git a/integrations/sample-app/app/http/controllers/icon.ts b/integrations/sample-app/app/http/controllers/icon.ts new file mode 100644 index 0000000..7bf5ec4 --- /dev/null +++ b/integrations/sample-app/app/http/controllers/icon.ts @@ -0,0 +1,163 @@ +import { + Accepts, + BufferBody, + Controller, + Dto, + File, + findProjectRoot, + Get, + Header, + Host, + IP, + Param, + Post, + Query, + Req, + Res, + Response, + StreamableFile, + UseGuards, + UserAgent, + Validate, +} from '@intentjs/core'; +import { CustomGuard } from '../guards/custom'; +import { Request, UploadedFile } from '@intentjs/hyper-express'; +import { createReadStream } from 'fs'; +import { join } from 'path'; +import { LoginDto } from 'app/validators/auth'; + +@Controller('/icon') +@UseGuards(CustomGuard) +export class IntentController { + public service: any; + + constructor() { + this.service = null; + } + + @Get('/:name') + @UseGuards(CustomGuard) + async getHello( + // @Req() req: Request, + // @Param('name') name: string, + // @Query() query: Record, + // @Query('b') bQuery: string, + // @Param() pathParams: string, + // @Host() hostname: string, + // @IP() ips: string, + // @Accepts() accepts: string, + // @BufferBody() bufferBody: Promise, + // @UserAgent() userAgent: string, + // @Header() headers: Record, + @Res() res: Response, + ) { + // console.log( + // 'query ==> ', + // query, + // 'bQuyery ==> ', + // bQuery, + // 'name ===> ', + // name, + // bufferBody, + // pathParams, + // 'hostname===> ', + // hostname, + // 'accepts ===> ', + // accepts, + // 'ips ===> ', + // ips, + // 'inside get method', + // 'user agent ===> ', + // userAgent, + // ); + // console.log('all headers ===> ', headers); + // throw new Error('hello there'); + // return { hello: 'world' }; + // const readStream = createReadStream( + // join(findProjectRoot(), 'storage/uploads/sample-image.jpg'), + // ); + // return new StreamableFile(readStream, { type: 'image/jpeg' }); + } + + @Get('/plain-with-query-param') + @UseGuards(CustomGuard) + async plainWithQueryParam(@Req() req: Request) { + console.log(req); + return { hello: 'world' }; + } + + @Get('/:id') + @UseGuards(CustomGuard) + async plainWithPathParam(@Req() req: Request) { + console.log(req); + return { hello: 'world' }; + } + + @Post('/json') + @Validate(LoginDto) + async postJson( + @Req() req: Request, + @Dto() dto: LoginDto, + @Param('name') name: string, + @Query() query: Record, + @Query('b') bQuery: string, + @Param() pathParams: string, + @Host() hostname: string, + @IP() ips: string, + @Accepts() accepts: string, + @BufferBody() bufferBody: Promise, + @UserAgent() userAgent: string, + @Header() headers: Record, + @File('file') file: UploadedFile, + @Res() res: Response, + ) { + // console.log( + // 'query ==> ', + // query, + // 'bQuyery ==> ', + // bQuery, + // 'name ===> ', + // name, + // bufferBody, + // pathParams, + // 'hostname===> ', + // hostname, + // 'accepts ===> ', + // accepts, + // 'ips ===> ', + // ips, + // 'inside get method', + // 'user agent ===> ', + // userAgent, + // ); + + // console.log('all headers ===> ', headers); + console.log('uploaded files ==> ', req.dto(), dto); + + const readStream = createReadStream( + join(findProjectRoot(), 'storage/uploads/sample-image.jpg'), + ); + + return new StreamableFile(readStream, { type: 'image/jpeg' }); + + return { hello: 'world from POST /json' }; + } + + @Post('/multipart') + @UseGuards(CustomGuard) + async postHello(@Req() req: Request) { + return { hello: 'world' }; + } + + @Post('/form-data') + @UseGuards(CustomGuard) + async postFormData(@Req() req: Request) { + return { hello: 'world' }; + } + + @Post('/binary') + @UseGuards(CustomGuard) + async postBinary(@Req() req: Request) { + return { hello: 'world' }; + } +} diff --git a/integrations/sample-app/app/http/decorators/custom-param.ts b/integrations/sample-app/app/http/decorators/custom-param.ts new file mode 100644 index 0000000..51973aa --- /dev/null +++ b/integrations/sample-app/app/http/decorators/custom-param.ts @@ -0,0 +1,7 @@ +import { createParamDecorator, ExecutionContext } from '@intentjs/core'; + +export const CustomParam = createParamDecorator( + (data: any, ctx: ExecutionContext, argIndex: number) => { + return 'data from custom decorator param'; + }, +); diff --git a/integrations/sample-app/app/http/guards/custom.ts b/integrations/sample-app/app/http/guards/custom.ts new file mode 100644 index 0000000..eeca0ad --- /dev/null +++ b/integrations/sample-app/app/http/guards/custom.ts @@ -0,0 +1,8 @@ +import { ExecutionContext, Injectable, IntentGuard } from '@intentjs/core'; + +@Injectable() +export class CustomGuard extends IntentGuard { + async guard(ctx: ExecutionContext): Promise { + return true; + } +} diff --git a/integrations/sample-app/app/http/guards/global.ts b/integrations/sample-app/app/http/guards/global.ts new file mode 100644 index 0000000..e7142a3 --- /dev/null +++ b/integrations/sample-app/app/http/guards/global.ts @@ -0,0 +1,8 @@ +import { ExecutionContext, Injectable, IntentGuard } from '@intentjs/core'; + +@Injectable() +export class GlobalGuard extends IntentGuard { + async guard(ctx: ExecutionContext): Promise { + return true; + } +} diff --git a/integrations/sample-app/app/http/kernel.ts b/integrations/sample-app/app/http/kernel.ts index ad0e668..b54fa38 100644 --- a/integrations/sample-app/app/http/kernel.ts +++ b/integrations/sample-app/app/http/kernel.ts @@ -1,7 +1,6 @@ import { CorsMiddleware, - HelmetMiddleware, - IntentApplication, + HttpMethods, IntentGuard, IntentMiddleware, Kernel, @@ -10,6 +9,11 @@ import { } from '@intentjs/core'; import { UserController } from './controllers/app'; import { AuthController } from './controllers/auth'; +import { SampleMiddleware } from './middlewares/sample'; +import { IntentController } from './controllers/icon'; +import { GlobalMiddleware } from './middlewares/global'; +import { Server } from '@intentjs/hyper-express'; +import { GlobalGuard } from './guards/global'; export class HttpKernel extends Kernel { /** @@ -17,7 +21,7 @@ export class HttpKernel extends Kernel { * Read more - https://tryintent.com/docs/controllers */ public controllers(): Type[] { - return [UserController, AuthController]; + return [UserController, AuthController, IntentController]; } /** @@ -28,7 +32,7 @@ export class HttpKernel extends Kernel { * Read more - https://tryintent.com/docs/middlewares */ public middlewares(): Type[] { - return [CorsMiddleware, HelmetMiddleware]; + return [GlobalMiddleware, CorsMiddleware]; } /** @@ -39,7 +43,15 @@ export class HttpKernel extends Kernel { * Read more - https://tryintent.com/docs/middlewares */ public routeMiddlewares(configurator: MiddlewareConfigurator) { - return; + configurator + .use(SampleMiddleware) + .for({ path: '/icon/sample', method: HttpMethods.POST }) + .for(IntentController); + // .exclude('/icon/:name'); + + configurator.use(GlobalMiddleware).exclude('/icon/:name'); + + configurator.use(SampleMiddleware).for('/icon/plain'); } /** @@ -50,13 +62,12 @@ export class HttpKernel extends Kernel { * Read more - https://tryintent.com/docs/guards */ public guards(): Type[] { - return []; + return [GlobalGuard]; } /** - * @param app + * @param server */ - public async boot(app: IntentApplication): Promise { - app.disable('x-powered-by'); - } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public async boot(server: Server): Promise {} } diff --git a/integrations/sample-app/app/http/middlewares/global.ts b/integrations/sample-app/app/http/middlewares/global.ts new file mode 100644 index 0000000..f050e2e --- /dev/null +++ b/integrations/sample-app/app/http/middlewares/global.ts @@ -0,0 +1,13 @@ +import { ConfigService, Injectable, IntentMiddleware } from '@intentjs/core'; +import { MiddlewareNext, Request, Response } from '@intentjs/hyper-express'; + +@Injectable() +export class GlobalMiddleware extends IntentMiddleware { + constructor(private readonly config: ConfigService) { + super(); + } + + use(req: Request, res: Response, next: MiddlewareNext): void { + next(); + } +} diff --git a/integrations/sample-app/app/http/middlewares/sample.ts b/integrations/sample-app/app/http/middlewares/sample.ts new file mode 100644 index 0000000..db13f44 --- /dev/null +++ b/integrations/sample-app/app/http/middlewares/sample.ts @@ -0,0 +1,9 @@ +import { IntentMiddleware, MiddlewareNext } from '@intentjs/core'; +import { Request, Response } from '@intentjs/hyper-express'; + +export class SampleMiddleware extends IntentMiddleware { + use(req: Request, res: Response, next: MiddlewareNext): void | Promise { + // console.log(req.isHttp(), req.httpHost(), req.all(), req.bearerToken()); + next(); + } +} diff --git a/integrations/sample-app/app/main.ts b/integrations/sample-app/app/main.ts index 2191f6d..dcb76c8 100644 --- a/integrations/sample-app/app/main.ts +++ b/integrations/sample-app/app/main.ts @@ -3,8 +3,12 @@ import { ApplicationContainer } from './boot/container'; import { ApplicationExceptionFilter } from './errors/filter'; import { IntentHttpServer } from '@intentjs/core'; -IntentHttpServer.init() - .useContainer(ApplicationContainer) - .useKernel(HttpKernel) - .handleErrorsWith(ApplicationExceptionFilter) - .start(); +const server = IntentHttpServer.init(); + +server.useContainer(ApplicationContainer); + +server.useKernel(HttpKernel); + +server.handleErrorsWith(ApplicationExceptionFilter); + +server.start(); diff --git a/integrations/sample-app/config/app.ts b/integrations/sample-app/config/app.ts index f83ffa3..81ee3ff 100644 --- a/integrations/sample-app/config/app.ts +++ b/integrations/sample-app/config/app.ts @@ -1,11 +1,11 @@ import { AppConfig, - registerNamespace, + configNamespace, toBoolean, ValidationErrorSerializer, } from '@intentjs/core'; -export default registerNamespace( +export default configNamespace( 'app', (): AppConfig => ({ /** @@ -69,16 +69,6 @@ export default registerNamespace( */ port: +process.env.APP_PORT || 5000, - /** - * ----------------------------------------------------- - * Cross Origin Resource Sharing - * ----------------------------------------------------- - * - * You can use this setting to define the CORS rule of - * your application. - */ - cors: { origin: true }, - error: { /** * ----------------------------------------------------- diff --git a/integrations/sample-app/config/auth.ts b/integrations/sample-app/config/auth.ts index 1f3d070..87d7172 100644 --- a/integrations/sample-app/config/auth.ts +++ b/integrations/sample-app/config/auth.ts @@ -1,6 +1,6 @@ -import { registerNamespace } from '@intentjs/core'; +import { configNamespace } from '@intentjs/core'; -export default registerNamespace('auth', () => ({ +export default configNamespace('auth', () => ({ /** * ----------------------------------------------------- * JWT SECRET diff --git a/integrations/sample-app/config/cache.ts b/integrations/sample-app/config/cache.ts index f9cf6fd..1092ec5 100644 --- a/integrations/sample-app/config/cache.ts +++ b/integrations/sample-app/config/cache.ts @@ -1,6 +1,6 @@ -import { CacheOptions, registerNamespace } from '@intentjs/core'; +import { CacheOptions, configNamespace } from '@intentjs/core'; -export default registerNamespace( +export default configNamespace( 'cache', (): CacheOptions => ({ /** diff --git a/integrations/sample-app/config/database.ts b/integrations/sample-app/config/database.ts index c108494..851acdb 100644 --- a/integrations/sample-app/config/database.ts +++ b/integrations/sample-app/config/database.ts @@ -1,7 +1,7 @@ -import { DatabaseOptions, registerNamespace } from '@intentjs/core'; +import { DatabaseOptions, configNamespace } from '@intentjs/core'; import { knexSnakeCaseMappers } from 'objection'; -export default registerNamespace( +export default configNamespace( 'db', (): DatabaseOptions => ({ isGlobal: true, diff --git a/integrations/sample-app/config/http.ts b/integrations/sample-app/config/http.ts new file mode 100644 index 0000000..e6c889c --- /dev/null +++ b/integrations/sample-app/config/http.ts @@ -0,0 +1,31 @@ +import { findProjectRoot, HttpConfig, configNamespace } from '@intentjs/core'; +import { join } from 'path'; + +export default configNamespace( + 'http', + (): HttpConfig => ({ + cors: { + origin: true, + methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], + allowedHeaders: ['Content-Type', 'Authorization'], + credentials: true, + }, + + server: { + max_body_buffer: 100000000, + max_body_length: 100000000, + }, + + staticServe: { + httpPath: 'assets', + filePath: join(findProjectRoot(), 'storage/uploads'), + keep: { + extensions: ['css', 'js', 'json', 'png', 'jpg', 'jpeg'], + }, + cache: { + max_file_count: 1000, + max_file_size: 4 * 1024 * 1024, + }, + }, + }), +); diff --git a/integrations/sample-app/config/index.ts b/integrations/sample-app/config/index.ts index 20db8d2..5ee538f 100644 --- a/integrations/sample-app/config/index.ts +++ b/integrations/sample-app/config/index.ts @@ -7,8 +7,10 @@ import mailer from './mailer'; import database from './database'; import cache from './cache'; import queue from './queue'; +import http from './http'; export default [ + http, app, auth, cache, diff --git a/integrations/sample-app/config/localization.ts b/integrations/sample-app/config/localization.ts index 86e9ed8..1c67111 100644 --- a/integrations/sample-app/config/localization.ts +++ b/integrations/sample-app/config/localization.ts @@ -1,11 +1,11 @@ import { findProjectRoot, LocalizationOptions, - registerNamespace, + configNamespace, } from '@intentjs/core'; import { join } from 'path'; -export default registerNamespace( +export default configNamespace( 'localization', (): LocalizationOptions => ({ /** diff --git a/integrations/sample-app/config/logger.ts b/integrations/sample-app/config/logger.ts index 573e4b2..7ad96d8 100644 --- a/integrations/sample-app/config/logger.ts +++ b/integrations/sample-app/config/logger.ts @@ -2,12 +2,12 @@ import { Formats, IntentLoggerOptions, LogLevel, - registerNamespace, + configNamespace, toBoolean, Transports, } from '@intentjs/core'; -export default registerNamespace( +export default configNamespace( 'logger', (): IntentLoggerOptions => ({ /** diff --git a/integrations/sample-app/config/mailer.ts b/integrations/sample-app/config/mailer.ts index 69f3a0f..eec5c5d 100644 --- a/integrations/sample-app/config/mailer.ts +++ b/integrations/sample-app/config/mailer.ts @@ -1,6 +1,6 @@ -import { MailerOptions, registerNamespace } from '@intentjs/core'; +import { MailerOptions, configNamespace } from '@intentjs/core'; -export default registerNamespace( +export default configNamespace( 'mailers', (): MailerOptions => ({ /** diff --git a/integrations/sample-app/config/queue.ts b/integrations/sample-app/config/queue.ts index 3f9b357..8870733 100644 --- a/integrations/sample-app/config/queue.ts +++ b/integrations/sample-app/config/queue.ts @@ -1,6 +1,6 @@ -import { QueueOptions, registerNamespace } from '@intentjs/core'; +import { QueueOptions, configNamespace } from '@intentjs/core'; -export default registerNamespace('queue', (): QueueOptions => { +export default configNamespace('queue', (): QueueOptions => { return { /** * ----------------------------------------------------- diff --git a/integrations/sample-app/config/storage.ts b/integrations/sample-app/config/storage.ts index a83952c..6ae33a7 100644 --- a/integrations/sample-app/config/storage.ts +++ b/integrations/sample-app/config/storage.ts @@ -1,12 +1,12 @@ import { fromIni } from '@aws-sdk/credential-providers'; import { findProjectRoot, - registerNamespace, + configNamespace, StorageOptions, } from '@intentjs/core'; import { join } from 'path'; -export default registerNamespace( +export default configNamespace( 'filesystem', (): StorageOptions => ({ diff --git a/package-lock.json b/package-lock.json index 7d66b91..54240f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,9 @@ "integrations/*" ], "dependencies": { - "concurrently": "^9.1.0" + "concurrently": "^9.1.0", + "hyper-express": "^6.17.2", + "uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.48.0" }, "devDependencies": { "@commitlint/cli": "^19.5.0", @@ -80,8 +82,6 @@ }, "node_modules/@aws-crypto/crc32": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", - "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/util": "^5.2.0", @@ -94,8 +94,6 @@ }, "node_modules/@aws-crypto/crc32c": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/crc32c/-/crc32c-5.2.0.tgz", - "integrity": "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/util": "^5.2.0", @@ -105,8 +103,6 @@ }, "node_modules/@aws-crypto/sha1-browser": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/sha1-browser/-/sha1-browser-5.2.0.tgz", - "integrity": "sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/supports-web-crypto": "^5.2.0", @@ -119,8 +115,6 @@ }, "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/is-array-buffer": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", - "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -131,8 +125,6 @@ }, "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-buffer-from": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", - "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", "license": "Apache-2.0", "dependencies": { "@smithy/is-array-buffer": "^2.2.0", @@ -144,8 +136,6 @@ }, "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-utf8": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", - "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", "license": "Apache-2.0", "dependencies": { "@smithy/util-buffer-from": "^2.2.0", @@ -157,8 +147,6 @@ }, "node_modules/@aws-crypto/sha256-browser": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", - "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-js": "^5.2.0", @@ -172,8 +160,6 @@ }, "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", - "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -184,8 +170,6 @@ }, "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", - "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", "license": "Apache-2.0", "dependencies": { "@smithy/is-array-buffer": "^2.2.0", @@ -197,8 +181,6 @@ }, "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", - "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", "license": "Apache-2.0", "dependencies": { "@smithy/util-buffer-from": "^2.2.0", @@ -210,8 +192,6 @@ }, "node_modules/@aws-crypto/sha256-js": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", - "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/util": "^5.2.0", @@ -224,8 +204,6 @@ }, "node_modules/@aws-crypto/supports-web-crypto": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", - "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -233,8 +211,6 @@ }, "node_modules/@aws-crypto/util": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", - "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.222.0", @@ -244,8 +220,6 @@ }, "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", - "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -256,8 +230,6 @@ }, "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", - "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", "license": "Apache-2.0", "dependencies": { "@smithy/is-array-buffer": "^2.2.0", @@ -269,8 +241,6 @@ }, "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", - "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", "license": "Apache-2.0", "dependencies": { "@smithy/util-buffer-from": "^2.2.0", @@ -282,8 +252,6 @@ }, "node_modules/@aws-sdk/client-cognito-identity": { "version": "3.687.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-cognito-identity/-/client-cognito-identity-3.687.0.tgz", - "integrity": "sha512-jcQTioloSed+Jc3snjrgpWejkOm8t3Zt+jWrApw3ejN8qBtpFCH43M7q/CSDVZ9RS1IjX+KRWoBFnrDOnbuw0Q==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", @@ -334,8 +302,6 @@ }, "node_modules/@aws-sdk/client-s3": { "version": "3.689.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.689.0.tgz", - "integrity": "sha512-qYD1GJEPeLM6H3x8BuAAMXZltvVce5vGiwtZc9uMkBBo3HyFnmPitIPTPfaD1q8LOn/7KFdkY4MJ4e8D3YpV9g==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", @@ -403,8 +369,6 @@ }, "node_modules/@aws-sdk/client-sso": { "version": "3.687.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.687.0.tgz", - "integrity": "sha512-dfj0y9fQyX4kFill/ZG0BqBTLQILKlL7+O5M4F9xlsh2WNuV2St6WtcOg14Y1j5UODPJiJs//pO+mD1lihT5Kw==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", @@ -452,8 +416,6 @@ }, "node_modules/@aws-sdk/client-sso-oidc": { "version": "3.687.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.687.0.tgz", - "integrity": "sha512-Rdd8kLeTeh+L5ZuG4WQnWgYgdv7NorytKdZsGjiag1D8Wv3PcJvPqqWdgnI0Og717BSXVoaTYaN34FyqFYSx6Q==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", @@ -505,8 +467,6 @@ }, "node_modules/@aws-sdk/client-sts": { "version": "3.687.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.687.0.tgz", - "integrity": "sha512-SQjDH8O4XCTtouuCVYggB0cCCrIaTzUZIkgJUpOsIEJBLlTbNOb/BZqUShAQw2o9vxr2rCeOGjAQOYPysW/Pmg==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", @@ -556,8 +516,6 @@ }, "node_modules/@aws-sdk/core": { "version": "3.686.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.686.0.tgz", - "integrity": "sha512-Xt3DV4DnAT3v2WURwzTxWQK34Ew+iiLzoUoguvLaZrVMFOqMMrwVjP+sizqIaHp1j7rGmFcN5I8saXnsDLuQLA==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "3.686.0", @@ -578,8 +536,6 @@ }, "node_modules/@aws-sdk/credential-provider-cognito-identity": { "version": "3.687.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-cognito-identity/-/credential-provider-cognito-identity-3.687.0.tgz", - "integrity": "sha512-hJq9ytoj2q/Jonc7mox/b0HT+j4NeMRuU184DkXRJbvIvwwB+oMt12221kThLezMhwIYfXEteZ7GEId7Hn8Y8g==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/client-cognito-identity": "3.687.0", @@ -594,8 +550,6 @@ }, "node_modules/@aws-sdk/credential-provider-env": { "version": "3.686.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.686.0.tgz", - "integrity": "sha512-osD7lPO8OREkgxPiTWmA1i6XEmOth1uW9HWWj/+A2YGCj1G/t2sHu931w4Qj9NWHYZtbTTXQYVRg+TErALV7nQ==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "3.686.0", @@ -610,8 +564,6 @@ }, "node_modules/@aws-sdk/credential-provider-http": { "version": "3.686.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.686.0.tgz", - "integrity": "sha512-xyGAD/f3vR/wssUiZrNFWQWXZvI4zRm2wpHhoHA1cC2fbRMNFYtFn365yw6dU7l00ZLcdFB1H119AYIUZS7xbw==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "3.686.0", @@ -631,8 +583,6 @@ }, "node_modules/@aws-sdk/credential-provider-ini": { "version": "3.687.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.687.0.tgz", - "integrity": "sha512-6d5ZJeZch+ZosJccksN0PuXv7OSnYEmanGCnbhUqmUSz9uaVX6knZZfHCZJRgNcfSqg9QC0zsFA/51W5HCUqSQ==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "3.686.0", @@ -657,8 +607,6 @@ }, "node_modules/@aws-sdk/credential-provider-node": { "version": "3.687.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.687.0.tgz", - "integrity": "sha512-Pqld8Nx11NYaBUrVk3bYiGGpLCxkz8iTONlpQWoVWFhSOzlO7zloNOaYbD2XgFjjqhjlKzE91drs/f41uGeCTA==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/credential-provider-env": "3.686.0", @@ -680,8 +628,6 @@ }, "node_modules/@aws-sdk/credential-provider-process": { "version": "3.686.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.686.0.tgz", - "integrity": "sha512-sXqaAgyzMOc+dm4CnzAR5Q6S9OWVHyZjLfW6IQkmGjqeQXmZl24c4E82+w64C+CTkJrFLzH1VNOYp1Hy5gE6Qw==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "3.686.0", @@ -697,8 +643,6 @@ }, "node_modules/@aws-sdk/credential-provider-sso": { "version": "3.687.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.687.0.tgz", - "integrity": "sha512-N1YCoE7DovIRF2ReyRrA4PZzF0WNi4ObPwdQQkVxhvSm7PwjbWxrfq7rpYB+6YB1Uq3QPzgVwUFONE36rdpxUQ==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/client-sso": "3.687.0", @@ -716,8 +660,6 @@ }, "node_modules/@aws-sdk/credential-provider-web-identity": { "version": "3.686.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.686.0.tgz", - "integrity": "sha512-40UqCpPxyHCXDP7CGd9JIOZDgDZf+u1OyLaGBpjQJlz1HYuEsIWnnbTe29Yg3Ah/Zc3g4NBWcUdlGVotlnpnDg==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "3.686.0", @@ -735,8 +677,6 @@ }, "node_modules/@aws-sdk/credential-providers": { "version": "3.687.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-providers/-/credential-providers-3.687.0.tgz", - "integrity": "sha512-3aKlmKaOplpanOycmoigbTrQsqtxpzhpfquCey51aHf9GYp2yYyYF1YOgkXpE3qm3w6eiEN1asjJ2gqoECUuPA==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/client-cognito-identity": "3.687.0", @@ -763,8 +703,6 @@ }, "node_modules/@aws-sdk/middleware-bucket-endpoint": { "version": "3.686.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.686.0.tgz", - "integrity": "sha512-6qCoWI73/HDzQE745MHQUYz46cAQxHCgy1You8MZQX9vHAQwqBnkcsb2hGp7S6fnQY5bNsiZkMWVQ/LVd2MNjg==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "3.686.0", @@ -781,8 +719,6 @@ }, "node_modules/@aws-sdk/middleware-expect-continue": { "version": "3.686.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.686.0.tgz", - "integrity": "sha512-5yYqIbyhLhH29vn4sHiTj7sU6GttvLMk3XwCmBXjo2k2j3zHqFUwh9RyFGF9VY6Z392Drf/E/cl+qOGypwULpg==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "3.686.0", @@ -796,8 +732,6 @@ }, "node_modules/@aws-sdk/middleware-flexible-checksums": { "version": "3.689.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.689.0.tgz", - "integrity": "sha512-6VxMOf3mgmAgg6SMagwKj5pAe+putcx2F2odOAWviLcobFpdM/xK9vNry7p6kY+RDNmSlBvcji9wnU59fjV74Q==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/crc32": "5.2.0", @@ -820,8 +754,6 @@ }, "node_modules/@aws-sdk/middleware-host-header": { "version": "3.686.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.686.0.tgz", - "integrity": "sha512-+Yc6rO02z+yhFbHmRZGvEw1vmzf/ifS9a4aBjJGeVVU+ZxaUvnk+IUZWrj4YQopUQ+bSujmMUzJLXSkbDq7yuw==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "3.686.0", @@ -835,8 +767,6 @@ }, "node_modules/@aws-sdk/middleware-location-constraint": { "version": "3.686.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.686.0.tgz", - "integrity": "sha512-pCLeZzt5zUGY3NbW4J/5x3kaHyJEji4yqtoQcUlJmkoEInhSxJ0OE8sTxAfyL3nIOF4yr6L2xdaLCqYgQT8Aog==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "3.686.0", @@ -849,8 +779,6 @@ }, "node_modules/@aws-sdk/middleware-logger": { "version": "3.686.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.686.0.tgz", - "integrity": "sha512-cX43ODfA2+SPdX7VRxu6gXk4t4bdVJ9pkktbfnkE5t27OlwNfvSGGhnHrQL8xTOFeyQ+3T+oowf26gf1OI+vIg==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "3.686.0", @@ -863,8 +791,6 @@ }, "node_modules/@aws-sdk/middleware-recursion-detection": { "version": "3.686.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.686.0.tgz", - "integrity": "sha512-jF9hQ162xLgp9zZ/3w5RUNhmwVnXDBlABEUX8jCgzaFpaa742qR/KKtjjZQ6jMbQnP+8fOCSXFAVNMU+s6v81w==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "3.686.0", @@ -878,8 +804,6 @@ }, "node_modules/@aws-sdk/middleware-sdk-s3": { "version": "3.687.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.687.0.tgz", - "integrity": "sha512-YGHYqiyRiNNucmvLrfx3QxIkjSDWR/+cc72bn0lPvqFUQBRHZgmYQLxVYrVZSmRzzkH2FQ1HsZcXhOafLbq4vQ==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "3.686.0", @@ -903,8 +827,6 @@ }, "node_modules/@aws-sdk/middleware-ssec": { "version": "3.686.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.686.0.tgz", - "integrity": "sha512-zJXml/CpVHFUdlGQqja87vNQ3rPB5SlDbfdwxlj1KBbjnRRwpBtxxmOlWRShg8lnVV6aIMGv95QmpIFy4ayqnQ==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "3.686.0", @@ -917,8 +839,6 @@ }, "node_modules/@aws-sdk/middleware-user-agent": { "version": "3.687.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.687.0.tgz", - "integrity": "sha512-nUgsKiEinyA50CaDXojAkOasAU3Apdg7Qox6IjNUC4ZjgOu7QWsCDB5N28AYMUt06cNYeYQdfMX1aEzG85a1Mg==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "3.686.0", @@ -935,8 +855,6 @@ }, "node_modules/@aws-sdk/region-config-resolver": { "version": "3.686.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.686.0.tgz", - "integrity": "sha512-6zXD3bSD8tcsMAVVwO1gO7rI1uy2fCD3czgawuPGPopeLiPpo6/3FoUWCQzk2nvEhj7p9Z4BbjwZGSlRkVrXTw==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "3.686.0", @@ -952,8 +870,6 @@ }, "node_modules/@aws-sdk/s3-request-presigner": { "version": "3.689.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.689.0.tgz", - "integrity": "sha512-E9P59HEsPeFuO10yKyYE180J3V1DRVFTa0H0XzrBTP+s2g9g8xvfyGqoDYJw5YHUckqls39jT5nlbrf+kBSrfg==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/signature-v4-multi-region": "3.687.0", @@ -971,8 +887,6 @@ }, "node_modules/@aws-sdk/signature-v4-multi-region": { "version": "3.687.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.687.0.tgz", - "integrity": "sha512-vdOQHCRHJPX9mT8BM6xOseazHD6NodvHl9cyF5UjNtLn+gERRJEItIA9hf0hlt62odGD8Fqp+rFRuqdmbNkcNw==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/middleware-sdk-s3": "3.687.0", @@ -988,8 +902,6 @@ }, "node_modules/@aws-sdk/token-providers": { "version": "3.686.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.686.0.tgz", - "integrity": "sha512-9oL4kTCSePFmyKPskibeiOXV6qavPZ63/kXM9Wh9V6dTSvBtLeNnMxqGvENGKJcTdIgtoqyqA6ET9u0PJ5IRIg==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "3.686.0", @@ -1007,8 +919,6 @@ }, "node_modules/@aws-sdk/types": { "version": "3.686.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.686.0.tgz", - "integrity": "sha512-xFnrb3wxOoJcW2Xrh63ZgFo5buIu9DF7bOHnwoUxHdNpUXicUh0AHw85TjXxyxIAd0d1psY/DU7QHoNI3OswgQ==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^3.6.0", @@ -1020,8 +930,6 @@ }, "node_modules/@aws-sdk/util-arn-parser": { "version": "3.679.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.679.0.tgz", - "integrity": "sha512-CwzEbU8R8rq9bqUFryO50RFBlkfufV9UfMArHPWlo+lmsC+NlSluHQALoj6Jkq3zf5ppn1CN0c1DDLrEqdQUXg==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -1032,8 +940,6 @@ }, "node_modules/@aws-sdk/util-endpoints": { "version": "3.686.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.686.0.tgz", - "integrity": "sha512-7msZE2oYl+6QYeeRBjlDgxQUhq/XRky3cXE0FqLFs2muLS7XSuQEXkpOXB3R782ygAP6JX0kmBxPTLurRTikZg==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "3.686.0", @@ -1047,8 +953,6 @@ }, "node_modules/@aws-sdk/util-format-url": { "version": "3.686.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-format-url/-/util-format-url-3.686.0.tgz", - "integrity": "sha512-9doB6O4FAlnWZrvnFDUxTtSFtuL8kUqxlP00HTiDgL1uDJZ8e0S4gqjKR+9+N5goFtxGi7IJeNsDEz2H7imvgw==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "3.686.0", @@ -1062,8 +966,6 @@ }, "node_modules/@aws-sdk/util-locate-window": { "version": "3.679.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.679.0.tgz", - "integrity": "sha512-zKTd48/ZWrCplkXpYDABI74rQlbR0DNHs8nH95htfSLj9/mWRSwaGptoxwcihaq/77vi/fl2X3y0a1Bo8bt7RA==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -1074,8 +976,6 @@ }, "node_modules/@aws-sdk/util-user-agent-browser": { "version": "3.686.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.686.0.tgz", - "integrity": "sha512-YiQXeGYZegF1b7B2GOR61orhgv79qmI0z7+Agm3NXLO6hGfVV3kFUJbXnjtH1BgWo5hbZYW7HQ2omGb3dnb6Lg==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "3.686.0", @@ -1086,8 +986,6 @@ }, "node_modules/@aws-sdk/util-user-agent-node": { "version": "3.687.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.687.0.tgz", - "integrity": "sha512-idkP6ojSTZ4ek1pJ8wIN7r9U3KR5dn0IkJn3KQBXQ58LWjkRqLtft2vxzdsktWwhPKjjmIKl1S0kbvqLawf8XQ==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/middleware-user-agent": "3.687.0", @@ -1110,8 +1008,6 @@ }, "node_modules/@aws-sdk/xml-builder": { "version": "3.686.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.686.0.tgz", - "integrity": "sha512-k0z5b5dkYSuOHY0AOZ4iyjcGBeVL9lWsQNF4+c+1oK3OW4fRWl/bNa1soMRMpangsHPzgyn/QkzuDbl7qR4qrw==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^3.6.0", @@ -2239,6 +2135,10 @@ "resolved": "packages/core", "link": true }, + "node_modules/@intentjs/hyper-express": { + "resolved": "packages/hyper-express", + "link": true + }, "node_modules/@ioredis/commands": { "version": "1.2.0", "license": "MIT" @@ -3038,7 +2938,9 @@ } }, "node_modules/@nestjs/common": { - "version": "10.4.7", + "version": "10.4.8", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.4.8.tgz", + "integrity": "sha512-PVor9dxihg3F2LMnVNkQu42vUmea2+qukkWXUSumtVKDsBo7X7jnZWXtF5bvNTcYK7IYL4/MM4awNfJVJcJpFg==", "license": "MIT", "dependencies": { "iterare": "1.2.1", @@ -3069,7 +2971,9 @@ "license": "0BSD" }, "node_modules/@nestjs/core": { - "version": "10.4.7", + "version": "10.4.8", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.4.8.tgz", + "integrity": "sha512-Kdi9rDZdlCkGL2AK9XuJ24bZp2YFV6dWBdogGsAHSP5u95wfnSkhduxHZy6q/i1nFFiLASUHabL8Jwr+bmD22Q==", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -3111,6 +3015,8 @@ "node_modules/@nestjs/platform-express": { "version": "10.4.7", "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "body-parser": "1.20.3", "cors": "2.8.5", @@ -3129,7 +3035,9 @@ }, "node_modules/@nestjs/platform-express/node_modules/tslib": { "version": "2.7.0", - "license": "0BSD" + "license": "0BSD", + "optional": true, + "peer": true }, "node_modules/@nestjs/testing": { "version": "10.4.7", @@ -4097,8 +4005,6 @@ }, "node_modules/@smithy/abort-controller": { "version": "3.1.6", - "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-3.1.6.tgz", - "integrity": "sha512-0XuhuHQlEqbNQZp7QxxrFTdVWdwxch4vjxYgfInF91hZFkPxf9QDrdQka0KfxFMPqLNzSw0b95uGTrLliQUavQ==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^3.6.0", @@ -4110,8 +4016,6 @@ }, "node_modules/@smithy/chunked-blob-reader": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader/-/chunked-blob-reader-4.0.0.tgz", - "integrity": "sha512-jSqRnZvkT4egkq/7b6/QRCNXmmYVcHwnJldqJ3IhVpQE2atObVJ137xmGeuGFhjFUr8gCEVAOKwSY79OvpbDaQ==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -4119,8 +4023,6 @@ }, "node_modules/@smithy/chunked-blob-reader-native": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-3.0.1.tgz", - "integrity": "sha512-VEYtPvh5rs/xlyqpm5NRnfYLZn+q0SRPELbvBV+C/G7IQ+ouTuo+NKKa3ShG5OaFR8NYVMXls9hPYLTvIKKDrQ==", "license": "Apache-2.0", "dependencies": { "@smithy/util-base64": "^3.0.0", @@ -4129,8 +4031,6 @@ }, "node_modules/@smithy/config-resolver": { "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-3.0.10.tgz", - "integrity": "sha512-Uh0Sz9gdUuz538nvkPiyv1DZRX9+D15EKDtnQP5rYVAzM/dnYk3P8cg73jcxyOitPgT3mE3OVj7ky7sibzHWkw==", "license": "Apache-2.0", "dependencies": { "@smithy/node-config-provider": "^3.1.9", @@ -4145,8 +4045,6 @@ }, "node_modules/@smithy/core": { "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-2.5.1.tgz", - "integrity": "sha512-DujtuDA7BGEKExJ05W5OdxCoyekcKT3Rhg1ZGeiUWaz2BJIWXjZmsG/DIP4W48GHno7AQwRsaCb8NcBgH3QZpg==", "license": "Apache-2.0", "dependencies": { "@smithy/middleware-serde": "^3.0.8", @@ -4164,8 +4062,6 @@ }, "node_modules/@smithy/credential-provider-imds": { "version": "3.2.5", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-3.2.5.tgz", - "integrity": "sha512-4FTQGAsuwqTzVMmiRVTn0RR9GrbRfkP0wfu/tXWVHd2LgNpTY0uglQpIScXK4NaEyXbB3JmZt8gfVqO50lP8wg==", "license": "Apache-2.0", "dependencies": { "@smithy/node-config-provider": "^3.1.9", @@ -4180,8 +4076,6 @@ }, "node_modules/@smithy/eventstream-codec": { "version": "3.1.7", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-3.1.7.tgz", - "integrity": "sha512-kVSXScIiRN7q+s1x7BrQtZ1Aa9hvvP9FeCqCdBxv37GimIHgBCOnZ5Ip80HLt0DhnAKpiobFdGqTFgbaJNrazA==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/crc32": "5.2.0", @@ -4192,8 +4086,6 @@ }, "node_modules/@smithy/eventstream-serde-browser": { "version": "3.0.11", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-3.0.11.tgz", - "integrity": "sha512-Pd1Wnq3CQ/v2SxRifDUihvpXzirJYbbtXfEnnLV/z0OGCTx/btVX74P86IgrZkjOydOASBGXdPpupYQI+iO/6A==", "license": "Apache-2.0", "dependencies": { "@smithy/eventstream-serde-universal": "^3.0.10", @@ -4206,8 +4098,6 @@ }, "node_modules/@smithy/eventstream-serde-config-resolver": { "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-3.0.8.tgz", - "integrity": "sha512-zkFIG2i1BLbfoGQnf1qEeMqX0h5qAznzaZmMVNnvPZz9J5AWBPkOMckZWPedGUPcVITacwIdQXoPcdIQq5FRcg==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^3.6.0", @@ -4219,8 +4109,6 @@ }, "node_modules/@smithy/eventstream-serde-node": { "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-3.0.10.tgz", - "integrity": "sha512-hjpU1tIsJ9qpcoZq9zGHBJPBOeBGYt+n8vfhDwnITPhEre6APrvqq/y3XMDEGUT2cWQ4ramNqBPRbx3qn55rhw==", "license": "Apache-2.0", "dependencies": { "@smithy/eventstream-serde-universal": "^3.0.10", @@ -4233,8 +4121,6 @@ }, "node_modules/@smithy/eventstream-serde-universal": { "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-3.0.10.tgz", - "integrity": "sha512-ewG1GHbbqsFZ4asaq40KmxCmXO+AFSM1b+DcO2C03dyJj/ZH71CiTg853FSE/3SHK9q3jiYQIFjlGSwfxQ9kww==", "license": "Apache-2.0", "dependencies": { "@smithy/eventstream-codec": "^3.1.7", @@ -4247,8 +4133,6 @@ }, "node_modules/@smithy/fetch-http-handler": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-4.0.0.tgz", - "integrity": "sha512-MLb1f5tbBO2X6K4lMEKJvxeLooyg7guq48C2zKr4qM7F2Gpkz4dc+hdSgu77pCJ76jVqFBjZczHYAs6dp15N+g==", "license": "Apache-2.0", "dependencies": { "@smithy/protocol-http": "^4.1.5", @@ -4260,8 +4144,6 @@ }, "node_modules/@smithy/hash-blob-browser": { "version": "3.1.7", - "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-3.1.7.tgz", - "integrity": "sha512-4yNlxVNJifPM5ThaA5HKnHkn7JhctFUHvcaz6YXxHlYOSIrzI6VKQPTN8Gs1iN5nqq9iFcwIR9THqchUCouIfg==", "license": "Apache-2.0", "dependencies": { "@smithy/chunked-blob-reader": "^4.0.0", @@ -4272,8 +4154,6 @@ }, "node_modules/@smithy/hash-node": { "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-3.0.8.tgz", - "integrity": "sha512-tlNQYbfpWXHimHqrvgo14DrMAgUBua/cNoz9fMYcDmYej7MAmUcjav/QKQbFc3NrcPxeJ7QClER4tWZmfwoPng==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^3.6.0", @@ -4287,8 +4167,6 @@ }, "node_modules/@smithy/hash-stream-node": { "version": "3.1.7", - "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-3.1.7.tgz", - "integrity": "sha512-xMAsvJ3hLG63lsBVi1Hl6BBSfhd8/Qnp8fC06kjOpJvyyCEXdwHITa5Kvdsk6gaAXLhbZMhQMIGvgUbfnJDP6Q==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^3.6.0", @@ -4301,8 +4179,6 @@ }, "node_modules/@smithy/invalid-dependency": { "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-3.0.8.tgz", - "integrity": "sha512-7Qynk6NWtTQhnGTTZwks++nJhQ1O54Mzi7fz4PqZOiYXb4Z1Flpb2yRvdALoggTS8xjtohWUM+RygOtB30YL3Q==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^3.6.0", @@ -4311,8 +4187,6 @@ }, "node_modules/@smithy/is-array-buffer": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-3.0.0.tgz", - "integrity": "sha512-+Fsu6Q6C4RSJiy81Y8eApjEB5gVtM+oFKTffg+jSuwtvomJJrhUJBu2zS8wjXSgH/g1MKEWrzyChTBe6clb5FQ==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -4323,8 +4197,6 @@ }, "node_modules/@smithy/md5-js": { "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-3.0.8.tgz", - "integrity": "sha512-LwApfTK0OJ/tCyNUXqnWCKoE2b4rDSr4BJlDAVCkiWYeHESr+y+d5zlAanuLW6fnitVJRD/7d9/kN/ZM9Su4mA==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^3.6.0", @@ -4334,8 +4206,6 @@ }, "node_modules/@smithy/middleware-content-length": { "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-3.0.10.tgz", - "integrity": "sha512-T4dIdCs1d/+/qMpwhJ1DzOhxCZjZHbHazEPJWdB4GDi2HjIZllVzeBEcdJUN0fomV8DURsgOyrbEUzg3vzTaOg==", "license": "Apache-2.0", "dependencies": { "@smithy/protocol-http": "^4.1.5", @@ -4348,8 +4218,6 @@ }, "node_modules/@smithy/middleware-endpoint": { "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-3.2.1.tgz", - "integrity": "sha512-wWO3xYmFm6WRW8VsEJ5oU6h7aosFXfszlz3Dj176pTij6o21oZnzkCLzShfmRaaCHDkBXWBdO0c4sQAvLFP6zA==", "license": "Apache-2.0", "dependencies": { "@smithy/core": "^2.5.1", @@ -4367,8 +4235,6 @@ }, "node_modules/@smithy/middleware-retry": { "version": "3.0.25", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-3.0.25.tgz", - "integrity": "sha512-m1F70cPaMBML4HiTgCw5I+jFNtjgz5z5UdGnUbG37vw6kh4UvizFYjqJGHvicfgKMkDL6mXwyPp5mhZg02g5sg==", "license": "Apache-2.0", "dependencies": { "@smithy/node-config-provider": "^3.1.9", @@ -4387,8 +4253,6 @@ }, "node_modules/@smithy/middleware-retry/node_modules/uuid": { "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" @@ -4400,8 +4264,6 @@ }, "node_modules/@smithy/middleware-serde": { "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-3.0.8.tgz", - "integrity": "sha512-Xg2jK9Wc/1g/MBMP/EUn2DLspN8LNt+GMe7cgF+Ty3vl+Zvu+VeZU5nmhveU+H8pxyTsjrAkci8NqY6OuvZnjA==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^3.6.0", @@ -4413,8 +4275,6 @@ }, "node_modules/@smithy/middleware-stack": { "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-3.0.8.tgz", - "integrity": "sha512-d7ZuwvYgp1+3682Nx0MD3D/HtkmZd49N3JUndYWQXfRZrYEnCWYc8BHcNmVsPAp9gKvlurdg/mubE6b/rPS9MA==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^3.6.0", @@ -4426,8 +4286,6 @@ }, "node_modules/@smithy/node-config-provider": { "version": "3.1.9", - "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-3.1.9.tgz", - "integrity": "sha512-qRHoah49QJ71eemjuS/WhUXB+mpNtwHRWQr77J/m40ewBVVwvo52kYAmb7iuaECgGTTcYxHS4Wmewfwy++ueew==", "license": "Apache-2.0", "dependencies": { "@smithy/property-provider": "^3.1.8", @@ -4441,8 +4299,6 @@ }, "node_modules/@smithy/node-http-handler": { "version": "3.2.5", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-3.2.5.tgz", - "integrity": "sha512-PkOwPNeKdvX/jCpn0A8n9/TyoxjGZB8WVoJmm9YzsnAgggTj4CrjpRHlTQw7dlLZ320n1mY1y+nTRUDViKi/3w==", "license": "Apache-2.0", "dependencies": { "@smithy/abort-controller": "^3.1.6", @@ -4457,8 +4313,6 @@ }, "node_modules/@smithy/property-provider": { "version": "3.1.8", - "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-3.1.8.tgz", - "integrity": "sha512-ukNUyo6rHmusG64lmkjFeXemwYuKge1BJ8CtpVKmrxQxc6rhUX0vebcptFA9MmrGsnLhwnnqeH83VTU9hwOpjA==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^3.6.0", @@ -4470,8 +4324,6 @@ }, "node_modules/@smithy/protocol-http": { "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-4.1.5.tgz", - "integrity": "sha512-hsjtwpIemmCkm3ZV5fd/T0bPIugW1gJXwZ/hpuVubt2hEUApIoUTrf6qIdh9MAWlw0vjMrA1ztJLAwtNaZogvg==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^3.6.0", @@ -4483,8 +4335,6 @@ }, "node_modules/@smithy/querystring-builder": { "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-3.0.8.tgz", - "integrity": "sha512-btYxGVqFUARbUrN6VhL9c3dnSviIwBYD9Rz1jHuN1hgh28Fpv2xjU1HeCeDJX68xctz7r4l1PBnFhGg1WBBPuA==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^3.6.0", @@ -4497,8 +4347,6 @@ }, "node_modules/@smithy/querystring-parser": { "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-3.0.8.tgz", - "integrity": "sha512-BtEk3FG7Ks64GAbt+JnKqwuobJNX8VmFLBsKIwWr1D60T426fGrV2L3YS5siOcUhhp6/Y6yhBw1PSPxA5p7qGg==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^3.6.0", @@ -4510,8 +4358,6 @@ }, "node_modules/@smithy/service-error-classification": { "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-3.0.8.tgz", - "integrity": "sha512-uEC/kCCFto83bz5ZzapcrgGqHOh/0r69sZ2ZuHlgoD5kYgXJEThCoTuw/y1Ub3cE7aaKdznb+jD9xRPIfIwD7g==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^3.6.0" @@ -4522,8 +4368,6 @@ }, "node_modules/@smithy/shared-ini-file-loader": { "version": "3.1.9", - "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-3.1.9.tgz", - "integrity": "sha512-/+OsJRNtoRbtsX0UpSgWVxFZLsJHo/4sTr+kBg/J78sr7iC+tHeOvOJrS5hCpVQ6sWBbhWLp1UNiuMyZhE6pmA==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^3.6.0", @@ -4535,8 +4379,6 @@ }, "node_modules/@smithy/signature-v4": { "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-4.2.1.tgz", - "integrity": "sha512-NsV1jF4EvmO5wqmaSzlnTVetemBS3FZHdyc5CExbDljcyJCEEkJr8ANu2JvtNbVg/9MvKAWV44kTrGS+Pi4INg==", "license": "Apache-2.0", "dependencies": { "@smithy/is-array-buffer": "^3.0.0", @@ -4554,8 +4396,6 @@ }, "node_modules/@smithy/smithy-client": { "version": "3.4.2", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-3.4.2.tgz", - "integrity": "sha512-dxw1BDxJiY9/zI3cBqfVrInij6ShjpV4fmGHesGZZUiP9OSE/EVfdwdRz0PgvkEvrZHpsj2htRaHJfftE8giBA==", "license": "Apache-2.0", "dependencies": { "@smithy/core": "^2.5.1", @@ -4572,8 +4412,6 @@ }, "node_modules/@smithy/types": { "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.6.0.tgz", - "integrity": "sha512-8VXK/KzOHefoC65yRgCn5vG1cysPJjHnOVt9d0ybFQSmJgQj152vMn4EkYhGuaOmnnZvCPav/KnYyE6/KsNZ2w==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -4584,8 +4422,6 @@ }, "node_modules/@smithy/url-parser": { "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-3.0.8.tgz", - "integrity": "sha512-4FdOhwpTW7jtSFWm7SpfLGKIBC9ZaTKG5nBF0wK24aoQKQyDIKUw3+KFWCQ9maMzrgTJIuOvOnsV2lLGW5XjTg==", "license": "Apache-2.0", "dependencies": { "@smithy/querystring-parser": "^3.0.8", @@ -4595,8 +4431,6 @@ }, "node_modules/@smithy/util-base64": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-3.0.0.tgz", - "integrity": "sha512-Kxvoh5Qtt0CDsfajiZOCpJxgtPHXOKwmM+Zy4waD43UoEMA+qPxxa98aE/7ZhdnBFZFXMOiBR5xbcaMhLtznQQ==", "license": "Apache-2.0", "dependencies": { "@smithy/util-buffer-from": "^3.0.0", @@ -4609,8 +4443,6 @@ }, "node_modules/@smithy/util-body-length-browser": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-3.0.0.tgz", - "integrity": "sha512-cbjJs2A1mLYmqmyVl80uoLTJhAcfzMOyPgjwAYusWKMdLeNtzmMz9YxNl3/jRLoxSS3wkqkf0jwNdtXWtyEBaQ==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -4618,8 +4450,6 @@ }, "node_modules/@smithy/util-body-length-node": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-3.0.0.tgz", - "integrity": "sha512-Tj7pZ4bUloNUP6PzwhN7K386tmSmEET9QtQg0TgdNOnxhZvCssHji+oZTUIuzxECRfG8rdm2PMw2WCFs6eIYkA==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -4630,8 +4460,6 @@ }, "node_modules/@smithy/util-buffer-from": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-3.0.0.tgz", - "integrity": "sha512-aEOHCgq5RWFbP+UDPvPot26EJHjOC+bRgse5A8V3FSShqd5E5UN4qc7zkwsvJPPAVsf73QwYcHN1/gt/rtLwQA==", "license": "Apache-2.0", "dependencies": { "@smithy/is-array-buffer": "^3.0.0", @@ -4643,8 +4471,6 @@ }, "node_modules/@smithy/util-config-provider": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-3.0.0.tgz", - "integrity": "sha512-pbjk4s0fwq3Di/ANL+rCvJMKM5bzAQdE5S/6RL5NXgMExFAi6UgQMPOm5yPaIWPpr+EOXKXRonJ3FoxKf4mCJQ==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -4655,8 +4481,6 @@ }, "node_modules/@smithy/util-defaults-mode-browser": { "version": "3.0.25", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-3.0.25.tgz", - "integrity": "sha512-fRw7zymjIDt6XxIsLwfJfYUfbGoO9CmCJk6rjJ/X5cd20+d2Is7xjU5Kt/AiDt6hX8DAf5dztmfP5O82gR9emA==", "license": "Apache-2.0", "dependencies": { "@smithy/property-provider": "^3.1.8", @@ -4671,8 +4495,6 @@ }, "node_modules/@smithy/util-defaults-mode-node": { "version": "3.0.25", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-3.0.25.tgz", - "integrity": "sha512-H3BSZdBDiVZGzt8TG51Pd2FvFO0PAx/A0mJ0EH8a13KJ6iUCdYnw/Dk/MdC1kTd0eUuUGisDFaxXVXo4HHFL1g==", "license": "Apache-2.0", "dependencies": { "@smithy/config-resolver": "^3.0.10", @@ -4689,8 +4511,6 @@ }, "node_modules/@smithy/util-endpoints": { "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-2.1.4.tgz", - "integrity": "sha512-kPt8j4emm7rdMWQyL0F89o92q10gvCUa6sBkBtDJ7nV2+P7wpXczzOfoDJ49CKXe5CCqb8dc1W+ZdLlrKzSAnQ==", "license": "Apache-2.0", "dependencies": { "@smithy/node-config-provider": "^3.1.9", @@ -4703,8 +4523,6 @@ }, "node_modules/@smithy/util-hex-encoding": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-3.0.0.tgz", - "integrity": "sha512-eFndh1WEK5YMUYvy3lPlVmYY/fZcQE1D8oSf41Id2vCeIkKJXPcYDCZD+4+xViI6b1XSd7tE+s5AmXzz5ilabQ==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -4715,8 +4533,6 @@ }, "node_modules/@smithy/util-middleware": { "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-3.0.8.tgz", - "integrity": "sha512-p7iYAPaQjoeM+AKABpYWeDdtwQNxasr4aXQEA/OmbOaug9V0odRVDy3Wx4ci8soljE/JXQo+abV0qZpW8NX0yA==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^3.6.0", @@ -4728,8 +4544,6 @@ }, "node_modules/@smithy/util-retry": { "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-3.0.8.tgz", - "integrity": "sha512-TCEhLnY581YJ+g1x0hapPz13JFqzmh/pMWL2KEFASC51qCfw3+Y47MrTmea4bUE5vsdxQ4F6/KFbUeSz22Q1ow==", "license": "Apache-2.0", "dependencies": { "@smithy/service-error-classification": "^3.0.8", @@ -4742,8 +4556,6 @@ }, "node_modules/@smithy/util-stream": { "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-3.2.1.tgz", - "integrity": "sha512-R3ufuzJRxSJbE58K9AEnL/uSZyVdHzud9wLS8tIbXclxKzoe09CRohj2xV8wpx5tj7ZbiJaKYcutMm1eYgz/0A==", "license": "Apache-2.0", "dependencies": { "@smithy/fetch-http-handler": "^4.0.0", @@ -4761,8 +4573,6 @@ }, "node_modules/@smithy/util-uri-escape": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-3.0.0.tgz", - "integrity": "sha512-LqR7qYLgZTD7nWLBecUi4aqolw8Mhza9ArpNEQ881MJJIU2sE5iHCK6TdyqqzcDLy0OPe10IY4T8ctVdtynubg==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -4773,8 +4583,6 @@ }, "node_modules/@smithy/util-utf8": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-3.0.0.tgz", - "integrity": "sha512-rUeT12bxFnplYDe815GXbq/oixEGHfRFFtcTF3YdDi/JaENIM6aSYYLJydG83UNzLXeRI5K8abYd/8Sp/QM0kA==", "license": "Apache-2.0", "dependencies": { "@smithy/util-buffer-from": "^3.0.0", @@ -4786,8 +4594,6 @@ }, "node_modules/@smithy/util-waiter": { "version": "3.1.7", - "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-3.1.7.tgz", - "integrity": "sha512-d5yGlQtmN/z5eoTtIYgkvOw27US2Ous4VycnXatyoImIF9tzlcpnKqQ/V7qhvJmb2p6xZne1NopCLakdTnkBBQ==", "license": "Apache-2.0", "dependencies": { "@smithy/abort-controller": "^3.1.6", @@ -5150,6 +4956,16 @@ "@types/node": "*" } }, + "node_modules/@types/busboy": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/busboy/-/busboy-1.5.4.tgz", + "integrity": "sha512-kG7WrUuAKK0NoyxfQHsVE6j1m01s6kMma64E+OZenQABMQyTJop1DumUWcLwAQ2JzpefU7PDYoRDKl8uZosFjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/cacheable-request": { "version": "6.0.3", "dev": true, @@ -6235,7 +6051,6 @@ }, "node_modules/anymatch": { "version": "3.1.3", - "dev": true, "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", @@ -6247,7 +6062,9 @@ }, "node_modules/append-field": { "version": "1.0.0", - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/aproba": { "version": "2.0.0", @@ -6322,7 +6139,9 @@ }, "node_modules/array-flatten": { "version": "1.1.1", - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/array-ify": { "version": "1.0.0", @@ -6802,7 +6621,6 @@ }, "node_modules/binary-extensions": { "version": "2.3.0", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6823,6 +6641,8 @@ "node_modules/body-parser": { "version": "1.20.3", "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", @@ -6845,18 +6665,20 @@ "node_modules/body-parser/node_modules/debug": { "version": "2.6.9", "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "ms": "2.0.0" } }, "node_modules/body-parser/node_modules/ms": { "version": "2.0.0", - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/bowser": { "version": "2.11.0", - "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz", - "integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==", "license": "MIT" }, "node_modules/brace-expansion": { @@ -6954,6 +6776,7 @@ }, "node_modules/buffer-from": { "version": "1.1.2", + "devOptional": true, "license": "MIT" }, "node_modules/busboy": { @@ -6976,6 +6799,8 @@ "node_modules/bytes": { "version": "3.1.2", "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">= 0.8" } @@ -7043,6 +6868,7 @@ }, "node_modules/call-bind": { "version": "1.0.7", + "devOptional": true, "license": "MIT", "dependencies": { "es-define-property": "^1.0.0", @@ -7147,7 +6973,6 @@ }, "node_modules/chokidar": { "version": "3.6.0", - "dev": true, "license": "MIT", "dependencies": { "anymatch": "~3.1.2", @@ -7170,7 +6995,6 @@ }, "node_modules/chokidar/node_modules/glob-parent": { "version": "5.1.2", - "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -7556,6 +7380,7 @@ }, "node_modules/content-disposition": { "version": "0.5.4", + "devOptional": true, "license": "MIT", "dependencies": { "safe-buffer": "5.2.1" @@ -7567,6 +7392,8 @@ "node_modules/content-type": { "version": "1.0.5", "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">= 0.6" } @@ -7734,13 +7561,17 @@ "node_modules/cookie": { "version": "0.7.1", "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">= 0.6" } }, "node_modules/cookie-signature": { "version": "1.0.6", - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/cookiejar": { "version": "2.1.4", @@ -7749,6 +7580,7 @@ }, "node_modules/core-util-is": { "version": "1.0.3", + "devOptional": true, "license": "MIT" }, "node_modules/cors": { @@ -8062,6 +7894,7 @@ }, "node_modules/define-data-property": { "version": "1.1.4", + "devOptional": true, "license": "MIT", "dependencies": { "es-define-property": "^1.0.0", @@ -8120,6 +7953,8 @@ "node_modules/depd": { "version": "2.0.0", "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">= 0.8" } @@ -8132,6 +7967,8 @@ "node_modules/destroy": { "version": "1.2.0", "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">= 0.8", "npm": "1.2.8000 || >= 1.4.16" @@ -8350,7 +8187,9 @@ }, "node_modules/ee-first": { "version": "1.1.1", - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/ejs": { "version": "3.1.10", @@ -8392,6 +8231,8 @@ "node_modules/encodeurl": { "version": "2.0.0", "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">= 0.8" } @@ -8582,6 +8423,7 @@ }, "node_modules/es-define-property": { "version": "1.0.0", + "devOptional": true, "license": "MIT", "dependencies": { "get-intrinsic": "^1.2.4" @@ -8592,6 +8434,7 @@ }, "node_modules/es-errors": { "version": "1.3.0", + "devOptional": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -8696,7 +8539,9 @@ }, "node_modules/escape-html": { "version": "1.0.3", - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/escape-string-regexp": { "version": "1.0.5", @@ -9219,6 +9064,8 @@ "node_modules/etag": { "version": "1.8.1", "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">= 0.6" } @@ -9313,6 +9160,8 @@ "node_modules/express": { "version": "4.21.1", "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -9353,17 +9202,23 @@ "node_modules/express/node_modules/debug": { "version": "2.6.9", "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "ms": "2.0.0" } }, "node_modules/express/node_modules/ms": { "version": "2.0.0", - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/express/node_modules/path-to-regexp": { "version": "0.1.10", - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/ext-list": { "version": "2.2.2", @@ -9465,8 +9320,6 @@ }, "node_modules/fast-xml-parser": { "version": "4.4.1", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz", - "integrity": "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==", "funding": [ { "type": "github", @@ -9612,6 +9465,8 @@ "node_modules/finalhandler": { "version": "1.3.1", "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", @@ -9628,13 +9483,17 @@ "node_modules/finalhandler/node_modules/debug": { "version": "2.6.9", "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "ms": "2.0.0" } }, "node_modules/finalhandler/node_modules/ms": { "version": "2.0.0", - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/find-up": { "version": "4.1.0", @@ -9814,6 +9673,8 @@ "node_modules/forwarded": { "version": "0.2.0", "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">= 0.6" } @@ -9821,6 +9682,8 @@ "node_modules/fresh": { "version": "0.5.2", "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">= 0.6" } @@ -9892,7 +9755,6 @@ }, "node_modules/fsevents": { "version": "2.3.3", - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9968,6 +9830,7 @@ }, "node_modules/get-intrinsic": { "version": "1.2.4", + "devOptional": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -10347,6 +10210,7 @@ }, "node_modules/gopd": { "version": "1.0.1", + "devOptional": true, "license": "MIT", "dependencies": { "get-intrinsic": "^1.1.3" @@ -10433,6 +10297,7 @@ }, "node_modules/has-property-descriptors": { "version": "1.0.2", + "devOptional": true, "license": "MIT", "dependencies": { "es-define-property": "^1.0.0" @@ -10443,6 +10308,7 @@ }, "node_modules/has-proto": { "version": "1.0.3", + "devOptional": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -10453,6 +10319,7 @@ }, "node_modules/has-symbols": { "version": "1.0.3", + "devOptional": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -10559,6 +10426,8 @@ "node_modules/http-errors": { "version": "2.0.0", "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", @@ -10639,8 +10508,38 @@ "url": "https://github.com/sponsors/typicode" } }, + "node_modules/hyper-express": { + "version": "6.17.2", + "license": "MIT", + "dependencies": { + "accepts": "^1.3.7", + "busboy": "^1.0.0", + "cookie": "^0.4.1", + "cookie-signature": "^1.1.0", + "mime-types": "^2.1.33", + "range-parser": "^1.2.1", + "type-is": "^1.6.18", + "typed-emitter": "^2.1.0", + "uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.48.0" + } + }, + "node_modules/hyper-express/node_modules/cookie": { + "version": "0.4.2", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/hyper-express/node_modules/cookie-signature": { + "version": "1.2.2", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", + "devOptional": true, "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3" @@ -10889,6 +10788,8 @@ "node_modules/ipaddr.js": { "version": "1.9.1", "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">= 0.10" } @@ -10926,7 +10827,6 @@ }, "node_modules/is-binary-path": { "version": "2.1.0", - "dev": true, "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" @@ -11274,6 +11174,7 @@ }, "node_modules/isarray": { "version": "1.0.0", + "devOptional": true, "license": "MIT" }, "node_modules/isexe": { @@ -12542,6 +12443,15 @@ "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, + "node_modules/live-directory": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/live-directory/-/live-directory-3.0.3.tgz", + "integrity": "sha512-d5jchsscPvkDqwv8lypjpxIIUz4w8fu+czfEkNEMGub4+EZ0SBj5Nclb4E2QmJNC5HJ4BwEdc5DHvoHZfIAK+w==", + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2" + } + }, "node_modules/load-json-file": { "version": "6.2.0", "dev": true, @@ -12972,6 +12882,8 @@ "node_modules/merge-descriptors": { "version": "1.0.3", "license": "MIT", + "optional": true, + "peer": true, "funding": { "url": "https://github.com/sponsors/sindresorhus" } @@ -12990,6 +12902,7 @@ }, "node_modules/methods": { "version": "1.1.2", + "devOptional": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -13009,6 +12922,8 @@ "node_modules/mime": { "version": "1.6.0", "license": "MIT", + "optional": true, + "peer": true, "bin": { "mime": "cli.js" }, @@ -13069,6 +12984,7 @@ }, "node_modules/minimist": { "version": "1.2.8", + "devOptional": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -13233,6 +13149,8 @@ "node_modules/multer": { "version": "1.4.4-lts.1", "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "append-field": "^1.0.0", "busboy": "^1.0.0", @@ -13252,6 +13170,8 @@ "node >= 0.8" ], "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", @@ -13262,6 +13182,8 @@ "node_modules/multer/node_modules/mkdirp": { "version": "0.5.6", "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "minimist": "^1.2.6" }, @@ -13272,6 +13194,8 @@ "node_modules/multer/node_modules/readable-stream": { "version": "2.3.8", "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -13284,11 +13208,15 @@ }, "node_modules/multer/node_modules/safe-buffer": { "version": "5.1.2", - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/multer/node_modules/string_decoder": { "version": "1.1.1", "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "safe-buffer": "~5.1.0" } @@ -13319,13 +13247,6 @@ "node": ">=8" } }, - "node_modules/mute-stdout": { - "version": "2.0.0", - "license": "MIT", - "engines": { - "node": ">= 10.13.0" - } - }, "node_modules/mute-stream": { "version": "0.0.8", "dev": true, @@ -13354,7 +13275,6 @@ }, "node_modules/negotiator": { "version": "0.6.4", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -13761,6 +13681,7 @@ }, "node_modules/object-inspect": { "version": "1.13.3", + "devOptional": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -13876,6 +13797,8 @@ "node_modules/on-finished": { "version": "2.4.1", "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "ee-first": "1.1.1" }, @@ -14227,6 +14150,8 @@ "node_modules/parseurl": { "version": "1.3.3", "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">= 0.8" } @@ -14600,6 +14525,7 @@ }, "node_modules/process-nextick-args": { "version": "2.0.1", + "devOptional": true, "license": "MIT" }, "node_modules/proggy": { @@ -14678,6 +14604,8 @@ "node_modules/proxy-addr": { "version": "2.0.7", "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" @@ -14729,6 +14657,7 @@ }, "node_modules/qs": { "version": "6.13.0", + "devOptional": true, "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.0.6" @@ -14792,6 +14721,8 @@ "node_modules/raw-body": { "version": "2.5.2", "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", @@ -15230,7 +15161,6 @@ }, "node_modules/readdirp": { "version": "3.6.0", - "dev": true, "license": "MIT", "dependencies": { "picomatch": "^2.2.1" @@ -15323,8 +15253,6 @@ }, "node_modules/resend": { "version": "4.0.1-alpha.0", - "resolved": "https://registry.npmjs.org/resend/-/resend-4.0.1-alpha.0.tgz", - "integrity": "sha512-qtyGk72ZJ3b3ifmz34l/z/X9EpKuqgjTc76/wihMR8I71IdhDIpIPsx/CgKlkA9oLesc8mryW+zulGr8RtEkJQ==", "license": "MIT", "dependencies": { "@react-email/render": "1.0.1" @@ -15598,6 +15526,7 @@ }, "node_modules/safer-buffer": { "version": "2.1.2", + "devOptional": true, "license": "MIT" }, "node_modules/sample-app": { @@ -15678,6 +15607,8 @@ "node_modules/send": { "version": "0.19.0", "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "debug": "2.6.9", "depd": "2.0.0", @@ -15700,17 +15631,23 @@ "node_modules/send/node_modules/debug": { "version": "2.6.9", "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "ms": "2.0.0" } }, "node_modules/send/node_modules/debug/node_modules/ms": { "version": "2.0.0", - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/send/node_modules/encodeurl": { "version": "1.0.2", "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">= 0.8" } @@ -15727,6 +15664,8 @@ "node_modules/serve-static": { "version": "1.16.2", "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", @@ -15743,6 +15682,7 @@ }, "node_modules/set-function-length": { "version": "1.2.2", + "devOptional": true, "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", @@ -15772,7 +15712,9 @@ }, "node_modules/setprototypeof": { "version": "1.2.0", - "license": "ISC" + "license": "ISC", + "optional": true, + "peer": true }, "node_modules/shallow-clone": { "version": "3.0.1", @@ -15811,6 +15753,7 @@ }, "node_modules/side-channel": { "version": "1.0.6", + "devOptional": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.7", @@ -16092,6 +16035,8 @@ "node_modules/statuses": { "version": "2.0.1", "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">= 0.8" } @@ -16272,8 +16217,6 @@ }, "node_modules/strnum": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", - "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==", "license": "MIT" }, "node_modules/strong-log-transformer": { @@ -16700,6 +16643,8 @@ "node_modules/toidentifier": { "version": "1.0.1", "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=0.6" } @@ -17059,8 +17004,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typed-emitter": { + "version": "2.1.0", + "license": "MIT", + "optionalDependencies": { + "rxjs": "*" + } + }, "node_modules/typedarray": { "version": "0.0.6", + "devOptional": true, "license": "MIT" }, "node_modules/typescript": { @@ -17098,6 +17051,8 @@ }, "node_modules/ulid": { "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ulid/-/ulid-2.3.0.tgz", + "integrity": "sha512-keqHubrlpvT6G2wH0OEfSW4mquYRcbe/J8NMmveoQOjUqmo+hXtO+ORCpWhdbZ7k72UtY61BL7haGxW6enBnjw==", "license": "MIT", "bin": { "ulid": "bin/cli.js" @@ -17169,6 +17124,8 @@ "node_modules/unpipe": { "version": "1.0.0", "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">= 0.8" } @@ -17225,6 +17182,8 @@ "node_modules/utils-merge": { "version": "1.0.1", "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">= 0.4.0" } @@ -17241,6 +17200,11 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/uWebSockets.js": { + "version": "20.48.0", + "resolved": "git+ssh://git@github.com/uNetworking/uWebSockets.js.git#51ae1d1fd92dff77cbbdc7c431021f85578da1a6", + "license": "Apache-2.0" + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "dev": true, @@ -17822,12 +17786,12 @@ }, "packages/core": { "name": "@intentjs/core", - "version": "0.1.35", + "version": "0.1.35-next-5", "license": "MIT", "dependencies": { - "@nestjs/common": "^10.4.4", - "@nestjs/core": "^10.4.4", - "@nestjs/platform-express": "^10.4.1", + "@intentjs/hyper-express": "^0.0.4", + "@nestjs/common": "^10.4.8", + "@nestjs/core": "^10.4.8", "@react-email/components": "^0.0.25", "archy": "^1.0.0", "axios": "^1.7.7", @@ -17838,13 +17802,12 @@ "dotenv": "^16.4.5", "enquirer": "^2.4.1", "eta": "^3.5.0", - "express": "^4.21.0", "fs-extra": "^11.1.1", "helmet": "^7.1.0", "ioredis": "^5.3.2", "knex": "^3.1.0", + "live-directory": "^3.0.3", "ms": "^2.1.3", - "mute-stdout": "^2.0.0", "objection": "^3.1.4", "picocolors": "^1.1.0", "react-email": "^3.0.1", @@ -17859,7 +17822,6 @@ "@nestjs/testing": "^10.3.9", "@stylistic/eslint-plugin-ts": "^2.6.1", "@types/archy": "^0.0.36", - "@types/express": "^4.17.21", "@types/fs-extra": "^11.0.1", "@types/inquirer": "^9.0.7", "@types/jest": "^29.5.13", @@ -18094,6 +18056,89 @@ "version": "0.1.14", "dev": true, "license": "Apache-2.0" + }, + "packages/hyper-express": { + "name": "@intentjs/hyper-express", + "version": "0.0.4", + "license": "MIT", + "dependencies": { + "busboy": "^1.6.0", + "cookie": "^1.0.1", + "cookie-signature": "^1.2.1", + "mime-types": "^2.1.35", + "negotiator": "^0.6.3", + "range-parser": "^1.2.1", + "type-is": "^1.6.18", + "typed-emitter": "^2.1.0", + "uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.49.0" + }, + "devDependencies": { + "@types/busboy": "^1.5.4", + "@types/express": "^5.0.0", + "@types/node": "^22.7.5", + "typescript": "^5.6.3" + } + }, + "packages/hyper-express/node_modules/@types/express": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz", + "integrity": "sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "packages/hyper-express/node_modules/@types/express-serve-static-core": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.2.tgz", + "integrity": "sha512-vluaspfvWEtE4vcSDlKRNer52DvOGrB2xv6diXy6UKyKW0lqZiWHGNApSyxOv+8DE5Z27IzVvE7hNkxg7EXIcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "packages/hyper-express/node_modules/@types/node": { + "version": "22.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.1.tgz", + "integrity": "sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "packages/hyper-express/node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "packages/hyper-express/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "packages/hyper-express/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" } } } diff --git a/package.json b/package.json index 3d2a9e5..02449b0 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,9 @@ "prepare": "husky" }, "dependencies": { - "concurrently": "^9.1.0" + "concurrently": "^9.1.0", + "hyper-express": "^6.17.2", + "uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.48.0" }, "devDependencies": { "@commitlint/cli": "^19.5.0", diff --git a/packages/core/lib/codegen/command.ts b/packages/core/lib/codegen/command.ts index da6e08f..83d46d5 100644 --- a/packages/core/lib/codegen/command.ts +++ b/packages/core/lib/codegen/command.ts @@ -1,9 +1,9 @@ import { join } from 'path'; -import { Injectable } from '@nestjs/common'; import { Command, CommandRunner, ConsoleIO } from '../console'; import { Str } from '../utils/string'; import { CodegenService } from './service'; import { getClassNamesFromFilePath } from './utils'; +import { Injectable } from '../foundation/decorators'; @Injectable() export class CodegenCommand { diff --git a/packages/core/lib/config/command.ts b/packages/core/lib/config/command.ts index aa21c19..38bd74f 100644 --- a/packages/core/lib/config/command.ts +++ b/packages/core/lib/config/command.ts @@ -1,10 +1,11 @@ -import { Command, ConsoleIO } from '../console'; import { ConfigMap } from './options'; import { CONFIG_FACTORY } from './constant'; import pc from 'picocolors'; import archy from 'archy'; -import { Inject } from '../foundation'; import { jsonToArchy } from '../utils/console-helpers'; +import { Command } from '../console/decorators'; +import { ConsoleIO } from '../console/consoleIO'; +import { Inject } from '../foundation/decorators'; @Command('config:view {--ns : Namespace of a particular config}', { desc: 'Command to view config for a given namespace', diff --git a/packages/core/lib/config/register-namespace.ts b/packages/core/lib/config/register-namespace.ts index 8af11c7..9b66ec3 100644 --- a/packages/core/lib/config/register-namespace.ts +++ b/packages/core/lib/config/register-namespace.ts @@ -7,7 +7,7 @@ import { // eslint-disable-next-line @typescript-eslint/no-var-requires require('dotenv').config(); -export const registerNamespace = ( +export const configNamespace = ( namespace: LiteralString, factory: () => T | Promise, options?: RegisterNamespaceOptions, diff --git a/packages/core/lib/config/service.ts b/packages/core/lib/config/service.ts index a2ffb79..d17028e 100644 --- a/packages/core/lib/config/service.ts +++ b/packages/core/lib/config/service.ts @@ -1,6 +1,6 @@ -import { Inject, Injectable } from '../foundation'; +import { Inject, Injectable } from '../foundation/decorators'; import { DotNotation, GetNestedPropertyType } from '../type-helpers'; -import { Obj } from '../utils'; +import { Obj } from '../utils/object'; import { CONFIG_FACTORY } from './constant'; import { ConfigMap, NamespacedConfigMapValues } from './options'; diff --git a/packages/core/lib/console/argumentParser.ts b/packages/core/lib/console/argumentParser.ts index 20b2869..0262306 100644 --- a/packages/core/lib/console/argumentParser.ts +++ b/packages/core/lib/console/argumentParser.ts @@ -1,5 +1,5 @@ -import { Str } from '../utils'; import { InternalLogger } from '../utils/logger'; +import { Str } from '../utils/string'; import { ArgumentOptionObject, ArgumentParserOutput } from './interfaces'; export class ArgumentParser { diff --git a/packages/core/lib/console/commands/route-list.ts b/packages/core/lib/console/commands/route-list.ts new file mode 100644 index 0000000..d0d767f --- /dev/null +++ b/packages/core/lib/console/commands/route-list.ts @@ -0,0 +1,20 @@ +import { DiscoveryService, MetadataScanner, ModuleRef } from '@nestjs/core'; +import { Command } from '../decorators'; +import { columnify } from '../../utils/columnify'; +import { RouteExplorer } from '../../rest'; + +@Command('routes:list') +export class ListRouteCommand { + constructor(private moduleRef: ModuleRef) {} + + async handle() { + const ds = this.moduleRef.get(DiscoveryService, { strict: false }); + const ms = this.moduleRef.get(MetadataScanner, { strict: false }); + const routeExplorer = new RouteExplorer(ds, ms, this.moduleRef); + const routes = routeExplorer.explorePlainRoutes(ds, ms); + + const formattedRows = columnify(routes, { padStart: 2 }); + + return 1; + } +} diff --git a/packages/core/lib/console/interfaces/index.ts b/packages/core/lib/console/interfaces.ts similarity index 93% rename from packages/core/lib/console/interfaces/index.ts rename to packages/core/lib/console/interfaces.ts index cb76c43..2a575b4 100644 --- a/packages/core/lib/console/interfaces/index.ts +++ b/packages/core/lib/console/interfaces.ts @@ -1,4 +1,4 @@ -import { ConsoleIO } from '../consoleIO'; +import { ConsoleIO } from './consoleIO'; export interface CommandMetaOptions { desc?: string; diff --git a/packages/core/lib/exceptions/base-exception-handler.ts b/packages/core/lib/exceptions/base-exception-handler.ts new file mode 100644 index 0000000..822699f --- /dev/null +++ b/packages/core/lib/exceptions/base-exception-handler.ts @@ -0,0 +1,71 @@ +import { ConfigService } from '../config/service'; +import { Log } from '../logger'; +import { Package } from '../utils'; +import { Type } from '../interfaces'; +import { HttpException } from './http-exception'; +import { ValidationFailed } from './validation-failed'; +import { HttpStatus } from '../rest/http-server/status-codes'; +import { ExecutionContext } from '../rest/http-server/contexts/execution-context'; + +export abstract class IntentExceptionFilter { + doNotReport(): Array> { + return []; + } + + report(): Array> | string { + return '*'; + } + + async catch(context: ExecutionContext, exception: any): Promise { + const ctx = context.switchToHttp(); + + this.reportToSentry(exception); + + Log().error('', exception); + + return this.handleHttp(context, exception); + } + + async handleHttp(context: ExecutionContext, exception: any): Promise { + const res = context.switchToHttp().getResponse(); + + const debugMode = ConfigService.get('app.debug'); + + if (exception instanceof ValidationFailed) { + return res.status(HttpStatus.UNPROCESSABLE_ENTITY).json({ + message: 'validation failed', + errors: exception.getErrors(), + }); + } + + if (exception instanceof HttpException) { + return res.status(exception.getStatus()).json({ + message: exception.message, + status: exception.getStatus(), + stack: debugMode && exception.stack, + }); + } + + return res.status(this.getStatus(exception)).json(exception); + } + + reportToSentry(exception: any): void { + const sentryConfig = ConfigService.get('app.sentry'); + if (!sentryConfig?.dsn) return; + + const exceptionConstructor = exception?.constructor; + const sentry = Package.load('@sentry/node'); + if ( + exceptionConstructor && + !this.doNotReport().includes(exceptionConstructor) + ) { + sentry.captureException(exception); + } + } + + getStatus(exception: any): HttpStatus { + return exception instanceof HttpException + ? exception.getStatus() + : HttpStatus.INTERNAL_SERVER_ERROR; + } +} diff --git a/packages/core/lib/exceptions/file-not-found-exception.ts b/packages/core/lib/exceptions/file-not-found-exception.ts new file mode 100644 index 0000000..68afc8c --- /dev/null +++ b/packages/core/lib/exceptions/file-not-found-exception.ts @@ -0,0 +1,11 @@ +import { HttpStatus } from '../rest/http-server/status-codes'; +import { HttpException } from './http-exception'; + +export class FileNotFoundException extends HttpException { + constructor( + response: string | Record, + status: number | HttpStatus = HttpStatus.NOT_FOUND, + ) { + super(response, status); + } +} diff --git a/packages/core/lib/exceptions/forbidden-exception.ts b/packages/core/lib/exceptions/forbidden-exception.ts new file mode 100644 index 0000000..cc21133 --- /dev/null +++ b/packages/core/lib/exceptions/forbidden-exception.ts @@ -0,0 +1,11 @@ +import { HttpStatus } from '../rest/http-server/status-codes'; +import { HttpException } from './http-exception'; + +export class ForbiddenException extends HttpException { + constructor( + response: string | Record, + status: number | HttpStatus = HttpStatus.FORBIDDEN, + ) { + super(response, status); + } +} diff --git a/packages/core/lib/exceptions/genericException.ts b/packages/core/lib/exceptions/genericException.ts index 11b08fa..f32d580 100644 --- a/packages/core/lib/exceptions/genericException.ts +++ b/packages/core/lib/exceptions/genericException.ts @@ -1,8 +1,9 @@ -import { HttpException } from '@nestjs/common'; +import { HttpException } from './http-exception'; +import { HttpStatus } from '../rest/http-server/status-codes'; export class GenericException extends HttpException { - constructor(message?: string) { + constructor(message?: string, status: HttpStatus = HttpStatus.FORBIDDEN) { message = message ? message : 'Something went wrong!'; - super(message, 403); + super(message, status); } } diff --git a/packages/core/lib/exceptions/http-exception.ts b/packages/core/lib/exceptions/http-exception.ts new file mode 100644 index 0000000..9a1919a --- /dev/null +++ b/packages/core/lib/exceptions/http-exception.ts @@ -0,0 +1,53 @@ +import { HttpStatus } from '../rest/http-server/status-codes'; +import { Obj } from '../utils/object'; +import { Str } from '../utils/string'; + +export interface HttpExceptionOptions { + cause?: string; + description?: string; +} + +export class HttpException extends Error { + public cause: unknown; + + constructor( + private readonly response: string | Record, + private readonly status: number | HttpStatus, + private readonly options?: HttpExceptionOptions, + ) { + super(); + this.initMessage(); + } + + public initMessage() { + if (Str.isString(this.response)) { + this.message = this.response as string; + } else if ( + Obj.isObj(this.response) && + Str.isString((this.response as Record).message) + ) { + this.message = (this.response as Record).message; + } else if (this.constructor) { + this.message = + this.constructor.name.match(/[A-Z][a-z]+|[0-9]+/g)?.join(' ') ?? + 'Error'; + } + } + + public initCause() { + this.cause = this.options?.cause ?? ''; + return; + } + + public initName(): void { + this.name = this.constructor.name; + } + + public getResponse(): string | object { + return this.response; + } + + public getStatus(): number { + return this.status; + } +} diff --git a/packages/core/lib/exceptions/index.ts b/packages/core/lib/exceptions/index.ts index 55f4532..b8c4fbc 100644 --- a/packages/core/lib/exceptions/index.ts +++ b/packages/core/lib/exceptions/index.ts @@ -1,8 +1,10 @@ export * from './unauthorized'; -export * from './invalidCredentials'; -export * from './validationfailed'; +export * from './invalid-credentials'; +export * from './validation-failed'; export * from './genericException'; -export * from './intentExceptionFilter'; -export * from './invalidValue'; -export * from './invalidValueType'; -export { HttpException, Catch } from '@nestjs/common'; +export * from './base-exception-handler'; +export * from './invalid-value'; +export * from './invalid-value-type'; +export * from './http-exception'; +export * from './forbidden-exception'; +export * from './file-not-found-exception'; diff --git a/packages/core/lib/exceptions/intentExceptionFilter.ts b/packages/core/lib/exceptions/intentExceptionFilter.ts deleted file mode 100644 index 4f8c328..0000000 --- a/packages/core/lib/exceptions/intentExceptionFilter.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { ArgumentsHost, HttpException, Type } from '@nestjs/common'; -import { BaseExceptionFilter } from '@nestjs/core'; -import { ConfigService } from '../config/service'; -import { Log } from '../logger'; -import { Request, Response } from '../rest/foundation'; -import { Package } from '../utils'; - -export abstract class IntentExceptionFilter extends BaseExceptionFilter { - abstract handleHttp(exception: any, req: Request, res: Response); - - doNotReport(): Array> { - return []; - } - - report(): Array> | string { - return '*'; - } - - catch(exception: any, host: ArgumentsHost) { - const ctx = host.switchToHttp(); - const request = ctx.getRequest() as Request; - const response = ctx.getResponse(); - - this.reportToSentry(exception); - - Log().error('', exception); - - return this.handleHttp(exception, request, response); - } - - reportToSentry(exception: any): void { - const sentryConfig = ConfigService.get('app.sentry'); - if (!sentryConfig?.dsn) return; - - const exceptionConstructor = exception?.constructor; - const sentry = Package.load('@sentry/node'); - if ( - exceptionConstructor && - !this.doNotReport().includes(exceptionConstructor) - ) { - sentry.captureException(exception); - } - } -} diff --git a/packages/core/lib/exceptions/invalid-credentials.ts b/packages/core/lib/exceptions/invalid-credentials.ts new file mode 100644 index 0000000..6e4348f --- /dev/null +++ b/packages/core/lib/exceptions/invalid-credentials.ts @@ -0,0 +1,8 @@ +import { HttpStatus } from '../rest/http-server/status-codes'; +import { HttpException } from './http-exception'; + +export class InvalidCredentials extends HttpException { + constructor() { + super('Invalid Credentials', HttpStatus.UNAUTHORIZED); + } +} diff --git a/packages/core/lib/exceptions/invalidValueType.ts b/packages/core/lib/exceptions/invalid-value-type.ts similarity index 100% rename from packages/core/lib/exceptions/invalidValueType.ts rename to packages/core/lib/exceptions/invalid-value-type.ts diff --git a/packages/core/lib/exceptions/invalidValue.ts b/packages/core/lib/exceptions/invalid-value.ts similarity index 100% rename from packages/core/lib/exceptions/invalidValue.ts rename to packages/core/lib/exceptions/invalid-value.ts diff --git a/packages/core/lib/exceptions/invalidCredentials.ts b/packages/core/lib/exceptions/invalidCredentials.ts deleted file mode 100644 index 5930b7a..0000000 --- a/packages/core/lib/exceptions/invalidCredentials.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { HttpException } from '@nestjs/common'; - -export class InvalidCredentials extends HttpException { - constructor() { - super('Invalid Credentials', 403); - } -} diff --git a/packages/core/lib/exceptions/unauthorized.ts b/packages/core/lib/exceptions/unauthorized.ts index 56f68f2..4c581a3 100644 --- a/packages/core/lib/exceptions/unauthorized.ts +++ b/packages/core/lib/exceptions/unauthorized.ts @@ -1,7 +1,8 @@ -import { HttpException } from '@nestjs/common'; +import { HttpStatus } from '../rest/http-server/status-codes'; +import { HttpException } from './http-exception'; export class Unauthorized extends HttpException { constructor() { - super('Unauthorized.', 401); + super('Unauthorized.', HttpStatus.UNAUTHORIZED); } } diff --git a/packages/core/lib/exceptions/validationfailed.ts b/packages/core/lib/exceptions/validation-failed.ts similarity index 75% rename from packages/core/lib/exceptions/validationfailed.ts rename to packages/core/lib/exceptions/validation-failed.ts index f007366..c5809e6 100644 --- a/packages/core/lib/exceptions/validationfailed.ts +++ b/packages/core/lib/exceptions/validation-failed.ts @@ -1,4 +1,5 @@ -import { HttpException, HttpStatus } from '@nestjs/common'; +import { HttpStatus } from '../rest/http-server/status-codes'; +import { HttpException } from './http-exception'; export class ValidationFailed extends HttpException { private errors: Record; diff --git a/packages/core/lib/explorer.ts b/packages/core/lib/explorer.ts index a77438d..416a219 100644 --- a/packages/core/lib/explorer.ts +++ b/packages/core/lib/explorer.ts @@ -3,10 +3,10 @@ import { CommandMeta, CommandMetaOptions } from './console'; import { ConsoleConstants } from './console/constants'; import { EventMetadata } from './events'; import { IntentEventConstants } from './events/constants'; -import { Injectable } from './foundation'; import { GenericFunction } from './interfaces'; import { JOB_NAME, JOB_OPTIONS } from './queue/constants'; import { QueueMetadata } from './queue/metadata'; +import { Injectable } from './foundation'; @Injectable() export class IntentExplorer { diff --git a/packages/core/lib/foundation/app-container.ts b/packages/core/lib/foundation/app-container.ts index 48ebf41..02774ef 100644 --- a/packages/core/lib/foundation/app-container.ts +++ b/packages/core/lib/foundation/app-container.ts @@ -1,10 +1,5 @@ import { Provider } from '@nestjs/common'; -import { NestExpressApplication } from '@nestjs/platform-express'; -import { - IntentApplication, - IntentApplicationContext, - Type, -} from '../interfaces'; +import { IntentApplicationContext, Type } from '../interfaces'; import { ImportType, ServiceProvider } from './service-provider'; export abstract class IntentAppContainer { @@ -36,7 +31,7 @@ export abstract class IntentAppContainer { return providers; } - async boot(app: IntentApplication | IntentApplicationContext): Promise { + async boot(app: IntentApplicationContext): Promise { for (const serviceProvider of IntentAppContainer.serviceProviders) { serviceProvider.boot(app); } diff --git a/packages/core/lib/foundation/decorators.ts b/packages/core/lib/foundation/decorators.ts new file mode 100644 index 0000000..1d14632 --- /dev/null +++ b/packages/core/lib/foundation/decorators.ts @@ -0,0 +1 @@ +export { Injectable, Inject, Optional } from '@nestjs/common'; diff --git a/packages/core/lib/foundation/index.ts b/packages/core/lib/foundation/index.ts index c24c610..3a7d772 100644 --- a/packages/core/lib/foundation/index.ts +++ b/packages/core/lib/foundation/index.ts @@ -1,5 +1,5 @@ -export { Injectable, Inject, Optional } from '@nestjs/common'; export * from './module-builder'; export * from './service-provider'; export * from './app-container'; export * from './container-factory'; +export * from './decorators'; diff --git a/packages/core/lib/foundation/module-builder.ts b/packages/core/lib/foundation/module-builder.ts index 96b4b64..0597aca 100644 --- a/packages/core/lib/foundation/module-builder.ts +++ b/packages/core/lib/foundation/module-builder.ts @@ -1,66 +1,18 @@ -import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common'; -import { APP_GUARD } from '@nestjs/core'; -import { Type } from '../interfaces'; -import { IntentGuard, Kernel } from '../rest'; -import { MiddlewareConfigurator } from '../rest/foundation/middlewares/configurator'; +import { Module } from '@nestjs/common'; +import { Kernel } from '../rest/foundation/kernel'; import { IntentAppContainer } from './app-container'; export class ModuleBuilder { static build(container: IntentAppContainer, kernel?: Kernel) { const providers = container.scanProviders(); - const controllers = kernel?.controllers() || []; - /** - * Scan for global middlewares - */ - const globalMiddlewares = kernel?.middlewares() || []; - const globalGuards = ModuleBuilder.buildGlobalGuardProviders( - kernel?.guards() || [], - ); @Module({ imports: container.scanImports(), - providers: [...providers, ...globalGuards], - controllers: controllers, + providers: [...providers, ...controllers], }) - class AppModule implements NestModule { - configure(consumer: MiddlewareConsumer) { - if (!kernel) return; - /** - * Apply global middleware for all routes if any found. - */ - if (globalMiddlewares.length) { - consumer.apply(...globalMiddlewares).forRoutes(''); - } - - const middlewareConfigurator = new MiddlewareConfigurator(); - kernel.routeMiddlewares(middlewareConfigurator); - /** - * Apply route specific middlewares - */ - if (middlewareConfigurator.hasAnyRule()) { - for (const rule of middlewareConfigurator.getAllRules()) { - consumer - .apply(rule.middleware) - .exclude(...rule.excludedFor) - .forRoutes(...rule.appliedFor); - } - } - } - } + class AppModule {} return AppModule; } - - static buildGlobalGuardProviders(guards: Type[]) { - const providers = []; - for (const guard of guards) { - providers.push({ - provide: APP_GUARD, - useClass: guard, - }); - } - - return providers; - } } diff --git a/packages/core/lib/foundation/service-provider.ts b/packages/core/lib/foundation/service-provider.ts index 72256d5..b4e96d5 100644 --- a/packages/core/lib/foundation/service-provider.ts +++ b/packages/core/lib/foundation/service-provider.ts @@ -5,11 +5,7 @@ import { OptionalFactoryDependency, Provider, } from '@nestjs/common'; -import { - IntentApplication, - IntentApplicationContext, - Type, -} from '../interfaces'; +import { IntentApplicationContext, Type } from '../interfaces'; export type ImportType = | Type @@ -73,5 +69,5 @@ export abstract class ServiceProvider { /** * Use this method to run */ - abstract boot(app: IntentApplication | IntentApplicationContext); + abstract boot(app: IntentApplicationContext); } diff --git a/packages/core/lib/interfaces/config.ts b/packages/core/lib/interfaces/config.ts index 30c419d..04fd059 100644 --- a/packages/core/lib/interfaces/config.ts +++ b/packages/core/lib/interfaces/config.ts @@ -2,7 +2,9 @@ import { CorsOptions, CorsOptionsDelegate, } from '@nestjs/common/interfaces/external/cors-options.interface'; -import { GenericClass } from '.'; +import { ServerConstructorOptions } from '@intentjs/hyper-express'; +import { GenericClass } from './utils'; +import { WatchOptions } from 'fs-extra'; export interface SentryConfig { dsn: string; @@ -17,9 +19,32 @@ export interface AppConfig { url: string; hostname?: string; port: number; - cors: CorsOptions | CorsOptionsDelegate; error?: { validationErrorSerializer?: GenericClass; }; sentry?: SentryConfig; } + +export type RequestParsers = + | 'json' + | 'urlencoded' + | 'formdata' + | 'plain' + | 'html' + | 'binary'; + +export interface HttpConfig { + cors?: CorsOptions | CorsOptionsDelegate; + server?: ServerConstructorOptions; + staticServe?: { + httpPath?: string; + filePath?: string; + keep?: { + extensions?: string[]; + }; + cache?: { + max_file_count?: number; + max_file_size?: number; + }; + }; +} diff --git a/packages/core/lib/interfaces/index.ts b/packages/core/lib/interfaces/index.ts index 0e0891b..747e0e8 100644 --- a/packages/core/lib/interfaces/index.ts +++ b/packages/core/lib/interfaces/index.ts @@ -1,16 +1,3 @@ -import { NestExpressApplication } from '@nestjs/platform-express'; -import { INestApplicationContext } from '@nestjs/common'; - -export type GenericFunction = (...args: any[]) => any; -export type GenericClass = Record; - -export * from './transformer'; export * from './config'; - -// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type -export interface Type extends Function { - new (...args: any[]): T; -} - -export type IntentApplication = NestExpressApplication; -export type IntentApplicationContext = INestApplicationContext; +export * from './utils'; +export * from './transformer'; diff --git a/packages/core/lib/interfaces/utils.ts b/packages/core/lib/interfaces/utils.ts new file mode 100644 index 0000000..ddc6c7b --- /dev/null +++ b/packages/core/lib/interfaces/utils.ts @@ -0,0 +1,11 @@ +import { INestApplicationContext } from '@nestjs/common'; + +export type GenericFunction = (...args: any[]) => any; +export type GenericClass = Record; + +// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type +export interface Type extends Function { + new (...args: any[]): T; +} + +export type IntentApplicationContext = INestApplicationContext; diff --git a/packages/core/lib/reflections/apply-decorators.ts b/packages/core/lib/reflections/apply-decorators.ts new file mode 100644 index 0000000..52c899c --- /dev/null +++ b/packages/core/lib/reflections/apply-decorators.ts @@ -0,0 +1,22 @@ +export function applyDecorators( + ...decorators: Array +) { + return ( + target: TFunction | object, + propertyKey?: string | symbol, + descriptor?: TypedPropertyDescriptor, + ) => { + for (const decorator of decorators) { + if (target instanceof Function && !descriptor) { + (decorator as ClassDecorator)(target); + continue; + } + + (decorator as MethodDecorator | PropertyDecorator)( + target, + propertyKey, + descriptor, + ); + } + }; +} diff --git a/packages/core/lib/reflections/index.ts b/packages/core/lib/reflections/index.ts index 25b3a4d..edaebea 100644 --- a/packages/core/lib/reflections/index.ts +++ b/packages/core/lib/reflections/index.ts @@ -1,2 +1,2 @@ export * from './reflector'; -export * from './setMetadata'; +export * from './set-metadata'; diff --git a/packages/core/lib/reflections/reflector.ts b/packages/core/lib/reflections/reflector.ts index 1073cbd..1019928 100644 --- a/packages/core/lib/reflections/reflector.ts +++ b/packages/core/lib/reflections/reflector.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/ban-types */ import 'reflect-metadata'; import { ulid } from 'ulid'; -import { Obj } from '../utils'; +import { Obj } from '../utils/object'; /** * Reflector is a class to easily fetch metadata from a class and request handler method @@ -83,7 +83,7 @@ export class Reflector { */ getFromMethod(keyOrDecorator: string | Object, defaultValue?: T): T { const key = - typeof keyOrDecorator === 'function' + typeof keyOrDecorator === 'object' ? keyOrDecorator['KEY'] : keyOrDecorator; diff --git a/packages/core/lib/reflections/setMetadata.ts b/packages/core/lib/reflections/set-metadata.ts similarity index 100% rename from packages/core/lib/reflections/setMetadata.ts rename to packages/core/lib/reflections/set-metadata.ts diff --git a/packages/core/lib/rest/decorators.ts b/packages/core/lib/rest/decorators.ts deleted file mode 100644 index 658d019..0000000 --- a/packages/core/lib/rest/decorators.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { ExecutionContext, createParamDecorator } from '@nestjs/common'; -import { plainToInstance } from 'class-transformer'; -import { Request } from './foundation'; - -export { Req, Res } from '@nestjs/common'; - -export const Body = createParamDecorator( - (data: any, ctx: ExecutionContext) => { - const request = ctx.switchToHttp().getRequest(); - if (request.dto()) return request.dto(); - - const types = Reflect.getMetadata( - 'design:paramtypes', - ctx.getClass().prototype, - ctx.getHandler().name, - ); - - const paramIndex = Reflect.getMetadata( - 'intent::body_decorator_index', - ctx.getHandler(), - ); - - const paramType = types[paramIndex]; - - /** - * Check the type of paramType, - * if the value is `function`, then we will assume that it's a DTO. - * otherwise if it is another data type, we will convert the value and then send it back. - */ - const typeParamType = typeof paramType; - - if (typeParamType === 'function') { - const dto = plainToInstance(paramType, request.all()); - request.setDto(dto); - request.body(); - } - - return request.all(); - }, - [ - (target: any, propKey: string | symbol, index: number): void => { - Reflect.defineMetadata( - 'intent::body_decorator_index', - index, - target[propKey], - ); - }, - ], -); diff --git a/packages/core/lib/rest/foundation/controller-scanner.ts b/packages/core/lib/rest/foundation/controller-scanner.ts new file mode 100644 index 0000000..6dc0457 --- /dev/null +++ b/packages/core/lib/rest/foundation/controller-scanner.ts @@ -0,0 +1,35 @@ +import { join } from 'path'; +import { Type } from '../../interfaces'; +import { + CONTROLLER_KEY, + METHOD_KEY, + METHOD_PATH, +} from '../http-server/constants'; +import { HttpRoute } from '../http-server/interfaces'; + +export class ControllerScanner { + handle(cls: Type): HttpRoute[] { + const controllerKey = Reflect.getMetadata(CONTROLLER_KEY, cls); + + const methodNames = Object.getOwnPropertyNames(cls['prototype']); + + if (!controllerKey) return; + + const routes = []; + for (const key of methodNames) { + const pathMethod = Reflect.getMetadata(METHOD_KEY, cls['prototype'], key); + const methodPath = Reflect.getMetadata( + METHOD_PATH, + cls['prototype'], + key, + ); + + if (!pathMethod) continue; + + const fullHttpPath = join(controllerKey, methodPath); + routes.push({ method: pathMethod, path: fullHttpPath }); + } + + return routes; + } +} diff --git a/packages/core/lib/rest/foundation/controller.ts b/packages/core/lib/rest/foundation/controller.ts deleted file mode 100644 index 484e3ec..0000000 --- a/packages/core/lib/rest/foundation/controller.ts +++ /dev/null @@ -1 +0,0 @@ -export { Controller } from '@nestjs/common'; diff --git a/packages/core/lib/rest/foundation/guards/base-guard.ts b/packages/core/lib/rest/foundation/guards/base-guard.ts new file mode 100644 index 0000000..ed365da --- /dev/null +++ b/packages/core/lib/rest/foundation/guards/base-guard.ts @@ -0,0 +1,13 @@ +import { ForbiddenException } from '../../../exceptions/forbidden-exception'; +import { ExecutionContext } from '../../http-server/contexts/execution-context'; + +export abstract class IntentGuard { + async handle(context: ExecutionContext): Promise { + const validationFromGuard = await this.guard(context); + if (!validationFromGuard) { + throw new ForbiddenException('Forbidden Resource'); + } + } + + abstract guard(ctx: ExecutionContext): boolean | Promise; +} diff --git a/packages/core/lib/rest/foundation/guards/baseGuard.ts b/packages/core/lib/rest/foundation/guards/baseGuard.ts deleted file mode 100644 index 692e0e2..0000000 --- a/packages/core/lib/rest/foundation/guards/baseGuard.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { CanActivate, ExecutionContext } from '@nestjs/common'; -import { Request, Response } from '../interface'; -import { Reflector } from '../../../reflections'; - -export abstract class IntentGuard implements CanActivate { - async canActivate(context: ExecutionContext): Promise { - /** - * Get Express Request Object - */ - const expressRequest = context.switchToHttp().getRequest(); - - /** - * Get Express Response Object - */ - const expressResponse = context.switchToHttp().getResponse(); - - /** - * Initialise a new Reflector class. - */ - const reflector = new Reflector(context.getClass(), context.getHandler()); - - return this.guard(expressRequest, expressResponse, reflector); - } - - abstract guard( - req: Request, - res: Response, - reflector: Reflector, - ): boolean | Promise; -} diff --git a/packages/core/lib/rest/foundation/guards/decorator.ts b/packages/core/lib/rest/foundation/guards/decorator.ts deleted file mode 100644 index 26140cd..0000000 --- a/packages/core/lib/rest/foundation/guards/decorator.ts +++ /dev/null @@ -1 +0,0 @@ -export { UseGuards } from '@nestjs/common'; diff --git a/packages/core/lib/rest/foundation/index.ts b/packages/core/lib/rest/foundation/index.ts index e819376..4c61bc1 100644 --- a/packages/core/lib/rest/foundation/index.ts +++ b/packages/core/lib/rest/foundation/index.ts @@ -1,12 +1,6 @@ -export * from './guards/baseGuard'; -export * from './guards/decorator'; -export * from './methods'; -export * from './interface'; +export * from './guards/base-guard'; export * from './kernel'; -export * from './controller'; export * from './middlewares/middleware'; export * from './middlewares/configurator'; export * from './server'; -export * from './statusCodes'; -export * from './methods'; -export * from './request-mixin'; +export { MiddlewareNext } from '@intentjs/hyper-express'; diff --git a/packages/core/lib/rest/foundation/interface.ts b/packages/core/lib/rest/foundation/interface.ts deleted file mode 100644 index cd36d02..0000000 --- a/packages/core/lib/rest/foundation/interface.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unsafe-function-type */ -import { IncomingHttpHeaders } from 'http'; -import { Request as ERequest, Response as EResponse } from 'express'; -import { Type } from '../../interfaces'; - -export type Response = EResponse; - -export interface Request extends ERequest { - logger: Function; - setDto: Function; - dto: () => any; - all: () => Record; - input: (name: string, defaultValue?: T) => T; - string: (name: string) => string; - number: (name: string) => number; - boolean: (name: string) => boolean; - hasHeader: (name: string) => boolean; - bearerToken: () => string; - // host: () => string; - httpHost: () => string; - isHttp: () => boolean; - isHttps: () => boolean; - fullUrl: () => string; - isMethod: (method: string) => boolean; - getAcceptableContentTypes: () => IncomingHttpHeaders; - // accepts: (...contentTypes: string[]) => boolean; - expectsJson: () => boolean; - validate: (schema: Type) => Promise; - setUser: (user: any) => void; - use: () => T; - only: (...keys: string[]) => Record; - except: (...keys: string[]) => Record; - isPath: (pattern: string) => boolean; - has: (...keys: string[]) => boolean; - hasAny: (...keys: string[]) => boolean; - missing: (...keys: string[]) => boolean; - hasHeaders: (...headers: string[]) => boolean; - hasIncludes: () => boolean; - includes: () => string[]; -} diff --git a/packages/core/lib/rest/foundation/kernel.ts b/packages/core/lib/rest/foundation/kernel.ts index d446598..63127ab 100644 --- a/packages/core/lib/rest/foundation/kernel.ts +++ b/packages/core/lib/rest/foundation/kernel.ts @@ -1,6 +1,6 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -import { IntentApplication, Type } from '../../interfaces'; -import { IntentGuard } from './guards/baseGuard'; +import { Server } from '@intentjs/hyper-express'; +import { Type } from '../../interfaces'; +import { IntentGuard } from './guards/base-guard'; import { MiddlewareConfigurator } from './middlewares/configurator'; import { IntentMiddleware } from './middlewares/middleware'; @@ -19,5 +19,5 @@ export abstract class Kernel { return []; } - public abstract boot(app: IntentApplication): Promise; + public abstract boot(app: Server): Promise; } diff --git a/packages/core/lib/rest/foundation/methods.ts b/packages/core/lib/rest/foundation/methods.ts deleted file mode 100644 index d92148e..0000000 --- a/packages/core/lib/rest/foundation/methods.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { - Get as NGet, - Post as NPost, - Put as NPut, - Patch as NPatch, - Delete as NDelete, - All as NAll, - Options as NOptions, - Head as NHead, - applyDecorators, -} from '@nestjs/common'; - -export const Get = (path?: string | string[]) => applyDecorators(NGet(path)); - -/** - * POST Method - * @param path - * @returns - */ -export const Post = (path?: string | string[]) => applyDecorators(NPost(path)); - -/** - * PUT Method - * @param path - * @returns - */ -export const Put = (path?: string | string[]) => applyDecorators(NPut(path)); - -/** - * PATCH Method - * @param path - * @returns - */ -export const Patch = (path?: string | string[]) => - applyDecorators(NPatch(path)); - -/** - * DELETE Method - * @param path - * @returns - */ -export const Delete = (path?: string | string[]) => - applyDecorators(NDelete(path)); - -/** - * ALL Method - * @param path - * @returns - */ -export const All = (path?: string | string[]) => applyDecorators(NAll(path)); - -/** - * Options Method - * @param path - * @returns - */ -export const Options = (path?: string | string[]) => - applyDecorators(NOptions(path)); - -/** - * HEAD Method - * @param path - * @returns - */ -export const Head = (path?: string | string[]) => applyDecorators(NHead(path)); - -export enum RequestMethod { - GET = 0, - POST = 1, - PUT = 2, - DELETE = 3, - PATCH = 4, - ALL = 5, - OPTIONS = 6, - HEAD = 7, - SEARCH = 8, -} diff --git a/packages/core/lib/rest/foundation/middlewares/configurator.ts b/packages/core/lib/rest/foundation/middlewares/configurator.ts index 90a8e64..bba6eb8 100644 --- a/packages/core/lib/rest/foundation/middlewares/configurator.ts +++ b/packages/core/lib/rest/foundation/middlewares/configurator.ts @@ -1,10 +1,12 @@ import { Type } from '../../../interfaces'; -import { RequestMethod } from '../methods'; +import { HttpMethods } from '../../http-server/interfaces'; import { IntentMiddleware } from './middleware'; -/** - * - */ +type MiddlewareRuleApplicationInfo = + | string + | Type + | { path: string; method: HttpMethods }; + export class MiddlewareConfigurator { private rules: { [key: string]: MiddlewareRule } = {}; @@ -27,14 +29,9 @@ export class MiddlewareConfigurator { } } -type MiddlewareRuleApplicationInfo = - | string - | Type - | { path: string; method: RequestMethod }; - export class MiddlewareRule { public appliedFor: Array = []; - public excludedFor: Array = + public excludedFor: Array = []; constructor(public middleware: Type) {} @@ -45,7 +42,7 @@ export class MiddlewareRule { } exclude( - ...path: Array + ...path: Array ): this { this.excludedFor.push(...path); return this; diff --git a/packages/core/lib/rest/foundation/middlewares/middleware-composer.ts b/packages/core/lib/rest/foundation/middlewares/middleware-composer.ts new file mode 100644 index 0000000..ab9ef75 --- /dev/null +++ b/packages/core/lib/rest/foundation/middlewares/middleware-composer.ts @@ -0,0 +1,137 @@ +import { ModuleRef } from '@nestjs/core'; +import { MiddlewareConfigurator } from './configurator'; +import { IntentMiddleware } from './middleware'; +import { ControllerScanner } from '../controller-scanner'; +import { Type } from '../../../interfaces/utils'; + +export class MiddlewareComposer { + private middlewareRoute = new Map(); + private excludedMiddlewareRoutes = new Map(); + + constructor( + private moduleRef: ModuleRef, + private middlewareConfigurator: MiddlewareConfigurator, + private middlewares: Type[], + ) {} + + async globalMiddlewares(): Promise { + const globalMiddlewares = []; + for (const middleware of this.middlewares) { + globalMiddlewares.push(await this.moduleRef.create(middleware)); + } + return globalMiddlewares; + } + + async getRouteMiddlewares(): Promise> { + /** + * Prepares a map like + * + * "GET:/route" => Middleware Collection + * "*:/route" => Middleware Collection + */ + for (const rule of this.middlewareConfigurator.getAllRules()) { + for (const excludedPath of rule.excludedFor) { + if ( + typeof excludedPath === 'object' && + excludedPath.path && + excludedPath.method + ) { + this.excludeMiddlewareForRoute( + `${excludedPath.method}:${excludedPath.path}`, + rule.middleware, + ); + } else if (typeof excludedPath === 'string') { + await this.excludeMiddlewareForRoute( + `*:${excludedPath}`, + rule.middleware, + ); + } + } + } + + for (const rule of this.middlewareConfigurator.getAllRules()) { + for (const appliedFor of rule.appliedFor) { + if (typeof appliedFor === 'string') { + await this.setMiddlewareForRoute(appliedFor, '*', rule.middleware); + } else if (typeof appliedFor === 'object' && appliedFor.path) { + this.setMiddlewareForRoute( + appliedFor.path, + appliedFor.method, + rule.middleware, + ); + } else { + const routes = new ControllerScanner().handle( + appliedFor as Type, + ); + for (const route of routes) { + this.setMiddlewareForRoute( + route.path, + route.method || '*', + rule.middleware, + ); + } + } + } + } + + return this.middlewareRoute; + } + + async getExcludedMiddlewaresForRoutes(): Promise> { + return this.excludedMiddlewareRoutes; + } + + async excludeMiddlewareForRoute( + routeKey: string, + middleware: Type, + ) { + const existingMiddlewares = this.excludedMiddlewareRoutes.get( + routeKey, + ) as any[]; + if (existingMiddlewares) { + this.excludedMiddlewareRoutes.set( + routeKey, + existingMiddlewares.concat(middleware.name), + ); + return; + } + + this.excludedMiddlewareRoutes.set(routeKey, [middleware.name]); + } + + async setMiddlewareForRoute( + routePath: string, + routeMethod: string, + middleware: Type, + ) { + const routeKey = `${routeMethod}:${routePath}`; + + /** + * Check if the middleware is excluded for the specified route + */ + const excludedMiddlewareNames = + this.excludedMiddlewareRoutes.get(routeKey) || []; + const excludedMiddlewareNamesWithoutMethod = + this.excludedMiddlewareRoutes.get(`*:${routePath}`) || []; + + const excludedMiddlewares = new Set([ + ...excludedMiddlewareNames, + ...excludedMiddlewareNamesWithoutMethod, + ]); + + if (excludedMiddlewares.has(middleware.name)) return; + + const existingMiddlewares = this.middlewareRoute.get(routeKey) as any[]; + if (existingMiddlewares) { + const middlewareInstance = await this.moduleRef.create(middleware); + this.middlewareRoute.set( + routeKey, + existingMiddlewares.concat(middlewareInstance), + ); + return; + } + + const middlewareInstance = await this.moduleRef.create(middleware); + this.middlewareRoute.set(routeKey, [middlewareInstance]); + } +} diff --git a/packages/core/lib/rest/foundation/middlewares/middleware.ts b/packages/core/lib/rest/foundation/middlewares/middleware.ts index 5cc60f7..ae6e7e5 100644 --- a/packages/core/lib/rest/foundation/middlewares/middleware.ts +++ b/packages/core/lib/rest/foundation/middlewares/middleware.ts @@ -1,15 +1,9 @@ -import { NestMiddleware } from '@nestjs/common'; -import { NextFunction } from 'express'; -import { Request, Response } from '../interface'; +import { MiddlewareNext, Request, Response } from '@intentjs/hyper-express'; -export abstract class IntentMiddleware implements NestMiddleware { - async use(req: Request, res: Response, next: NextFunction): Promise { - await this.boot(req, res, next); - } - - abstract boot( +export abstract class IntentMiddleware { + abstract use( req: Request, res: Response, - next: NextFunction, + next: MiddlewareNext, ): void | Promise; } diff --git a/packages/core/lib/rest/foundation/request-mixin.ts b/packages/core/lib/rest/foundation/request-mixin.ts deleted file mode 100644 index 06d0319..0000000 --- a/packages/core/lib/rest/foundation/request-mixin.ts +++ /dev/null @@ -1,163 +0,0 @@ -import { Request as ERequest } from 'express'; -import { Validator } from '../../validator'; -import { Type } from '../../interfaces'; -import { isEmpty } from '../../utils'; - -export const RequestMixin = (request: ERequest) => ({ - $dto: null, - $user: null, - logger() {}, - - setDto(dto: any): void { - this.$dto = dto; - }, - - dto(): any { - return this.$dto; - }, - - all(): Record { - return { - ...(request.query || {}), - ...(request.params || {}), - ...(request.body || {}), - }; - }, - - input(name: string, defaultValue?: T): T { - const payload = this.all(); - return name in payload ? payload[name] : defaultValue; - }, - - string(name: string): string { - const value = this.input(name); - return value && value.toString(); - }, - - number(name: string): number { - const value = this.input(name); - return +value; - }, - - boolean(name: string): boolean { - const payload = this.all(); - const val = payload[name] as string; - return [true, 'yes', 'on', '1', 1, 'true'].includes(val.toLowerCase()); - }, - - hasHeader(name: string): boolean { - return name in request.headers; - }, - - bearerToken(): string { - const authHeader = request.headers['authorization']; - const asArray = authHeader?.split(' '); - if (!isEmpty(asArray)) return asArray[1]; - return undefined; - }, - - httpHost(): string { - return request.protocol; - }, - - isHttp(): boolean { - return this.httpHost() === 'http'; - }, - - isHttps(): boolean { - return this.httpHost() === 'https'; - }, - - fullUrl(): string { - return request.url; - }, - - isMethod(method: string): boolean { - return request.method.toLowerCase() === method.toLowerCase(); - }, - - getAcceptableContentTypes(): string { - return request.headers['accept']; - }, - - expectsJson(): boolean { - return request.accepts('json') === 'json'; - }, - - async validate(schema: Type): Promise { - const payload = this.all(); - const validator = Validator.compareWith(schema); - const dto = await validator - .addMeta({ ...payload, _headers: { ...request.headers } }) - .validate({ ...payload }); - this.setDto(dto); - return true; - }, - - setUser(user: any): void { - this.$user = user; - }, - - user(): T { - return this.$user as T; - }, - - only(...keys: string[]): Record { - console.log(keys); - return {}; - }, - - except(...keys: string[]): Record { - console.log(keys); - return {}; - }, - - isPath(pathPattern: string): boolean { - console.log(request, pathPattern); - return false; - }, - - has(...keys: string[]): boolean { - const payload = this.all(); - for (const key of keys) { - if (!(key in payload)) return false; - } - - return true; - }, - - hasAny(...keys: string[]): boolean { - const payload = this.all(); - for (const key of keys) { - if (key in payload) return true; - } - - return false; - }, - - missing(...keys: string[]): boolean { - const payload = this.all(); - for (const key of keys) { - if (key in payload) return false; - } - - return true; - }, - - hasHeaders(...keys: string[]): boolean { - for (const key of keys) { - if (!(key in request.headers)) return false; - } - - return true; - }, - - hasIncludes(): boolean { - const includes = this.includes(); - return includes === ''; - }, - - includes(): string { - return this.string('include'); - }, -}); diff --git a/packages/core/lib/rest/foundation/response-custom.ts b/packages/core/lib/rest/foundation/response-custom.ts deleted file mode 100644 index 6e6139f..0000000 --- a/packages/core/lib/rest/foundation/response-custom.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { ClassSerializerContextOptions, StreamableFile } from '@nestjs/common'; -import { StreamableFileOptions } from '@nestjs/common/file-stream/interfaces'; -import { instanceToPlain, plainToInstance } from 'class-transformer'; -import { Response as EResponse } from 'express'; -import { ReadStream } from 'fs'; - -export class Response { - private $headers: Record; - private $data: Record; - private $statusCode: number; - - constructor(private response: EResponse) { - this.$data = undefined; - this.$headers = {}; - this.$statusCode = 200; - } - - send(data: any, statusCode: number = 200): this { - this.$data = data; - this.response.status(statusCode); - return this; - } - - header(key: string, value: string): this { - // this.$headers[key] = value; - this.response.setHeader(key, value); - return this; - } - - status(status: number = 200): this { - this.response.status(status); - return this; - } - - stream(stream: ReadStream, options: StreamableFileOptions = {}): this { - this.$data = new StreamableFile(stream, options); - this.status(200); - return this; - } - - data(): any { - if (this.$data instanceof StreamableFile) { - return this.$data; - } - - return this.$data; - } - - transformToPlain( - plainOrClass: any, - options: ClassSerializerContextOptions, - ): Record { - if (!plainOrClass) { - return plainOrClass; - } - if (!options.type) { - return instanceToPlain(plainOrClass, options); - } - if (plainOrClass instanceof options.type) { - return instanceToPlain(plainOrClass, options); - } - const instance = plainToInstance(options.type, plainOrClass); - return instanceToPlain(instance, options); - } -} diff --git a/packages/core/lib/rest/foundation/server.ts b/packages/core/lib/rest/foundation/server.ts index 2d97e2d..e07124e 100644 --- a/packages/core/lib/rest/foundation/server.ts +++ b/packages/core/lib/rest/foundation/server.ts @@ -1,16 +1,31 @@ -import { HttpAdapterHost, NestFactory } from '@nestjs/core'; -import { NestExpressApplication } from '@nestjs/platform-express'; +import { + DiscoveryService, + MetadataScanner, + ModuleRef, + NestFactory, +} from '@nestjs/core'; import { useContainer } from 'class-validator'; import { ConfigService } from '../../config/service'; import { IntentExceptionFilter } from '../../exceptions'; import { IntentAppContainer, ModuleBuilder } from '../../foundation'; import { Type } from '../../interfaces'; -import { Obj, Package } from '../../utils'; +import { findProjectRoot, getPackageJson, Obj, Package } from '../../utils'; import { Kernel } from '../foundation/kernel'; -import { requestMiddleware } from '../middlewares/functional/requestSerializer'; import pc from 'picocolors'; import { printBulletPoints } from '../../utils/console-helpers'; import 'console.mute'; +import { Response as HyperResponse, Server } from '@intentjs/hyper-express'; +import { MiddlewareConfigurator } from './middlewares/configurator'; +import { MiddlewareComposer } from './middlewares/middleware-composer'; +import { HyperServer } from '../http-server/server'; +import { HttpExecutionContext } from '../http-server/contexts/http-execution-context'; +import { ExecutionContext } from '../http-server/contexts/execution-context'; +import { Response } from '../http-server/response'; +import { RouteExplorer } from '../http-server/route-explorer'; +import { readSync } from 'fs-extra'; +import { join } from 'path'; + +const signals = ['SIGTERM', 'SIGINT', 'SIGUSR2']; export class IntentHttpServer { private kernel: Kernel; @@ -38,55 +53,104 @@ export class IntentHttpServer { } async start() { - console['mute'](); const module = ModuleBuilder.build(this.container, this.kernel); - const app = await NestFactory.create(module, { - bodyParser: true, - logger: false, + const app = await NestFactory.createApplicationContext(module, { + logger: ['error', 'warn'], }); - if (this.errorHandler) { - const { httpAdapter } = app.get(HttpAdapterHost); - app.useGlobalFilters(new this.errorHandler(httpAdapter)); - } + const globalGuards = this.kernel.guards(); - app.useBodyParser('json'); - app.useBodyParser('raw'); - app.useBodyParser('urlencoded'); + const appModule = app.select(module); + const ds = app.get(DiscoveryService); + const ms = app.get(MetadataScanner, { strict: false }); + const mr = appModule.get(ModuleRef, { strict: true }); - app.use(requestMiddleware); + const errorHandler = await mr.create(this.errorHandler); - useContainer(app.select(module), { fallbackOnErrors: true }); + const config = appModule.get(ConfigService); - await this.container.boot(app); + useContainer(appModule, { fallbackOnErrors: true }); + + const middlewareConfigurator = new MiddlewareConfigurator(); + this.kernel.routeMiddlewares(middlewareConfigurator); + + const composer = new MiddlewareComposer( + mr, + middlewareConfigurator, + this.kernel.middlewares(), + ); + + const globalMiddlewares = await composer.globalMiddlewares(); + const routeMiddlewares = await composer.getRouteMiddlewares(); + const excludedMiddlewares = + await composer.getExcludedMiddlewaresForRoutes(); + + const routeExplorer = new RouteExplorer(ds, ms, mr); + const routes = await routeExplorer + .useGlobalGuards(globalGuards) + .exploreFullRoutes(errorHandler); - const config = app.get(ConfigService, { strict: false }); + const serverOptions = config.get('http.server'); + + const customServer = new HyperServer(); + const server = await customServer + .useGlobalMiddlewares(globalMiddlewares) + .useRouteMiddlewares(routeMiddlewares) + .build(routes, serverOptions); + + await this.container.boot(app); + await this.kernel.boot(server); + server.set_error_handler((hReq: any, hRes: HyperResponse, error: Error) => { + const res = new Response(); + const httpContext = new HttpExecutionContext(hReq, hRes); + const context = new ExecutionContext(httpContext, null, null); + errorHandler.catch(context, error); + res.reply(hReq, hRes); + }); this.configureErrorReporter(config.get('app.sentry')); const port = config.get('app.port'); const hostname = config.get('app.hostname'); - const environment = config.get('app.env'); - const debug = config.get('app.debug'); - await app.listen(+port || 5001, hostname); + await server.listen(port, hostname || '0.0.0.0'); - console['resume'](); + for (const signal of signals) { + process.on(signal, () => this.shutdown(server, signal)); + } - console.clear(); + this.printToConsole(config, [['➜', 'routes scanned', routes.length + '']]); + } - console.log(` ${pc.green(pc.bold('Intent'))} ${pc.green('v1.5.0')}`); + printToConsole( + config: ConfigService, + extraInfo: [string, string, string][] = [], + ) { + console.clear(); console.log(); + const port = config.get('app.port'); + const hostname = config.get('app.hostname'); + const environment = config.get('app.env'); + const debug = config.get('app.debug'); + + try { + const { dependencies } = getPackageJson(); + console.log( + ` ${pc.bold(pc.green('Intent'))} ${pc.green(dependencies['@intentjs/core'])}`, + ); + console.log(); + } catch {} printBulletPoints([ ['➜', 'environment', environment], ['➜', 'debug', debug], - ['➜', 'hostname', hostname], + ['➜', 'hostname', hostname ?? '127.0.0.1'], ['➜', 'port', port], + ...extraInfo, ]); const url = new URL( - ['127.0.0.1', '0.0.0.0'].includes(hostname) + ['127.0.0.1', 'localhost', undefined].includes(hostname) ? 'http://localhost' : `http://${hostname}`, ); @@ -96,6 +160,18 @@ export class IntentHttpServer { console.log(` ${pc.white('Listening on')}: ${pc.cyan(url.toString())}`); } + async shutdown(server: Server, signal: string): Promise { + console.log(`\nReceived ${signal}, starting graceful shutdown...`); + + if (server) { + await new Promise(res => + server.close(() => { + res(1); + }), + ); + } + } + configureErrorReporter(config: Record) { if (!config) return; diff --git a/packages/core/lib/rest/http-server/constants.ts b/packages/core/lib/rest/http-server/constants.ts new file mode 100644 index 0000000..0908633 --- /dev/null +++ b/packages/core/lib/rest/http-server/constants.ts @@ -0,0 +1,8 @@ +export const ROUTE_ARGS = '__route_args__'; +export const CONTROLLER_KEY = '@intentjs/controller_path'; +export const CONTROLLER_OPTIONS = '@intentjs/controller_options'; + +export const METHOD_KEY = '@intentjs/controller_method_key'; +export const METHOD_PATH = '@intentjs/controller_method_path'; + +export const GUARD_KEY = '@intentjs/controller_guards'; diff --git a/packages/core/lib/rest/http-server/contexts/execution-context.ts b/packages/core/lib/rest/http-server/contexts/execution-context.ts new file mode 100644 index 0000000..59a6ea4 --- /dev/null +++ b/packages/core/lib/rest/http-server/contexts/execution-context.ts @@ -0,0 +1,31 @@ +import { GenericClass } from '../../../interfaces'; +import { Reflector } from '../../../reflections'; +import { HttpExecutionContext } from './http-execution-context'; + +export class ExecutionContext { + private reflectorClass: Reflector; + + constructor( + private protocolContext: HttpExecutionContext, + private readonly handlerClass: GenericClass, + private readonly handlerMethod: Function, + ) { + this.reflectorClass = new Reflector(this.handlerClass, this.handlerMethod); + } + + getClass(): GenericClass { + return this.handlerClass; + } + + getHandler(): Function { + return this.handlerMethod; + } + + switchToHttp(): HttpExecutionContext { + return this.protocolContext; + } + + getReflector(): Reflector { + return this.reflectorClass; + } +} diff --git a/packages/core/lib/rest/http-server/contexts/http-execution-context.ts b/packages/core/lib/rest/http-server/contexts/http-execution-context.ts new file mode 100644 index 0000000..bc537a6 --- /dev/null +++ b/packages/core/lib/rest/http-server/contexts/http-execution-context.ts @@ -0,0 +1,85 @@ +import { RouteArgType, RouteParamtypes } from '../param-decorators'; +import { MiddlewareNext, Request, Response } from '@intentjs/hyper-express'; + +export class HttpExecutionContext { + constructor( + private readonly request: Request, + private readonly response: Response, + private readonly next?: MiddlewareNext, + ) {} + + getRequest(): Request { + return this.request; + } + + getResponse(): Response { + return this.response; + } + + getNext(): MiddlewareNext { + return this.next; + } + + getInjectableValueFromArgType(routeArg: RouteArgType, index: number): any { + const { type, data } = routeArg; + switch (type) { + case RouteParamtypes.REQUEST: + return this.getRequest(); + + case RouteParamtypes.RESPONSE: + return this.getResponse(); + + case RouteParamtypes.QUERY: + if (data) { + return this.request.query_parameters[data as string]; + } + return { ...this.request.query_parameters }; + + case RouteParamtypes.ACCEPTS: + return this.request.headers['accept']; + + case RouteParamtypes.NEXT: + return this.getNext(); + + case RouteParamtypes.BODY: + if (data) { + return this.request.body[data as string]; + } + return { ...this.request.body }; + + case RouteParamtypes.PARAM: + if (data) { + return this.request.params[data as string]; + } + + return { ...this.request.params }; + + case RouteParamtypes.DTO: + return this.request.dto(); + + case RouteParamtypes.IP: + return this.request.ip; + + case RouteParamtypes.USER_AGENT: + return this.request.headers['user-agent']; + + case RouteParamtypes.HOST: + return this.request.hostname; + + case RouteParamtypes.BUFFER: + return this.request.buffer(); + + case RouteParamtypes.FILE: + if (data) { + return this.request.file(data as string); + } + + case RouteParamtypes.HEADERS: + if (data) { + return this.request.headers[data as string]; + } + + return { ...(this.request.headers || {}) }; + } + } +} diff --git a/packages/core/lib/rest/http-server/decorators.ts b/packages/core/lib/rest/http-server/decorators.ts new file mode 100644 index 0000000..f6bd5f2 --- /dev/null +++ b/packages/core/lib/rest/http-server/decorators.ts @@ -0,0 +1,93 @@ +import { Injectable } from '@nestjs/common'; +import { applyDecorators } from '../../reflections/apply-decorators'; +import { Type } from '../../interfaces'; +import { + CONTROLLER_KEY, + CONTROLLER_OPTIONS, + GUARD_KEY, + METHOD_KEY, + METHOD_PATH, +} from './constants'; +import { HttpMethods } from './interfaces'; +import { IntentGuard } from '../foundation/guards/base-guard'; + +export type ControllerOptions = { + host?: string; +}; + +export function Controller(path?: string, options?: ControllerOptions) { + return applyDecorators(Injectable(), ControllerMetadata(path, options)); +} + +export function ControllerMetadata(path?: string, options?: ControllerOptions) { + return function (target: Function) { + Reflect.defineMetadata(CONTROLLER_KEY, path || '', target); + Reflect.defineMetadata(CONTROLLER_OPTIONS, options, target); + }; +} + +function createRouteDecorators( + method: HttpMethods, + path?: string, + options?: ControllerOptions, +): MethodDecorator { + return function (target: object, key?: string | symbol, descriptor?: any) { + Reflect.defineMetadata(METHOD_KEY, method, target, key); + Reflect.defineMetadata(METHOD_PATH, path || '', target, key); + return descriptor; + }; +} + +export type RouteDecoratorType = ( + path?: string, + options?: ControllerOptions, +) => MethodDecorator; + +export const Get: RouteDecoratorType = (path, options) => + createRouteDecorators(HttpMethods.GET, path, options); + +export const Post: RouteDecoratorType = ( + path: string, + options?: ControllerOptions, +) => createRouteDecorators(HttpMethods.POST, path, options); + +export const Put: RouteDecoratorType = ( + path: string, + options?: ControllerOptions, +) => createRouteDecorators(HttpMethods.PUT, path, options); + +export const Patch: RouteDecoratorType = ( + path: string, + options?: ControllerOptions, +) => createRouteDecorators(HttpMethods.PATCH, path, options); + +export const Delete: RouteDecoratorType = ( + path: string, + options?: ControllerOptions, +) => createRouteDecorators(HttpMethods.DELETE, path, options); + +export const Options: RouteDecoratorType = ( + path: string, + options?: ControllerOptions, +) => createRouteDecorators(HttpMethods.OPTIONS, path, options); + +export const Head: RouteDecoratorType = ( + path: string, + options?: ControllerOptions, +) => createRouteDecorators(HttpMethods.HEAD, path, options); + +export const ANY: RouteDecoratorType = ( + path: string, + options?: ControllerOptions, +) => createRouteDecorators(HttpMethods.ANY, path, options); + +export const UseGuards = (...guards: Type[]) => { + return function (target: object, key?: string | symbol, descriptor?: any) { + if (key) { + Reflect.defineMetadata(GUARD_KEY, guards, target, key); + return; + } + Reflect.defineMetadata(GUARD_KEY, guards, target); + return; + }; +}; diff --git a/packages/core/lib/rest/http-server/http-handler.ts b/packages/core/lib/rest/http-server/http-handler.ts new file mode 100644 index 0000000..8272f5f --- /dev/null +++ b/packages/core/lib/rest/http-server/http-handler.ts @@ -0,0 +1,47 @@ +import { Response } from '@intentjs/hyper-express'; +import { IntentExceptionFilter } from '../../exceptions/base-exception-handler'; +import { IntentGuard } from '../foundation/guards/base-guard'; +import { ExecutionContext } from './contexts/execution-context'; +import { Reply } from './reply'; +import { HttpStatus } from './status-codes'; +import { StreamableFile } from './streamable-file'; +import { isUndefined } from '../../utils/helpers'; + +export class HttpRouteHandler { + constructor( + protected readonly guards: IntentGuard[], + protected readonly handler: Function, + protected readonly exceptionFilter: IntentExceptionFilter, + ) {} + + async handle( + context: ExecutionContext, + args: any[], + replyHandler: Reply, + ): Promise { + try { + /** + * Handle the Guards + */ + for (const guard of this.guards) { + await guard.handle(context); + } + + /** + * Handle the request + */ + const responseFromHandler = await this.handler(...args); + + if (responseFromHandler instanceof Response) { + return responseFromHandler; + } + + const request = context.switchToHttp().getRequest(); + const response = context.switchToHttp().getResponse(); + replyHandler.handle(request, response, responseFromHandler); + } catch (e) { + const res = this.exceptionFilter.catch(context, e); + return res; + } + } +} diff --git a/packages/core/lib/rest/http-server/index.ts b/packages/core/lib/rest/http-server/index.ts new file mode 100644 index 0000000..7460dbe --- /dev/null +++ b/packages/core/lib/rest/http-server/index.ts @@ -0,0 +1,11 @@ +export * from './contexts/execution-context'; +export * from './contexts/http-execution-context'; +export * from './decorators'; +export * from './http-handler'; +export * from './route-explorer'; +export * from './server'; +export * from './streamable-file'; +export * from './status-codes'; +export * from './param-decorators'; +export * from './interfaces'; +export { Request, Response, MiddlewareNext } from '@intentjs/hyper-express'; diff --git a/packages/core/lib/rest/http-server/interfaces.ts b/packages/core/lib/rest/http-server/interfaces.ts new file mode 100644 index 0000000..5d24717 --- /dev/null +++ b/packages/core/lib/rest/http-server/interfaces.ts @@ -0,0 +1,17 @@ +export type HttpRoute = { + method: string; + path: string; + httpHandler?: any; + middlewares?: any[]; +}; + +export enum HttpMethods { + GET = 'GET', + POST = 'POST', + PUT = 'PUT', + PATCH = 'PATCH', + OPTIONS = 'OPTIONS', + HEAD = 'HEAD', + DELETE = 'DELETE', + ANY = 'ANY', +} diff --git a/packages/core/lib/rest/http-server/param-decorators.ts b/packages/core/lib/rest/http-server/param-decorators.ts new file mode 100644 index 0000000..002cc74 --- /dev/null +++ b/packages/core/lib/rest/http-server/param-decorators.ts @@ -0,0 +1,135 @@ +import { plainToInstance } from 'class-transformer'; +import { ROUTE_ARGS } from './constants'; +import { ExecutionContext } from './contexts/execution-context'; + +export enum RouteParamtypes { + REQUEST = 0, + RESPONSE = 1, + NEXT = 2, + BODY = 3, + QUERY = 4, + PARAM = 5, + HEADERS = 6, + SESSION = 7, + FILE = 8, + HOST = 9, + IP = 10, + USER_AGENT = 11, + ACCEPTS = 12, + BUFFER = 13, + DTO = 14, +} + +export type RouteArgType = { + type: RouteParamtypes; + data: object | string | number; + factory: CustomRouteParamDecoratorFactory; +}; + +function createRouteParamDecorator(paramType: RouteParamtypes) { + return (data?: object | string | number): ParameterDecorator => + (target, key, index) => { + const args = + Reflect.getMetadata(ROUTE_ARGS, target.constructor, key) || []; + args[index] = { + type: paramType, + data: data, + }; + + Reflect.defineMetadata(ROUTE_ARGS, args, target.constructor, key); + }; +} + +type CustomRouteParamDecoratorFactory = ( + data: T, + context: ExecutionContext, + argIndex?: number, +) => any; + +export function createParamDecorator( + factory: CustomRouteParamDecoratorFactory, + enhancers?: ParameterDecorator[], +): (data?: T) => ParameterDecorator { + return (data?: T): ParameterDecorator => + (target, key, index) => { + const args = + Reflect.getMetadata(ROUTE_ARGS, target.constructor, key) || []; + args[index] = { data: data, factory }; + Reflect.defineMetadata(ROUTE_ARGS, args, target.constructor, key); + + if (Array.isArray(enhancers)) { + for (const enhancer of enhancers) enhancer(target, key, index); + } + }; +} + +export const Req: () => ParameterDecorator = createRouteParamDecorator( + RouteParamtypes.REQUEST, +); + +export const Res: () => ParameterDecorator = createRouteParamDecorator( + RouteParamtypes.RESPONSE, +); + +// export const Dto: () => ParameterDecorator = createRouteParamDecorator( +// RouteParamtypes.DTO, +// ); + +export const BufferBody: () => ParameterDecorator = createRouteParamDecorator( + RouteParamtypes.BUFFER, +); + +export const Query: (key?: string) => ParameterDecorator = + createRouteParamDecorator(RouteParamtypes.QUERY); + +export const Param: (key?: string) => ParameterDecorator = + createRouteParamDecorator(RouteParamtypes.PARAM); + +export const Body: (key?: string) => ParameterDecorator = + createRouteParamDecorator(RouteParamtypes.BODY); + +export const Header: (key?: string) => ParameterDecorator = + createRouteParamDecorator(RouteParamtypes.HEADERS); + +export const IP: () => ParameterDecorator = createRouteParamDecorator( + RouteParamtypes.IP, +); + +export const UserAgent: () => ParameterDecorator = createRouteParamDecorator( + RouteParamtypes.USER_AGENT, +); + +export const Host: () => ParameterDecorator = createRouteParamDecorator( + RouteParamtypes.HOST, +); + +export const Accepts: () => ParameterDecorator = createRouteParamDecorator( + RouteParamtypes.ACCEPTS, +); + +export const File: (key?: string) => ParameterDecorator = + createRouteParamDecorator(RouteParamtypes.FILE); + +export const Dto = createParamDecorator( + async (data: any, ctx: ExecutionContext, argIndex: number) => { + const req = ctx.switchToHttp().getRequest(); + if (req.dto()) return req.dto(); + + const types = Reflect.getMetadata( + 'design:paramtypes', + ctx.getClass().prototype, + ctx.getHandler().name, + ); + + const paramType = types[argIndex]; + const typeParamType = typeof paramType; + + if (typeParamType === 'function') { + const dto = await plainToInstance(paramType, req.all()); + req.setDto(dto); + return dto; + } + + return req.all(); + }, +); diff --git a/packages/core/lib/rest/http-server/reply.ts b/packages/core/lib/rest/http-server/reply.ts new file mode 100644 index 0000000..2f6dbf2 --- /dev/null +++ b/packages/core/lib/rest/http-server/reply.ts @@ -0,0 +1,70 @@ +import { Request, Response } from '@intentjs/hyper-express'; +import { HttpStatus } from './status-codes'; +import { isClass, isUndefined } from '../../utils/helpers'; +import { StreamableFile } from './streamable-file'; +import { Obj } from '../../utils'; +import { instanceToPlain, plainToInstance } from 'class-transformer'; + +export class Reply { + async handle(req: Request, res: Response, dataFromHandler: any) { + const { method } = req; + + /** + * Set the status code of the response + */ + if (!res.statusCode && method === 'POST') { + res.status(HttpStatus.CREATED); + } else if (!res.statusCode) { + res.status(HttpStatus.OK); + } + + if (dataFromHandler instanceof StreamableFile) { + const headers = dataFromHandler.getHeaders(); + if ( + isUndefined(res.getHeader('Content-Type')) && + !isUndefined(headers.type) + ) { + res.header('Content-Type', headers.type); + } + if ( + isUndefined(res.getHeader('Content-Disposition')) && + !isUndefined(headers.type) + ) { + res.header('Content-Disposition', headers.disposition); + } + + if ( + isUndefined(res.getHeader('Content-Length')) && + !isUndefined(headers.length) + ) { + res.header('Content-Length', headers.length + ''); + } + + return res.stream(dataFromHandler.getStream()); + } + + if (!dataFromHandler) return res.send(); + + /** + * Default to JSON + */ + let plainData = Array.isArray(dataFromHandler) + ? dataFromHandler.map(r => this.transformToPlain(r)) + : this.transformToPlain(dataFromHandler); + + // console.log(plainData); + // console.time('time_to_detect_object'); + // console.log(Obj.isObj(dataFromHandler), instanceToPlain(dataFromHandler)); + // console.timeEnd('time_to_detect_object'); + if (typeof plainData != null && typeof plainData === 'object') { + return res.json(plainData); + } + + return res.send(String(plainData)); + } + + transformToPlain(plainOrClass: any) { + if (!plainOrClass) return plainOrClass; + return instanceToPlain(plainOrClass); + } +} diff --git a/packages/core/lib/rest/http-server/response.ts b/packages/core/lib/rest/http-server/response.ts new file mode 100644 index 0000000..f9949d1 --- /dev/null +++ b/packages/core/lib/rest/http-server/response.ts @@ -0,0 +1,131 @@ +import { + Response as HResponse, + Request as HRequest, +} from '@intentjs/hyper-express'; +import { StreamableFile } from './streamable-file'; +import { HttpStatus } from './status-codes'; +import { + EXTENSTION_TO_MIME, + SupportedExtentions, +} from '../../utils/extension-to-mime'; +import { isUndefined, Obj } from '../../utils'; + +export class Response { + private statusCode: HttpStatus; + private bodyData: any | StreamableFile; + private responseHeaders: Map; + + constructor() { + this.responseHeaders = new Map(); + this.statusCode = HttpStatus.OK; + } + + status(statusCode: HttpStatus): Response { + this.statusCode = statusCode; + return this; + } + + header(key: string, value: string): Response { + this.responseHeaders.set(key, value); + return this; + } + + type(type: SupportedExtentions): Response { + this.header('Content-Type', EXTENSTION_TO_MIME[type]); + return this; + } + + body(body: any): Response { + if (Obj.isObj(body) || Array.isArray(body)) { + this.type('json'); + this.bodyData = JSON.stringify(body); + } else { + this.bodyData = body; + } + return this; + } + + text(text: string): Response { + this.type('text'); + this.bodyData = text; + return this; + } + + stream(stream: StreamableFile): Response { + this.bodyData = stream; + return this; + } + + json(json: Record | Record[]): Response { + this.type('json'); + this.bodyData = JSON.stringify(json); + return this; + } + + html(html: string): Response { + this.type('html'); + this.responseHeaders.set('Content-Type', 'text/html'); + this.bodyData = html; + return this; + } + + notFound(): Response { + this.statusCode = HttpStatus.NOT_FOUND; + return this; + } + + redirect(url: string): Response { + this.statusCode = HttpStatus.FOUND; + this.responseHeaders.set('location', url); + return this; + } + + reply(req: HRequest, res: HResponse) { + const { method } = req; + + /** + * Set the status code of the response + */ + if (!this.statusCode && method === 'POST') { + res.status(HttpStatus.CREATED); + } else if (this.statusCode) { + res.status(this.statusCode); + } else { + res.status(HttpStatus.OK); + } + + /** + * Set the headers + */ + for (const key of this.responseHeaders.keys()) { + res.setHeader(key, this.responseHeaders.get(key)); + } + + if (this.bodyData instanceof StreamableFile) { + const headers = this.bodyData.getHeaders(); + if ( + isUndefined(res.getHeader('Content-Type')) && + !isUndefined(headers.type) + ) { + res.setHeader('Content-Type', headers.type); + } + if ( + isUndefined(res.getHeader('Content-Disposition')) && + !isUndefined(headers.type) + ) { + res.setHeader('Content-Disposition', headers.disposition); + } + + if ( + isUndefined(res.getHeader('Content-Length')) && + !isUndefined(headers.length) + ) { + res.setHeader('Content-Length', headers.length + ''); + } + + return res.stream(this.bodyData.getStream()); + } + + return res.send(this.bodyData); + } +} diff --git a/packages/core/lib/rest/http-server/route-explorer.ts b/packages/core/lib/rest/http-server/route-explorer.ts new file mode 100644 index 0000000..fb9321a --- /dev/null +++ b/packages/core/lib/rest/http-server/route-explorer.ts @@ -0,0 +1,201 @@ +import { DiscoveryService, MetadataScanner, ModuleRef } from '@nestjs/core'; +import { join } from 'path'; +import { HttpRoute } from './interfaces'; +import { + Request, + Response as HResponse, + MiddlewareNext, +} from '@intentjs/hyper-express'; +import { HttpExecutionContext } from './contexts/http-execution-context'; +import { HttpRouteHandler } from './http-handler'; +import { Response } from './response'; +import { ExecutionContext } from './contexts/execution-context'; +import { Type } from '../../interfaces'; +import { + CONTROLLER_KEY, + GUARD_KEY, + METHOD_KEY, + METHOD_PATH, + ROUTE_ARGS, +} from './constants'; +import { RouteArgType } from './param-decorators'; +import { IntentGuard } from '../foundation/guards/base-guard'; +import { IntentMiddleware } from '../foundation/middlewares/middleware'; +import { IntentExceptionFilter } from '../../exceptions/base-exception-handler'; +import { Reply } from './reply'; + +export class RouteExplorer { + globalGuards: Type[] = []; + + constructor( + private discoveryService: DiscoveryService, + private metadataScanner: MetadataScanner, + private moduleRef: ModuleRef, + ) {} + + async exploreFullRoutes( + errorHandler: IntentExceptionFilter, + ): Promise { + const routes = []; + const providers = this.discoveryService.getProviders(); + for (const provider of providers) { + const { instance } = provider; + // if ( + // !instance || + // typeof instance === 'string' || + // !Object.getPrototypeOf(instance) + // ) { + // return; + // } + + const methodNames = this.metadataScanner.getAllMethodNames(instance); + for (const methodName of methodNames) { + const route = await this.scanFullRoute( + instance, + methodName, + errorHandler, + ); + route && routes.push(route); + } + } + + return routes; + } + + explorePlainRoutes( + discoveryService: DiscoveryService, + metadataScanner: MetadataScanner, + ): HttpRoute[] { + const routes = []; + const providers = discoveryService.getProviders(); + for (const provider of providers) { + const { instance } = provider; + // if ( + // !instance || + // typeof instance === 'string' || + // !Object.getPrototypeOf(instance) + // ) { + // return; + // } + + const methodNames = metadataScanner.getAllMethodNames(instance); + for (const methodName of methodNames) { + const route = this.scanPlainRoute(instance, methodName); + route && routes.push(route); + } + } + + return routes; + } + + scanPlainRoute(instance: any, key: string): Record { + const controllerKey = Reflect.getMetadata( + CONTROLLER_KEY, + instance.constructor, + ); + + if (!controllerKey) return; + + const pathMethod = Reflect.getMetadata(METHOD_KEY, instance, key); + const methodPath = Reflect.getMetadata(METHOD_PATH, instance, key); + + if (!pathMethod) return; + + console.log(instance.constructor); + console.log(controllerKey, methodPath, pathMethod, key); + + const fullHttpPath = join(controllerKey, methodPath); + return { method: pathMethod, path: fullHttpPath }; + } + + async scanFullRoute( + instance: any, + key: string, + errorHandler: IntentExceptionFilter, + ): Promise { + const controllerKey = Reflect.getMetadata( + CONTROLLER_KEY, + instance.constructor, + ); + if (controllerKey === undefined) return; + + const pathMethod = Reflect.getMetadata(METHOD_KEY, instance, key); + const methodPath = Reflect.getMetadata(METHOD_PATH, instance, key); + + if (!pathMethod) return; + const controllerGuards = Reflect.getMetadata( + GUARD_KEY, + instance.constructor, + ); + + const methodGuards = Reflect.getMetadata(GUARD_KEY, instance, key); + + const composedGuardTypes = [ + ...(controllerGuards || []), + ...(methodGuards || []), + ] as Type[]; + + const composedGuards = []; + for (const globalGuard of this.globalGuards) { + composedGuards.push(await this.moduleRef.create(globalGuard)); + } + + for (const guardType of composedGuardTypes) { + composedGuards.push(await this.moduleRef.create(guardType)); + } + + const routeArgs = + (Reflect.getMetadata( + ROUTE_ARGS, + instance.constructor, + key, + ) as RouteArgType[]) || []; + + const handler = new HttpRouteHandler( + composedGuards, + instance[key].bind(instance), + errorHandler, + ); + + const replyHandler = new Reply(); + + const cb = async (hReq: Request, hRes: HResponse, next: MiddlewareNext) => { + const httpContext = new HttpExecutionContext(hReq, hRes, next); + const context = new ExecutionContext( + httpContext, + instance.constructor, + instance[key], + ); + + const args = []; + for (const index in routeArgs) { + const routeArg = routeArgs[index]; + args.push( + routeArg.factory + ? await routeArg.factory( + routeArg.data, + context, + index as unknown as number, + ) + : httpContext.getInjectableValueFromArgType( + routeArg, + index as unknown as number, + ), + ); + } + + await handler.handle(context, args, replyHandler); + }; + + return { + method: pathMethod, + path: join('/', controllerKey, methodPath), + httpHandler: cb, + }; + } + + useGlobalGuards(guards: Type[]): RouteExplorer { + this.globalGuards.push(...guards); + return this; + } +} diff --git a/packages/core/lib/rest/http-server/server.ts b/packages/core/lib/rest/http-server/server.ts new file mode 100644 index 0000000..463b770 --- /dev/null +++ b/packages/core/lib/rest/http-server/server.ts @@ -0,0 +1,157 @@ +import HyperExpress, { MiddlewareHandler } from '@intentjs/hyper-express'; +import { HttpMethods, HttpRoute } from './interfaces'; +import { IntentMiddleware } from '../foundation/middlewares/middleware'; +import { Validator } from '../../validator'; +import { ConfigService } from '../../config'; +import LiveDirectory from 'live-directory'; +import { join } from 'path'; +import { FileNotFoundException } from '../../exceptions/file-not-found-exception'; +import { Str } from '../../utils'; + +export class HyperServer { + protected hyper: HyperExpress.Server; + globalMiddlewares: IntentMiddleware[] = []; + routeMiddlewares: Map; + excludedRouteMiddlewares: Map; + + constructor() {} + + async build( + routes: HttpRoute[], + config: HyperExpress.ServerConstructorOptions, + ): Promise { + this.hyper = new HyperExpress.Server(config || {}); + + /** + * process the body by default, so that it's available in all of the middleware, guards and controllers + */ + this.hyper.use(async (req, res) => { + req.setValidator(Validator); + await req.processBody(); + }); + + for (const middleware of this.globalMiddlewares) { + this.hyper.use(middleware.use.bind(middleware)); + } + + for (const route of routes) { + const { path, httpHandler } = route; + + const middlewares = this.composeMiddlewares(path, route.method); + switch (route.method) { + case HttpMethods.GET: + this.hyper.get(path, ...middlewares, httpHandler); + break; + + case HttpMethods.POST: + this.hyper.post(path, ...middlewares, httpHandler); + break; + + case HttpMethods.DELETE: + this.hyper.delete(path, ...middlewares, httpHandler); + break; + + case HttpMethods.HEAD: + this.hyper.head(path, ...middlewares, httpHandler); + break; + + case HttpMethods.PUT: + this.hyper.put(path, ...middlewares, httpHandler); + break; + + case HttpMethods.PATCH: + this.hyper.patch(path, ...middlewares, httpHandler); + break; + + case HttpMethods.OPTIONS: + this.hyper.options(path, ...middlewares, httpHandler); + break; + + case HttpMethods.ANY: + this.hyper.any(path, ...middlewares, httpHandler); + break; + } + } + + this.configureStaticServer(); + + return this.hyper; + } + + configureStaticServer() { + const staticServeConfig = ConfigService.get('http.staticServe'); + if (!staticServeConfig.filePath || !staticServeConfig.httpPath) return; + + const liveAssets = new LiveDirectory(staticServeConfig.filePath, { + static: true, + ...staticServeConfig, + }); + + const httpPath = join( + '/', + staticServeConfig.httpPath.replace('*', ''), + '/*', + ); + + this.hyper.get(httpPath, (req, res) => { + const path = Str.replaceFirst( + req.path.replace(join('/', staticServeConfig.httpPath), ''), + '/', + '', + ); + + const file = liveAssets.get(path); + + // Return a 404 if no asset/file exists on the derived path + if (file === undefined) { + throw new FileNotFoundException(`File at ${path} does not exist`); + } + + const fileParts = file.path.split('.'); + const extension = fileParts[fileParts.length - 1]; + + const content = file.content; + if (content instanceof Buffer) { + return res.type(extension).send(content); + } else { + return res.type(extension).stream(content); + } + }); + } + + composeMiddlewares(path: string, method: string): MiddlewareHandler[] { + const methodBasedRouteKey = `${method}:${path}`; + const routeKey = `*:${path}`; + + const middlewareInstances = [ + ...(this.routeMiddlewares.get(methodBasedRouteKey) || []), + ...(this.routeMiddlewares.get(routeKey) || []), + ]; + + const middlewares = []; + for (const middlewareInstance of middlewareInstances) { + middlewares.push(middlewareInstance.use.bind(middlewareInstance)); + } + + return middlewares; + } + + useGlobalMiddlewares(globalMiddlewares: IntentMiddleware[]): HyperServer { + this.globalMiddlewares = globalMiddlewares; + return this; + } + + useExcludeMiddlewareRoutes( + routeMiddlewares: Map, + ): HyperServer { + this.excludedRouteMiddlewares = routeMiddlewares; + return this; + } + + useRouteMiddlewares( + routeMiddlewares: Map, + ): HyperServer { + this.routeMiddlewares = routeMiddlewares; + return this; + } +} diff --git a/packages/core/lib/rest/foundation/statusCodes.ts b/packages/core/lib/rest/http-server/status-codes.ts similarity index 100% rename from packages/core/lib/rest/foundation/statusCodes.ts rename to packages/core/lib/rest/http-server/status-codes.ts diff --git a/packages/core/lib/rest/http-server/streamable-file.ts b/packages/core/lib/rest/http-server/streamable-file.ts new file mode 100644 index 0000000..d3334ab --- /dev/null +++ b/packages/core/lib/rest/http-server/streamable-file.ts @@ -0,0 +1,57 @@ +import { Readable } from 'stream'; +import { types } from 'util'; + +export type StreamableFileOptions = { + /** + * The value that will be used for the `Content-Type` response header. + */ + type?: string; + + /** + * The value that will be used for the `Content-Disposition` response header. + */ + disposition?: string; + + /** + * The value that will be used for the `Content-Length` response header. + */ + length?: number; +}; + +export class StreamableFile { + private readonly stream: Readable; + + constructor(buffer: Uint8Array, options?: StreamableFileOptions); + constructor(readable: Readable, options?: StreamableFileOptions); + constructor( + bufferOrReadable: Uint8Array | Readable, + readonly options: StreamableFileOptions = {}, + ) { + if (types.isUint8Array(bufferOrReadable)) { + this.stream = new Readable(); + this.stream.push(bufferOrReadable); + this.stream.push(null); + this.options.length ??= bufferOrReadable.length; + } else if (bufferOrReadable.pipe) { + this.stream = bufferOrReadable; + } + } + + getStream(): Readable { + return this.stream; + } + + getHeaders() { + const { + type = 'application/octet-stream', + disposition = undefined, + length = undefined, + } = this.options; + + return { + type, + disposition, + length, + }; + } +} diff --git a/packages/core/lib/rest/index.ts b/packages/core/lib/rest/index.ts index 42c715f..2a3c325 100644 --- a/packages/core/lib/rest/index.ts +++ b/packages/core/lib/rest/index.ts @@ -1,9 +1,6 @@ export * from './interceptors/timeout'; -export * from './interfaces'; -export * from './decorators'; -export { Res } from '@nestjs/common'; -export { Response } from 'express'; export * from './foundation'; export * from './middlewares/cors'; export * from './middlewares/helmet'; export * from './foundation'; +export * from './http-server'; diff --git a/packages/core/lib/rest/interfaces.ts b/packages/core/lib/rest/interfaces.ts deleted file mode 100644 index ecb97ba..0000000 --- a/packages/core/lib/rest/interfaces.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { CorsOptions } from '@nestjs/common/interfaces/external/cors-options.interface'; -import { AbstractHttpAdapter, BaseExceptionFilter } from '@nestjs/core'; -import { Request as BaseRequest } from 'express'; - -export interface IRequest extends BaseRequest { - /** - * Get all inputs from the request object - */ - all(): Record; - - /** - * Get the current user from the request object - */ - user: Record; -} - -export interface ServerOptions { - addValidationContainer?: boolean; - port?: number; - globalPrefix?: string; - exceptionFilter?: (httpAdapter: AbstractHttpAdapter) => BaseExceptionFilter; - cors?: CorsOptions; -} diff --git a/packages/core/lib/rest/middlewares/cors.ts b/packages/core/lib/rest/middlewares/cors.ts index 419bbd9..ffe516b 100644 --- a/packages/core/lib/rest/middlewares/cors.ts +++ b/packages/core/lib/rest/middlewares/cors.ts @@ -1,8 +1,8 @@ import cors, { CorsOptions } from 'cors'; -import { NextFunction } from 'express'; -import { ConfigService } from '../../config/service'; -import { Injectable } from '../../foundation'; -import { IntentMiddleware, Request, Response } from '../foundation'; +import { IntentMiddleware, MiddlewareNext } from '../foundation'; +import { ConfigService } from '../../config'; +import { Injectable } from '@nestjs/common'; +import { Request, Response } from '@intentjs/hyper-express'; @Injectable() export class CorsMiddleware extends IntentMiddleware { @@ -10,8 +10,13 @@ export class CorsMiddleware extends IntentMiddleware { super(); } - boot(req: Request, res: Response, next: NextFunction): void | Promise { - cors(this.config.get('app.cors') as CorsOptions); - next(); + async use(req: Request, res: Response): Promise { + const corsOptions = this.config.get('http.cors') || ({} as CorsOptions); + const corsMiddleware = cors(corsOptions); + await new Promise(resolve => { + corsMiddleware(req, res, () => { + resolve(1); + }); + }); } } diff --git a/packages/core/lib/rest/middlewares/functional/requestSerializer.ts b/packages/core/lib/rest/middlewares/functional/requestSerializer.ts deleted file mode 100644 index 465b751..0000000 --- a/packages/core/lib/rest/middlewares/functional/requestSerializer.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { NextFunction, Request, Response } from 'express'; -import { RequestMixin } from '../../foundation/request-mixin'; - -export const requestMiddleware = ( - req: Request, - res: Response, - next: NextFunction, -) => { - Object.assign(req, RequestMixin(req)); - next(); -}; diff --git a/packages/core/lib/rest/middlewares/helmet.ts b/packages/core/lib/rest/middlewares/helmet.ts index 80546bb..3218437 100644 --- a/packages/core/lib/rest/middlewares/helmet.ts +++ b/packages/core/lib/rest/middlewares/helmet.ts @@ -1,8 +1,8 @@ -import { NextFunction } from 'express'; import helmet from 'helmet'; import { Injectable } from '../../foundation'; -import { IntentMiddleware, Request, Response } from '../foundation'; +import { IntentMiddleware, MiddlewareNext } from '../foundation'; import { ConfigService } from '../../config'; +import { Request, Response } from '@intentjs/hyper-express'; @Injectable() export class HelmetMiddleware extends IntentMiddleware { @@ -10,7 +10,7 @@ export class HelmetMiddleware extends IntentMiddleware { super(); } - boot(req: Request, res: Response, next: NextFunction): void | Promise { + use(req: Request, res: Response, next: MiddlewareNext): void | Promise { helmet(this.config.get('app.helmet') as any); next(); } diff --git a/packages/core/lib/serviceProvider.ts b/packages/core/lib/serviceProvider.ts index 54b50c7..e241c20 100644 --- a/packages/core/lib/serviceProvider.ts +++ b/packages/core/lib/serviceProvider.ts @@ -1,7 +1,5 @@ import { DiscoveryModule } from '@nestjs/core'; import { CacheService } from './cache'; -// import { CodegenCommand } from './codegen/command'; -// import { CodegenService } from './codegen/service'; import { ViewConfigCommand } from './config/command'; import { ListCommands } from './console'; import { ObjectionService } from './database'; diff --git a/packages/core/lib/storage/file-handlers/uploaded-file.ts b/packages/core/lib/storage/file-handlers/uploaded-file.ts new file mode 100644 index 0000000..5514aad --- /dev/null +++ b/packages/core/lib/storage/file-handlers/uploaded-file.ts @@ -0,0 +1,20 @@ +import { readFileSync } from 'fs-extra'; +import { Str } from '../../utils'; + +export class UploadedFile { + constructor( + public readonly filename: string, + public readonly size: number, + public readonly mimeType: string, + public readonly tempName: string, + public readonly tempPath: string, + ) {} + + get extension(): string { + return Str.afterLast(this.filename, '.'); + } + + async toBuffer(): Promise { + return readFileSync(this.tempPath); + } +} diff --git a/packages/core/lib/storage/service.ts b/packages/core/lib/storage/service.ts index 93b36db..b4d0ba3 100644 --- a/packages/core/lib/storage/service.ts +++ b/packages/core/lib/storage/service.ts @@ -1,9 +1,9 @@ -import { Injectable } from '@nestjs/common'; import { LocalDiskOptions, S3DiskOptions } from './interfaces'; import { StorageDriver } from './interfaces'; import { DiskNotFoundException } from './exceptions/diskNotFound'; import { ConfigService } from '../config'; import { DriverMap } from './driver-mapper'; +import { Injectable } from '../foundation/decorators'; @Injectable() export class StorageService { diff --git a/packages/core/lib/transformers/interfaces.ts b/packages/core/lib/transformers/interfaces.ts index bf6c16e..f67697f 100644 --- a/packages/core/lib/transformers/interfaces.ts +++ b/packages/core/lib/transformers/interfaces.ts @@ -1,4 +1,4 @@ -import { Request } from '../rest'; +import { Request } from '@intentjs/hyper-express'; export interface TransformableContextOptions { req?: Request; diff --git a/packages/core/lib/utils/array.ts b/packages/core/lib/utils/array.ts index 605ada9..77c4fac 100644 --- a/packages/core/lib/utils/array.ts +++ b/packages/core/lib/utils/array.ts @@ -1,4 +1,4 @@ -import { InvalidValue } from '../exceptions'; +import { InvalidValue } from '../exceptions/invalid-value'; import { Obj } from './object'; export class Arr { diff --git a/packages/core/lib/utils/context.ts b/packages/core/lib/utils/context.ts deleted file mode 100644 index 906e554..0000000 --- a/packages/core/lib/utils/context.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Request } from '../rest'; - -export class Context { - req: Request; - - setRequest(req: Request): this { - this.req = req; - return this; - } - - getRequest(): Request { - return this.req; - } -} diff --git a/packages/core/lib/utils/extension-to-mime.ts b/packages/core/lib/utils/extension-to-mime.ts new file mode 100644 index 0000000..197291f --- /dev/null +++ b/packages/core/lib/utils/extension-to-mime.ts @@ -0,0 +1,2405 @@ +export type SupportedExtentions = + | '1km' + | '3dml' + | '3ds' + | '3g2' + | '3gp' + | '3gpp' + | '3mf' + | '7z' + | 'aab' + | 'aac' + | 'aam' + | 'aas' + | 'abw' + | 'ac' + | 'acc' + | 'ace' + | 'acu' + | 'acutc' + | 'adp' + | 'adts' + | 'aep' + | 'afm' + | 'afp' + | 'age' + | 'ahead' + | 'ai' + | 'aif' + | 'aifc' + | 'aiff' + | 'air' + | 'ait' + | 'ami' + | 'aml' + | 'amlx' + | 'amr' + | 'apk' + | 'apng' + | 'appcache' + | 'appinstaller' + | 'application' + | 'appx' + | 'appxbundle' + | 'apr' + | 'arc' + | 'arj' + | 'asc' + | 'asf' + | 'asm' + | 'aso' + | 'asx' + | 'atc' + | 'atom' + | 'atomcat' + | 'atomdeleted' + | 'atomsvc' + | 'atx' + | 'au' + | 'avci' + | 'avcs' + | 'avi' + | 'avif' + | 'aw' + | 'azf' + | 'azs' + | 'azv' + | 'azw' + | 'b16' + | 'bat' + | 'bcpio' + | 'bdf' + | 'bdm' + | 'bdoc' + | 'bed' + | 'bh2' + | 'bin' + | 'blb' + | 'blorb' + | 'bmi' + | 'bmml' + | 'bmp' + | 'book' + | 'box' + | 'boz' + | 'bpk' + | 'bsp' + | 'btf' + | 'btif' + | 'buffer' + | 'bz' + | 'bz2' + | 'c' + | 'c11amc' + | 'c11amz' + | 'c4d' + | 'c4f' + | 'c4g' + | 'c4p' + | 'c4u' + | 'cab' + | 'caf' + | 'cap' + | 'car' + | 'cat' + | 'cb7' + | 'cba' + | 'cbr' + | 'cbt' + | 'cbz' + | 'cc' + | 'cco' + | 'cct' + | 'ccxml' + | 'cdbcmsg' + | 'cdf' + | 'cdfx' + | 'cdkey' + | 'cdmia' + | 'cdmic' + | 'cdmid' + | 'cdmio' + | 'cdmiq' + | 'cdx' + | 'cdxml' + | 'cdy' + | 'cer' + | 'cfs' + | 'cgm' + | 'chat' + | 'chm' + | 'chrt' + | 'cif' + | 'cii' + | 'cil' + | 'cjs' + | 'cla' + | 'class' + | 'cld' + | 'clkk' + | 'clkp' + | 'clkt' + | 'clkw' + | 'clkx' + | 'clp' + | 'cmc' + | 'cmdf' + | 'cml' + | 'cmp' + | 'cmx' + | 'cod' + | 'coffee' + | 'com' + | 'conf' + | 'cpio' + | 'cpl' + | 'cpp' + | 'cpt' + | 'crd' + | 'crl' + | 'crt' + | 'crx' + | 'cryptonote' + | 'csh' + | 'csl' + | 'csml' + | 'csp' + | 'css' + | 'cst' + | 'csv' + | 'cu' + | 'curl' + | 'cwl' + | 'cww' + | 'cxt' + | 'cxx' + | 'dae' + | 'daf' + | 'dart' + | 'dataless' + | 'davmount' + | 'dbf' + | 'dbk' + | 'dcr' + | 'dcurl' + | 'dd2' + | 'ddd' + | 'ddf' + | 'dds' + | 'deb' + | 'def' + | 'deploy' + | 'der' + | 'dfac' + | 'dgc' + | 'dib' + | 'dic' + | 'dir' + | 'dis' + | 'disposition-notification' + | 'dist' + | 'distz' + | 'djv' + | 'djvu' + | 'dll' + | 'dmg' + | 'dmp' + | 'dms' + | 'dna' + | 'doc' + | 'docm' + | 'docx' + | 'dot' + | 'dotm' + | 'dotx' + | 'dp' + | 'dpg' + | 'dpx' + | 'dra' + | 'drle' + | 'dsc' + | 'dssc' + | 'dtb' + | 'dtd' + | 'dts' + | 'dtshd' + | 'dump' + | 'dvb' + | 'dvi' + | 'dwd' + | 'dwf' + | 'dwg' + | 'dxf' + | 'dxp' + | 'dxr' + | 'ear' + | 'ecelp4800' + | 'ecelp7470' + | 'ecelp9600' + | 'ecma' + | 'edm' + | 'edx' + | 'efif' + | 'ei6' + | 'elc' + | 'emf' + | 'eml' + | 'emma' + | 'emotionml' + | 'emz' + | 'eol' + | 'eot' + | 'eps' + | 'epub' + | 'es3' + | 'esa' + | 'esf' + | 'et3' + | 'etx' + | 'eva' + | 'evy' + | 'exe' + | 'exi' + | 'exp' + | 'exr' + | 'ext' + | 'ez' + | 'ez2' + | 'ez3' + | 'f' + | 'f4v' + | 'f77' + | 'f90' + | 'fbs' + | 'fcdt' + | 'fcs' + | 'fdf' + | 'fdt' + | 'fe_launch' + | 'fg5' + | 'fgd' + | 'fh' + | 'fh4' + | 'fh5' + | 'fh7' + | 'fhc' + | 'fig' + | 'fits' + | 'flac' + | 'fli' + | 'flo' + | 'flv' + | 'flw' + | 'flx' + | 'fly' + | 'fm' + | 'fnc' + | 'fo' + | 'for' + | 'fpx' + | 'frame' + | 'fsc' + | 'fst' + | 'ftc' + | 'fti' + | 'fvt' + | 'fxp' + | 'fxpl' + | 'fzs' + | 'g2w' + | 'g3' + | 'g3w' + | 'gac' + | 'gam' + | 'gbr' + | 'gca' + | 'gdl' + | 'gdoc' + | 'ged' + | 'geo' + | 'geojson' + | 'gex' + | 'ggb' + | 'ggt' + | 'ghf' + | 'gif' + | 'gim' + | 'glb' + | 'gltf' + | 'gml' + | 'gmx' + | 'gnumeric' + | 'gph' + | 'gpx' + | 'gqf' + | 'gqs' + | 'gram' + | 'gramps' + | 'gre' + | 'grv' + | 'grxml' + | 'gsf' + | 'gsheet' + | 'gslides' + | 'gtar' + | 'gtm' + | 'gtw' + | 'gv' + | 'gxf' + | 'gxt' + | 'gz' + | 'h' + | 'h261' + | 'h263' + | 'h264' + | 'hal' + | 'hbci' + | 'hbs' + | 'hdd' + | 'hdf' + | 'heic' + | 'heics' + | 'heif' + | 'heifs' + | 'hej2' + | 'held' + | 'hh' + | 'hjson' + | 'hlp' + | 'hpgl' + | 'hpid' + | 'hps' + | 'hqx' + | 'hsj2' + | 'htc' + | 'htke' + | 'htm' + | 'html' + | 'hvd' + | 'hvp' + | 'hvs' + | 'i2g' + | 'icc' + | 'ice' + | 'icm' + | 'ico' + | 'ics' + | 'ief' + | 'ifb' + | 'ifm' + | 'iges' + | 'igl' + | 'igm' + | 'igs' + | 'igx' + | 'iif' + | 'img' + | 'imp' + | 'ims' + | 'in' + | 'ini' + | 'ink' + | 'inkml' + | 'install' + | 'iota' + | 'ipfix' + | 'ipk' + | 'irm' + | 'irp' + | 'iso' + | 'itp' + | 'its' + | 'ivp' + | 'ivu' + | 'jad' + | 'jade' + | 'jam' + | 'jar' + | 'jardiff' + | 'java' + | 'jhc' + | 'jisp' + | 'jls' + | 'jlt' + | 'jng' + | 'jnlp' + | 'joda' + | 'jp2' + | 'jpe' + | 'jpeg' + | 'jpf' + | 'jpg' + | 'jpg2' + | 'jpgm' + | 'jpgv' + | 'jph' + | 'jpm' + | 'jpx' + | 'js' + | 'json' + | 'json5' + | 'jsonld' + | 'jsonml' + | 'jsx' + | 'jt' + | 'jxr' + | 'jxra' + | 'jxrs' + | 'jxs' + | 'jxsc' + | 'jxsi' + | 'jxss' + | 'kar' + | 'karbon' + | 'kdbx' + | 'key' + | 'kfo' + | 'kia' + | 'kml' + | 'kmz' + | 'kne' + | 'knp' + | 'kon' + | 'kpr' + | 'kpt' + | 'kpxx' + | 'ksp' + | 'ktr' + | 'ktx' + | 'ktx2' + | 'ktz' + | 'kwd' + | 'kwt' + | 'lasxml' + | 'latex' + | 'lbd' + | 'lbe' + | 'les' + | 'less' + | 'lgr' + | 'lha' + | 'link66' + | 'list' + | 'list3820' + | 'listafp' + | 'litcoffee' + | 'lnk' + | 'log' + | 'lostxml' + | 'lrf' + | 'lrm' + | 'ltf' + | 'lua' + | 'luac' + | 'lvp' + | 'lwp' + | 'lzh' + | 'm13' + | 'm14' + | 'm1v' + | 'm21' + | 'm2a' + | 'm2v' + | 'm3a' + | 'm3u' + | 'm3u8' + | 'm4a' + | 'm4p' + | 'm4s' + | 'm4u' + | 'm4v' + | 'ma' + | 'mads' + | 'maei' + | 'mag' + | 'maker' + | 'man' + | 'manifest' + | 'map' + | 'mar' + | 'markdown' + | 'mathml' + | 'mb' + | 'mbk' + | 'mbox' + | 'mc1' + | 'mcd' + | 'mcurl' + | 'md' + | 'mdb' + | 'mdi' + | 'mdx' + | 'me' + | 'mesh' + | 'meta4' + | 'metalink' + | 'mets' + | 'mfm' + | 'mft' + | 'mgp' + | 'mgz' + | 'mid' + | 'midi' + | 'mie' + | 'mif' + | 'mime' + | 'mj2' + | 'mjp2' + | 'mjs' + | 'mk3d' + | 'mka' + | 'mkd' + | 'mks' + | 'mkv' + | 'mlp' + | 'mmd' + | 'mmf' + | 'mml' + | 'mmr' + | 'mng' + | 'mny' + | 'mobi' + | 'mods' + | 'mov' + | 'movie' + | 'mp2' + | 'mp21' + | 'mp2a' + | 'mp3' + | 'mp4' + | 'mp4a' + | 'mp4s' + | 'mp4v' + | 'mpc' + | 'mpd' + | 'mpe' + | 'mpeg' + | 'mpf' + | 'mpg' + | 'mpg4' + | 'mpga' + | 'mpkg' + | 'mpm' + | 'mpn' + | 'mpp' + | 'mpt' + | 'mpy' + | 'mqy' + | 'mrc' + | 'mrcx' + | 'ms' + | 'mscml' + | 'mseed' + | 'mseq' + | 'msf' + | 'msg' + | 'msh' + | 'msi' + | 'msix' + | 'msixbundle' + | 'msl' + | 'msm' + | 'msp' + | 'msty' + | 'mtl' + | 'mts' + | 'mus' + | 'musd' + | 'musicxml' + | 'mvb' + | 'mvt' + | 'mwf' + | 'mxf' + | 'mxl' + | 'mxmf' + | 'mxml' + | 'mxs' + | 'mxu' + | 'n-gage' + | 'n3' + | 'nb' + | 'nbp' + | 'nc' + | 'ncx' + | 'nfo' + | 'ngdat' + | 'nitf' + | 'nlu' + | 'nml' + | 'nnd' + | 'nns' + | 'nnw' + | 'npx' + | 'nq' + | 'nsc' + | 'nsf' + | 'nt' + | 'ntf' + | 'numbers' + | 'nzb' + | 'oa2' + | 'oa3' + | 'oas' + | 'obd' + | 'obgx' + | 'obj' + | 'oda' + | 'odb' + | 'odc' + | 'odf' + | 'odft' + | 'odg' + | 'odi' + | 'odm' + | 'odp' + | 'ods' + | 'odt' + | 'oga' + | 'ogex' + | 'ogg' + | 'ogv' + | 'ogx' + | 'omdoc' + | 'onepkg' + | 'onetmp' + | 'onetoc' + | 'onetoc2' + | 'opf' + | 'opml' + | 'oprc' + | 'opus' + | 'org' + | 'osf' + | 'osfpvg' + | 'osm' + | 'otc' + | 'otf' + | 'otg' + | 'oth' + | 'oti' + | 'otp' + | 'ots' + | 'ott' + | 'ova' + | 'ovf' + | 'owl' + | 'oxps' + | 'oxt' + | 'p' + | 'p10' + | 'p12' + | 'p7b' + | 'p7c' + | 'p7m' + | 'p7r' + | 'p7s' + | 'p8' + | 'pac' + | 'pages' + | 'pas' + | 'paw' + | 'pbd' + | 'pbm' + | 'pcap' + | 'pcf' + | 'pcl' + | 'pclxl' + | 'pct' + | 'pcurl' + | 'pcx' + | 'pdb' + | 'pde' + | 'pdf' + | 'pem' + | 'pfa' + | 'pfb' + | 'pfm' + | 'pfr' + | 'pfx' + | 'pgm' + | 'pgn' + | 'pgp' + | 'php' + | 'pic' + | 'pkg' + | 'pki' + | 'pkipath' + | 'pkpass' + | 'pl' + | 'plb' + | 'plc' + | 'plf' + | 'pls' + | 'pm' + | 'pml' + | 'png' + | 'pnm' + | 'portpkg' + | 'pot' + | 'potm' + | 'potx' + | 'ppam' + | 'ppd' + | 'ppm' + | 'pps' + | 'ppsm' + | 'ppsx' + | 'ppt' + | 'pptm' + | 'pptx' + | 'pqa' + | 'prc' + | 'pre' + | 'prf' + | 'provx' + | 'ps' + | 'psb' + | 'psd' + | 'psf' + | 'pskcxml' + | 'pti' + | 'ptid' + | 'pub' + | 'pvb' + | 'pwn' + | 'pya' + | 'pyo' + | 'pyox' + | 'pyv' + | 'qam' + | 'qbo' + | 'qfx' + | 'qps' + | 'qt' + | 'qwd' + | 'qwt' + | 'qxb' + | 'qxd' + | 'qxl' + | 'qxt' + | 'ra' + | 'ram' + | 'raml' + | 'rapd' + | 'rar' + | 'ras' + | 'rcprofile' + | 'rdf' + | 'rdz' + | 'relo' + | 'rep' + | 'res' + | 'rgb' + | 'rif' + | 'rip' + | 'ris' + | 'rl' + | 'rlc' + | 'rld' + | 'rm' + | 'rmi' + | 'rmp' + | 'rms' + | 'rmvb' + | 'rnc' + | 'rng' + | 'roa' + | 'roff' + | 'rp9' + | 'rpm' + | 'rpss' + | 'rpst' + | 'rq' + | 'rs' + | 'rsat' + | 'rsd' + | 'rsheet' + | 'rss' + | 'rtf' + | 'rtx' + | 'run' + | 'rusd' + | 's' + | 's3m' + | 'saf' + | 'sass' + | 'sbml' + | 'sc' + | 'scd' + | 'scm' + | 'scq' + | 'scs' + | 'scss' + | 'scurl' + | 'sda' + | 'sdc' + | 'sdd' + | 'sdkd' + | 'sdkm' + | 'sdp' + | 'sdw' + | 'sea' + | 'see' + | 'seed' + | 'sema' + | 'semd' + | 'semf' + | 'senmlx' + | 'sensmlx' + | 'ser' + | 'setpay' + | 'setreg' + | 'sfd-hdstx' + | 'sfs' + | 'sfv' + | 'sgi' + | 'sgl' + | 'sgm' + | 'sgml' + | 'sh' + | 'shar' + | 'shex' + | 'shf' + | 'shtml' + | 'sid' + | 'sieve' + | 'sig' + | 'sil' + | 'silo' + | 'sis' + | 'sisx' + | 'sit' + | 'sitx' + | 'siv' + | 'skd' + | 'skm' + | 'skp' + | 'skt' + | 'sldm' + | 'sldx' + | 'slim' + | 'slm' + | 'sls' + | 'slt' + | 'sm' + | 'smf' + | 'smi' + | 'smil' + | 'smv' + | 'smzip' + | 'snd' + | 'snf' + | 'so' + | 'spc' + | 'spdx' + | 'spf' + | 'spl' + | 'spot' + | 'spp' + | 'spq' + | 'spx' + | 'sql' + | 'src' + | 'srt' + | 'sru' + | 'srx' + | 'ssdl' + | 'sse' + | 'ssf' + | 'ssml' + | 'st' + | 'stc' + | 'std' + | 'stf' + | 'sti' + | 'stk' + | 'stl' + | 'stpx' + | 'stpxz' + | 'stpz' + | 'str' + | 'stw' + | 'styl' + | 'stylus' + | 'sub' + | 'sus' + | 'susp' + | 'sv4cpio' + | 'sv4crc' + | 'svc' + | 'svd' + | 'svg' + | 'svgz' + | 'swa' + | 'swf' + | 'swi' + | 'swidtag' + | 'sxc' + | 'sxd' + | 'sxg' + | 'sxi' + | 'sxm' + | 'sxw' + | 't' + | 't3' + | 't38' + | 'taglet' + | 'tao' + | 'tap' + | 'tar' + | 'tcap' + | 'tcl' + | 'td' + | 'teacher' + | 'tei' + | 'teicorpus' + | 'tex' + | 'texi' + | 'texinfo' + | 'text' + | 'tfi' + | 'tfm' + | 'tfx' + | 'tga' + | 'thmx' + | 'tif' + | 'tiff' + | 'tk' + | 'tmo' + | 'toml' + | 'torrent' + | 'tpl' + | 'tpt' + | 'tr' + | 'tra' + | 'trig' + | 'trm' + | 'ts' + | 'tsd' + | 'tsv' + | 'ttc' + | 'ttf' + | 'ttl' + | 'ttml' + | 'twd' + | 'twds' + | 'txd' + | 'txf' + | 'txt' + | 'u32' + | 'u3d' + | 'u8dsn' + | 'u8hdr' + | 'u8mdn' + | 'u8msg' + | 'ubj' + | 'udeb' + | 'ufd' + | 'ufdl' + | 'ulx' + | 'umj' + | 'unityweb' + | 'uo' + | 'uoml' + | 'uri' + | 'uris' + | 'urls' + | 'usda' + | 'usdz' + | 'ustar' + | 'utz' + | 'uu' + | 'uva' + | 'uvd' + | 'uvf' + | 'uvg' + | 'uvh' + | 'uvi' + | 'uvm' + | 'uvp' + | 'uvs' + | 'uvt' + | 'uvu' + | 'uvv' + | 'uvva' + | 'uvvd' + | 'uvvf' + | 'uvvg' + | 'uvvh' + | 'uvvi' + | 'uvvm' + | 'uvvp' + | 'uvvs' + | 'uvvt' + | 'uvvu' + | 'uvvv' + | 'uvvx' + | 'uvvz' + | 'uvx' + | 'uvz' + | 'vbox' + | 'vbox-extpack' + | 'vcard' + | 'vcd' + | 'vcf' + | 'vcg' + | 'vcs' + | 'vcx' + | 'vdi' + | 'vds' + | 'vhd' + | 'vis' + | 'viv' + | 'vmdk' + | 'vob' + | 'vor' + | 'vox' + | 'vrml' + | 'vsd' + | 'vsf' + | 'vss' + | 'vst' + | 'vsw' + | 'vtf' + | 'vtt' + | 'vtu' + | 'vxml' + | 'w3d' + | 'wad' + | 'wadl' + | 'war' + | 'wasm' + | 'wav' + | 'wax' + | 'wbmp' + | 'wbs' + | 'wbxml' + | 'wcm' + | 'wdb' + | 'wdp' + | 'weba' + | 'webapp' + | 'webm' + | 'webmanifest' + | 'webp' + | 'wg' + | 'wgsl' + | 'wgt' + | 'wif' + | 'wks' + | 'wm' + | 'wma' + | 'wmd' + | 'wmf' + | 'wml' + | 'wmlc' + | 'wmls' + | 'wmlsc' + | 'wmv' + | 'wmx' + | 'wmz' + | 'woff' + | 'woff2' + | 'wpd' + | 'wpl' + | 'wps' + | 'wqd' + | 'wri' + | 'wrl' + | 'wsc' + | 'wsdl' + | 'wspolicy' + | 'wtb' + | 'wvx' + | 'x32' + | 'x3d' + | 'x3db' + | 'x3dbz' + | 'x3dv' + | 'x3dvz' + | 'x3dz' + | 'x_b' + | 'x_t' + | 'xaml' + | 'xap' + | 'xar' + | 'xav' + | 'xbap' + | 'xbd' + | 'xbm' + | 'xca' + | 'xcs' + | 'xdf' + | 'xdm' + | 'xdp' + | 'xdssc' + | 'xdw' + | 'xel' + | 'xenc' + | 'xer' + | 'xfdf' + | 'xfdl' + | 'xht' + | 'xhtm' + | 'xhtml' + | 'xhvml' + | 'xif' + | 'xla' + | 'xlam' + | 'xlc' + | 'xlf' + | 'xlm' + | 'xls' + | 'xlsb' + | 'xlsm' + | 'xlsx' + | 'xlt' + | 'xltm' + | 'xltx' + | 'xlw' + | 'xm' + | 'xml' + | 'xns' + | 'xo' + | 'xop' + | 'xpi' + | 'xpl' + | 'xpm' + | 'xpr' + | 'xps' + | 'xpw' + | 'xpx' + | 'xsd' + | 'xsf' + | 'xsl' + | 'xslt' + | 'xsm' + | 'xspf' + | 'xul' + | 'xvm' + | 'xvml' + | 'xwd' + | 'xyz' + | 'xz' + | 'yaml' + | 'yang' + | 'yin' + | 'yml' + | 'ymp' + | 'z1' + | 'z2' + | 'z3' + | 'z4' + | 'z5' + | 'z6' + | 'z7' + | 'z8' + | 'zaz' + | 'zip' + | 'zir' + | 'zirz' + | 'zmm'; + +export const EXTENSTION_TO_MIME = { + // '123':'application/vnd.lotus-1-2-3', + '1km': 'application/vnd.1000minds.decision-model+xml', + '3dml': 'text/vnd.in3d.3dml', + '3ds': 'image/x-3ds', + '3g2': 'video/3gpp2', + '3gp': 'video/3gpp', + '3gpp': 'video/3gpp', + '3mf': 'model/3mf', + '7z': 'application/x-7z-compressed', + aab: 'application/x-authorware-bin', + aac: 'audio/x-aac', + aam: 'application/x-authorware-map', + aas: 'application/x-authorware-seg', + abw: 'application/x-abiword', + ac: 'application/vnd.nokia.n-gage.ac+xml', + acc: 'application/vnd.americandynamics.acc', + ace: 'application/x-ace-compressed', + acu: 'application/vnd.acucobol', + acutc: 'application/vnd.acucorp', + adp: 'audio/adpcm', + adts: 'audio/aac', + aep: 'application/vnd.audiograph', + afm: 'application/x-font-type1', + afp: 'application/vnd.ibm.modcap', + age: 'application/vnd.age', + ahead: 'application/vnd.ahead.space', + ai: 'application/postscript', + aif: 'audio/x-aiff', + aifc: 'audio/x-aiff', + aiff: 'audio/x-aiff', + air: 'application/vnd.adobe.air-application-installer-package+zip', + ait: 'application/vnd.dvb.ait', + ami: 'application/vnd.amiga.ami', + aml: 'application/automationml-aml+xml', + amlx: 'application/automationml-amlx+zip', + amr: 'audio/amr', + apk: 'application/vnd.android.package-archive', + apng: 'image/apng', + appcache: 'text/cache-manifest', + appinstaller: 'application/appinstaller', + application: 'application/x-ms-application', + appx: 'application/appx', + appxbundle: 'application/appxbundle', + apr: 'application/vnd.lotus-approach', + arc: 'application/x-freearc', + arj: 'application/x-arj', + asc: 'application/pgp-signature', + asf: 'video/x-ms-asf', + asm: 'text/x-asm', + aso: 'application/vnd.accpac.simply.aso', + asx: 'video/x-ms-asf', + atc: 'application/vnd.acucorp', + atom: 'application/atom+xml', + atomcat: 'application/atomcat+xml', + atomdeleted: 'application/atomdeleted+xml', + atomsvc: 'application/atomsvc+xml', + atx: 'application/vnd.antix.game-component', + au: 'audio/basic', + avci: 'image/avci', + avcs: 'image/avcs', + avi: 'video/x-msvideo', + avif: 'image/avif', + aw: 'application/applixware', + azf: 'application/vnd.airzip.filesecure.azf', + azs: 'application/vnd.airzip.filesecure.azs', + azv: 'image/vnd.airzip.accelerator.azv', + azw: 'application/vnd.amazon.ebook', + b16: 'image/vnd.pco.b16', + bat: 'application/x-msdownload', + bcpio: 'application/x-bcpio', + bdf: 'application/x-font-bdf', + bdm: 'application/vnd.syncml.dm+wbxml', + bdoc: 'application/x-bdoc', + bed: 'application/vnd.realvnc.bed', + bh2: 'application/vnd.fujitsu.oasysprs', + bin: 'application/octet-stream', + blb: 'application/x-blorb', + blorb: 'application/x-blorb', + bmi: 'application/vnd.bmi', + bmml: 'application/vnd.balsamiq.bmml+xml', + bmp: 'image/x-ms-bmp', + book: 'application/vnd.framemaker', + box: 'application/vnd.previewsystems.box', + boz: 'application/x-bzip2', + bpk: 'application/octet-stream', + bsp: 'model/vnd.valve.source.compiled-map', + btf: 'image/prs.btif', + btif: 'image/prs.btif', + buffer: 'application/octet-stream', + bz: 'application/x-bzip', + bz2: 'application/x-bzip2', + c: 'text/x-c', + c11amc: 'application/vnd.cluetrust.cartomobile-config', + c11amz: 'application/vnd.cluetrust.cartomobile-config-pkg', + c4d: 'application/vnd.clonk.c4group', + c4f: 'application/vnd.clonk.c4group', + c4g: 'application/vnd.clonk.c4group', + c4p: 'application/vnd.clonk.c4group', + c4u: 'application/vnd.clonk.c4group', + cab: 'application/vnd.ms-cab-compressed', + caf: 'audio/x-caf', + cap: 'application/vnd.tcpdump.pcap', + car: 'application/vnd.curl.car', + cat: 'application/vnd.ms-pki.seccat', + cb7: 'application/x-cbr', + cba: 'application/x-cbr', + cbr: 'application/x-cbr', + cbt: 'application/x-cbr', + cbz: 'application/x-cbr', + cc: 'text/x-c', + cco: 'application/x-cocoa', + cct: 'application/x-director', + ccxml: 'application/ccxml+xml', + cdbcmsg: 'application/vnd.contact.cmsg', + cdf: 'application/x-netcdf', + cdfx: 'application/cdfx+xml', + cdkey: 'application/vnd.mediastation.cdkey', + cdmia: 'application/cdmi-capability', + cdmic: 'application/cdmi-container', + cdmid: 'application/cdmi-domain', + cdmio: 'application/cdmi-object', + cdmiq: 'application/cdmi-queue', + cdx: 'chemical/x-cdx', + cdxml: 'application/vnd.chemdraw+xml', + cdy: 'application/vnd.cinderella', + cer: 'application/pkix-cert', + cfs: 'application/x-cfs-compressed', + cgm: 'image/cgm', + chat: 'application/x-chat', + chm: 'application/vnd.ms-htmlhelp', + chrt: 'application/vnd.kde.kchart', + cif: 'chemical/x-cif', + cii: 'application/vnd.anser-web-certificate-issue-initiation', + cil: 'application/vnd.ms-artgalry', + cjs: 'application/node', + cla: 'application/vnd.claymore', + class: 'application/java-vm', + cld: 'model/vnd.cld', + clkk: 'application/vnd.crick.clicker.keyboard', + clkp: 'application/vnd.crick.clicker.palette', + clkt: 'application/vnd.crick.clicker.template', + clkw: 'application/vnd.crick.clicker.wordbank', + clkx: 'application/vnd.crick.clicker', + clp: 'application/x-msclip', + cmc: 'application/vnd.cosmocaller', + cmdf: 'chemical/x-cmdf', + cml: 'chemical/x-cml', + cmp: 'application/vnd.yellowriver-custom-menu', + cmx: 'image/x-cmx', + cod: 'application/vnd.rim.cod', + coffee: 'text/coffeescript', + com: 'application/x-msdownload', + conf: 'text/plain', + cpio: 'application/x-cpio', + cpl: 'application/cpl+xml', + cpp: 'text/x-c', + cpt: 'application/mac-compactpro', + crd: 'application/x-mscardfile', + crl: 'application/pkix-crl', + crt: 'application/x-x509-ca-cert', + crx: 'application/x-chrome-extension', + cryptonote: 'application/vnd.rig.cryptonote', + csh: 'application/x-csh', + csl: 'application/vnd.citationstyles.style+xml', + csml: 'chemical/x-csml', + csp: 'application/vnd.commonspace', + css: 'text/css', + cst: 'application/x-director', + csv: 'text/csv', + cu: 'application/cu-seeme', + curl: 'text/vnd.curl', + cwl: 'application/cwl', + cww: 'application/prs.cww', + cxt: 'application/x-director', + cxx: 'text/x-c', + dae: 'model/vnd.collada+xml', + daf: 'application/vnd.mobius.daf', + dart: 'application/vnd.dart', + dataless: 'application/vnd.fdsn.seed', + davmount: 'application/davmount+xml', + dbf: 'application/vnd.dbf', + dbk: 'application/docbook+xml', + dcr: 'application/x-director', + dcurl: 'text/vnd.curl.dcurl', + dd2: 'application/vnd.oma.dd2+xml', + ddd: 'application/vnd.fujixerox.ddd', + ddf: 'application/vnd.syncml.dmddf+xml', + dds: 'image/vnd.ms-dds', + deb: 'application/x-debian-package', + def: 'text/plain', + deploy: 'application/octet-stream', + der: 'application/x-x509-ca-cert', + dfac: 'application/vnd.dreamfactory', + dgc: 'application/x-dgc-compressed', + dib: 'image/bmp', + dic: 'text/x-c', + dir: 'application/x-director', + dis: 'application/vnd.mobius.dis', + 'disposition-notification': 'message/disposition-notification', + dist: 'application/octet-stream', + distz: 'application/octet-stream', + djv: 'image/vnd.djvu', + djvu: 'image/vnd.djvu', + dll: 'application/x-msdownload', + dmg: 'application/x-apple-diskimage', + dmp: 'application/vnd.tcpdump.pcap', + dms: 'application/octet-stream', + dna: 'application/vnd.dna', + doc: 'application/msword', + docm: 'application/vnd.ms-word.document.macroenabled.12', + docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + dot: 'application/msword', + dotm: 'application/vnd.ms-word.template.macroenabled.12', + dotx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.template', + dp: 'application/vnd.osgi.dp', + dpg: 'application/vnd.dpgraph', + dpx: 'image/dpx', + dra: 'audio/vnd.dra', + drle: 'image/dicom-rle', + dsc: 'text/prs.lines.tag', + dssc: 'application/dssc+der', + dtb: 'application/x-dtbook+xml', + dtd: 'application/xml-dtd', + dts: 'audio/vnd.dts', + dtshd: 'audio/vnd.dts.hd', + dump: 'application/octet-stream', + dvb: 'video/vnd.dvb.file', + dvi: 'application/x-dvi', + dwd: 'application/atsc-dwd+xml', + dwf: 'model/vnd.dwf', + dwg: 'image/vnd.dwg', + dxf: 'image/vnd.dxf', + dxp: 'application/vnd.spotfire.dxp', + dxr: 'application/x-director', + ear: 'application/java-archive', + ecelp4800: 'audio/vnd.nuera.ecelp4800', + ecelp7470: 'audio/vnd.nuera.ecelp7470', + ecelp9600: 'audio/vnd.nuera.ecelp9600', + ecma: 'application/ecmascript', + edm: 'application/vnd.novadigm.edm', + edx: 'application/vnd.novadigm.edx', + efif: 'application/vnd.picsel', + ei6: 'application/vnd.pg.osasli', + elc: 'application/octet-stream', + emf: 'image/emf', + eml: 'message/rfc822', + emma: 'application/emma+xml', + emotionml: 'application/emotionml+xml', + emz: 'application/x-msmetafile', + eol: 'audio/vnd.digital-winds', + eot: 'application/vnd.ms-fontobject', + eps: 'application/postscript', + epub: 'application/epub+zip', + es3: 'application/vnd.eszigno3+xml', + esa: 'application/vnd.osgi.subsystem', + esf: 'application/vnd.epson.esf', + et3: 'application/vnd.eszigno3+xml', + etx: 'text/x-setext', + eva: 'application/x-eva', + evy: 'application/x-envoy', + exe: 'application/x-msdownload', + exi: 'application/exi', + exp: 'application/express', + exr: 'image/aces', + ext: 'application/vnd.novadigm.ext', + ez: 'application/andrew-inset', + ez2: 'application/vnd.ezpix-album', + ez3: 'application/vnd.ezpix-package', + f: 'text/x-fortran', + f4v: 'video/x-f4v', + f77: 'text/x-fortran', + f90: 'text/x-fortran', + fbs: 'image/vnd.fastbidsheet', + fcdt: 'application/vnd.adobe.formscentral.fcdt', + fcs: 'application/vnd.isac.fcs', + fdf: 'application/vnd.fdf', + fdt: 'application/fdt+xml', + fe_launch: 'application/vnd.denovo.fcselayout-link', + fg5: 'application/vnd.fujitsu.oasysgp', + fgd: 'application/x-director', + fh: 'image/x-freehand', + fh4: 'image/x-freehand', + fh5: 'image/x-freehand', + fh7: 'image/x-freehand', + fhc: 'image/x-freehand', + fig: 'application/x-xfig', + fits: 'image/fits', + flac: 'audio/x-flac', + fli: 'video/x-fli', + flo: 'application/vnd.micrografx.flo', + flv: 'video/x-flv', + flw: 'application/vnd.kde.kivio', + flx: 'text/vnd.fmi.flexstor', + fly: 'text/vnd.fly', + fm: 'application/vnd.framemaker', + fnc: 'application/vnd.frogans.fnc', + fo: 'application/vnd.software602.filler.form+xml', + for: 'text/x-fortran', + fpx: 'image/vnd.fpx', + frame: 'application/vnd.framemaker', + fsc: 'application/vnd.fsc.weblaunch', + fst: 'image/vnd.fst', + ftc: 'application/vnd.fluxtime.clip', + fti: 'application/vnd.anser-web-funds-transfer-initiation', + fvt: 'video/vnd.fvt', + fxp: 'application/vnd.adobe.fxp', + fxpl: 'application/vnd.adobe.fxp', + fzs: 'application/vnd.fuzzysheet', + g2w: 'application/vnd.geoplan', + g3: 'image/g3fax', + g3w: 'application/vnd.geospace', + gac: 'application/vnd.groove-account', + gam: 'application/x-tads', + gbr: 'application/rpki-ghostbusters', + gca: 'application/x-gca-compressed', + gdl: 'model/vnd.gdl', + gdoc: 'application/vnd.google-apps.document', + ged: 'text/vnd.familysearch.gedcom', + geo: 'application/vnd.dynageo', + geojson: 'application/geo+json', + gex: 'application/vnd.geometry-explorer', + ggb: 'application/vnd.geogebra.file', + ggt: 'application/vnd.geogebra.tool', + ghf: 'application/vnd.groove-help', + gif: 'image/gif', + gim: 'application/vnd.groove-identity-message', + glb: 'model/gltf-binary', + gltf: 'model/gltf+json', + gml: 'application/gml+xml', + gmx: 'application/vnd.gmx', + gnumeric: 'application/x-gnumeric', + gph: 'application/vnd.flographit', + gpx: 'application/gpx+xml', + gqf: 'application/vnd.grafeq', + gqs: 'application/vnd.grafeq', + gram: 'application/srgs', + gramps: 'application/x-gramps-xml', + gre: 'application/vnd.geometry-explorer', + grv: 'application/vnd.groove-injector', + grxml: 'application/srgs+xml', + gsf: 'application/x-font-ghostscript', + gsheet: 'application/vnd.google-apps.spreadsheet', + gslides: 'application/vnd.google-apps.presentation', + gtar: 'application/x-gtar', + gtm: 'application/vnd.groove-tool-message', + gtw: 'model/vnd.gtw', + gv: 'text/vnd.graphviz', + gxf: 'application/gxf', + gxt: 'application/vnd.geonext', + gz: 'application/gzip', + h: 'text/x-c', + h261: 'video/h261', + h263: 'video/h263', + h264: 'video/h264', + hal: 'application/vnd.hal+xml', + hbci: 'application/vnd.hbci', + hbs: 'text/x-handlebars-template', + hdd: 'application/x-virtualbox-hdd', + hdf: 'application/x-hdf', + heic: 'image/heic', + heics: 'image/heic-sequence', + heif: 'image/heif', + heifs: 'image/heif-sequence', + hej2: 'image/hej2k', + held: 'application/atsc-held+xml', + hh: 'text/x-c', + hjson: 'application/hjson', + hlp: 'application/winhlp', + hpgl: 'application/vnd.hp-hpgl', + hpid: 'application/vnd.hp-hpid', + hps: 'application/vnd.hp-hps', + hqx: 'application/mac-binhex40', + hsj2: 'image/hsj2', + htc: 'text/x-component', + htke: 'application/vnd.kenameaapp', + htm: 'text/html', + html: 'text/html', + hvd: 'application/vnd.yamaha.hv-dic', + hvp: 'application/vnd.yamaha.hv-voice', + hvs: 'application/vnd.yamaha.hv-script', + i2g: 'application/vnd.intergeo', + icc: 'application/vnd.iccprofile', + ice: 'x-conference/x-cooltalk', + icm: 'application/vnd.iccprofile', + ico: 'image/x-icon', + ics: 'text/calendar', + ief: 'image/ief', + ifb: 'text/calendar', + ifm: 'application/vnd.shana.informed.formdata', + iges: 'model/iges', + igl: 'application/vnd.igloader', + igm: 'application/vnd.insors.igm', + igs: 'model/iges', + igx: 'application/vnd.micrografx.igx', + iif: 'application/vnd.shana.informed.interchange', + img: 'application/octet-stream', + imp: 'application/vnd.accpac.simply.imp', + ims: 'application/vnd.ms-ims', + in: 'text/plain', + ini: 'text/plain', + ink: 'application/inkml+xml', + inkml: 'application/inkml+xml', + install: 'application/x-install-instructions', + iota: 'application/vnd.astraea-software.iota', + ipfix: 'application/ipfix', + ipk: 'application/vnd.shana.informed.package', + irm: 'application/vnd.ibm.rights-management', + irp: 'application/vnd.irepository.package+xml', + iso: 'application/x-iso9660-image', + itp: 'application/vnd.shana.informed.formtemplate', + its: 'application/its+xml', + ivp: 'application/vnd.immervision-ivp', + ivu: 'application/vnd.immervision-ivu', + jad: 'text/vnd.sun.j2me.app-descriptor', + jade: 'text/jade', + jam: 'application/vnd.jam', + jar: 'application/java-archive', + jardiff: 'application/x-java-archive-diff', + java: 'text/x-java-source', + jhc: 'image/jphc', + jisp: 'application/vnd.jisp', + jls: 'image/jls', + jlt: 'application/vnd.hp-jlyt', + jng: 'image/x-jng', + jnlp: 'application/x-java-jnlp-file', + joda: 'application/vnd.joost.joda-archive', + jp2: 'image/jp2', + jpe: 'image/jpeg', + jpeg: 'image/jpeg', + jpf: 'image/jpx', + jpg: 'image/jpeg', + jpg2: 'image/jp2', + jpgm: 'video/jpm', + jpgv: 'video/jpeg', + jph: 'image/jph', + jpm: 'video/jpm', + jpx: 'image/jpx', + js: 'text/javascript', + json: 'application/json', + json5: 'application/json5', + jsonld: 'application/ld+json', + jsonml: 'application/jsonml+json', + jsx: 'text/jsx', + jt: 'model/jt', + jxr: 'image/jxr', + jxra: 'image/jxra', + jxrs: 'image/jxrs', + jxs: 'image/jxs', + jxsc: 'image/jxsc', + jxsi: 'image/jxsi', + jxss: 'image/jxss', + kar: 'audio/midi', + karbon: 'application/vnd.kde.karbon', + kdbx: 'application/x-keepass2', + key: 'application/x-iwork-keynote-sffkey', + kfo: 'application/vnd.kde.kformula', + kia: 'application/vnd.kidspiration', + kml: 'application/vnd.google-earth.kml+xml', + kmz: 'application/vnd.google-earth.kmz', + kne: 'application/vnd.kinar', + knp: 'application/vnd.kinar', + kon: 'application/vnd.kde.kontour', + kpr: 'application/vnd.kde.kpresenter', + kpt: 'application/vnd.kde.kpresenter', + kpxx: 'application/vnd.ds-keypoint', + ksp: 'application/vnd.kde.kspread', + ktr: 'application/vnd.kahootz', + ktx: 'image/ktx', + ktx2: 'image/ktx2', + ktz: 'application/vnd.kahootz', + kwd: 'application/vnd.kde.kword', + kwt: 'application/vnd.kde.kword', + lasxml: 'application/vnd.las.las+xml', + latex: 'application/x-latex', + lbd: 'application/vnd.llamagraphics.life-balance.desktop', + lbe: 'application/vnd.llamagraphics.life-balance.exchange+xml', + les: 'application/vnd.hhe.lesson-player', + less: 'text/less', + lgr: 'application/lgr+xml', + lha: 'application/x-lzh-compressed', + link66: 'application/vnd.route66.link66+xml', + list: 'text/plain', + list3820: 'application/vnd.ibm.modcap', + listafp: 'application/vnd.ibm.modcap', + litcoffee: 'text/coffeescript', + lnk: 'application/x-ms-shortcut', + log: 'text/plain', + lostxml: 'application/lost+xml', + lrf: 'application/octet-stream', + lrm: 'application/vnd.ms-lrm', + ltf: 'application/vnd.frogans.ltf', + lua: 'text/x-lua', + luac: 'application/x-lua-bytecode', + lvp: 'audio/vnd.lucent.voice', + lwp: 'application/vnd.lotus-wordpro', + lzh: 'application/x-lzh-compressed', + m13: 'application/x-msmediaview', + m14: 'application/x-msmediaview', + m1v: 'video/mpeg', + m21: 'application/mp21', + m2a: 'audio/mpeg', + m2v: 'video/mpeg', + m3a: 'audio/mpeg', + m3u: 'audio/x-mpegurl', + m3u8: 'application/vnd.apple.mpegurl', + m4a: 'audio/x-m4a', + m4p: 'application/mp4', + m4s: 'video/iso.segment', + m4u: 'video/vnd.mpegurl', + m4v: 'video/x-m4v', + ma: 'application/mathematica', + mads: 'application/mads+xml', + maei: 'application/mmt-aei+xml', + mag: 'application/vnd.ecowin.chart', + maker: 'application/vnd.framemaker', + man: 'text/troff', + manifest: 'text/cache-manifest', + map: 'application/json', + mar: 'application/octet-stream', + markdown: 'text/markdown', + mathml: 'application/mathml+xml', + mb: 'application/mathematica', + mbk: 'application/vnd.mobius.mbk', + mbox: 'application/mbox', + mc1: 'application/vnd.medcalcdata', + mcd: 'application/vnd.mcd', + mcurl: 'text/vnd.curl.mcurl', + md: 'text/markdown', + mdb: 'application/x-msaccess', + mdi: 'image/vnd.ms-modi', + mdx: 'text/mdx', + me: 'text/troff', + mesh: 'model/mesh', + meta4: 'application/metalink4+xml', + metalink: 'application/metalink+xml', + mets: 'application/mets+xml', + mfm: 'application/vnd.mfmp', + mft: 'application/rpki-manifest', + mgp: 'application/vnd.osgeo.mapguide.package', + mgz: 'application/vnd.proteus.magazine', + mid: 'audio/midi', + midi: 'audio/midi', + mie: 'application/x-mie', + mif: 'application/vnd.mif', + mime: 'message/rfc822', + mj2: 'video/mj2', + mjp2: 'video/mj2', + mjs: 'text/javascript', + mk3d: 'video/x-matroska', + mka: 'audio/x-matroska', + mkd: 'text/x-markdown', + mks: 'video/x-matroska', + mkv: 'video/x-matroska', + mlp: 'application/vnd.dolby.mlp', + mmd: 'application/vnd.chipnuts.karaoke-mmd', + mmf: 'application/vnd.smaf', + mml: 'text/mathml', + mmr: 'image/vnd.fujixerox.edmics-mmr', + mng: 'video/x-mng', + mny: 'application/x-msmoney', + mobi: 'application/x-mobipocket-ebook', + mods: 'application/mods+xml', + mov: 'video/quicktime', + movie: 'video/x-sgi-movie', + mp2: 'audio/mpeg', + mp21: 'application/mp21', + mp2a: 'audio/mpeg', + mp3: 'audio/mpeg', + mp4: 'video/mp4', + mp4a: 'audio/mp4', + mp4s: 'application/mp4', + mp4v: 'video/mp4', + mpc: 'application/vnd.mophun.certificate', + mpd: 'application/dash+xml', + mpe: 'video/mpeg', + mpeg: 'video/mpeg', + mpf: 'application/media-policy-dataset+xml', + mpg: 'video/mpeg', + mpg4: 'video/mp4', + mpga: 'audio/mpeg', + mpkg: 'application/vnd.apple.installer+xml', + mpm: 'application/vnd.blueice.multipass', + mpn: 'application/vnd.mophun.application', + mpp: 'application/vnd.ms-project', + mpt: 'application/vnd.ms-project', + mpy: 'application/vnd.ibm.minipay', + mqy: 'application/vnd.mobius.mqy', + mrc: 'application/marc', + mrcx: 'application/marcxml+xml', + ms: 'text/troff', + mscml: 'application/mediaservercontrol+xml', + mseed: 'application/vnd.fdsn.mseed', + mseq: 'application/vnd.mseq', + msf: 'application/vnd.epson.msf', + msg: 'application/vnd.ms-outlook', + msh: 'model/mesh', + msi: 'application/x-msdownload', + msix: 'application/msix', + msixbundle: 'application/msixbundle', + msl: 'application/vnd.mobius.msl', + msm: 'application/octet-stream', + msp: 'application/octet-stream', + msty: 'application/vnd.muvee.style', + mtl: 'model/mtl', + mts: 'model/vnd.mts', + mus: 'application/vnd.musician', + musd: 'application/mmt-usd+xml', + musicxml: 'application/vnd.recordare.musicxml+xml', + mvb: 'application/x-msmediaview', + mvt: 'application/vnd.mapbox-vector-tile', + mwf: 'application/vnd.mfer', + mxf: 'application/mxf', + mxl: 'application/vnd.recordare.musicxml', + mxmf: 'audio/mobile-xmf', + mxml: 'application/xv+xml', + mxs: 'application/vnd.triscape.mxs', + mxu: 'video/vnd.mpegurl', + 'n-gage': 'application/vnd.nokia.n-gage.symbian.install', + n3: 'text/n3', + nb: 'application/mathematica', + nbp: 'application/vnd.wolfram.player', + nc: 'application/x-netcdf', + ncx: 'application/x-dtbncx+xml', + nfo: 'text/x-nfo', + ngdat: 'application/vnd.nokia.n-gage.data', + nitf: 'application/vnd.nitf', + nlu: 'application/vnd.neurolanguage.nlu', + nml: 'application/vnd.enliven', + nnd: 'application/vnd.noblenet-directory', + nns: 'application/vnd.noblenet-sealer', + nnw: 'application/vnd.noblenet-web', + npx: 'image/vnd.net-fpx', + nq: 'application/n-quads', + nsc: 'application/x-conference', + nsf: 'application/vnd.lotus-notes', + nt: 'application/n-triples', + ntf: 'application/vnd.nitf', + numbers: 'application/x-iwork-numbers-sffnumbers', + nzb: 'application/x-nzb', + oa2: 'application/vnd.fujitsu.oasys2', + oa3: 'application/vnd.fujitsu.oasys3', + oas: 'application/vnd.fujitsu.oasys', + obd: 'application/x-msbinder', + obgx: 'application/vnd.openblox.game+xml', + obj: 'model/obj', + oda: 'application/oda', + odb: 'application/vnd.oasis.opendocument.database', + odc: 'application/vnd.oasis.opendocument.chart', + odf: 'application/vnd.oasis.opendocument.formula', + odft: 'application/vnd.oasis.opendocument.formula-template', + odg: 'application/vnd.oasis.opendocument.graphics', + odi: 'application/vnd.oasis.opendocument.image', + odm: 'application/vnd.oasis.opendocument.text-master', + odp: 'application/vnd.oasis.opendocument.presentation', + ods: 'application/vnd.oasis.opendocument.spreadsheet', + odt: 'application/vnd.oasis.opendocument.text', + oga: 'audio/ogg', + ogex: 'model/vnd.opengex', + ogg: 'audio/ogg', + ogv: 'video/ogg', + ogx: 'application/ogg', + omdoc: 'application/omdoc+xml', + onepkg: 'application/onenote', + onetmp: 'application/onenote', + onetoc: 'application/onenote', + onetoc2: 'application/onenote', + opf: 'application/oebps-package+xml', + opml: 'text/x-opml', + oprc: 'application/vnd.palm', + opus: 'audio/ogg', + org: 'text/x-org', + osf: 'application/vnd.yamaha.openscoreformat', + osfpvg: 'application/vnd.yamaha.openscoreformat.osfpvg+xml', + osm: 'application/vnd.openstreetmap.data+xml', + otc: 'application/vnd.oasis.opendocument.chart-template', + otf: 'font/otf', + otg: 'application/vnd.oasis.opendocument.graphics-template', + oth: 'application/vnd.oasis.opendocument.text-web', + oti: 'application/vnd.oasis.opendocument.image-template', + otp: 'application/vnd.oasis.opendocument.presentation-template', + ots: 'application/vnd.oasis.opendocument.spreadsheet-template', + ott: 'application/vnd.oasis.opendocument.text-template', + ova: 'application/x-virtualbox-ova', + ovf: 'application/x-virtualbox-ovf', + owl: 'application/rdf+xml', + oxps: 'application/oxps', + oxt: 'application/vnd.openofficeorg.extension', + p: 'text/x-pascal', + p10: 'application/pkcs10', + p12: 'application/x-pkcs12', + p7b: 'application/x-pkcs7-certificates', + p7c: 'application/pkcs7-mime', + p7m: 'application/pkcs7-mime', + p7r: 'application/x-pkcs7-certreqresp', + p7s: 'application/pkcs7-signature', + p8: 'application/pkcs8', + pac: 'application/x-ns-proxy-autoconfig', + pages: 'application/x-iwork-pages-sffpages', + pas: 'text/x-pascal', + paw: 'application/vnd.pawaafile', + pbd: 'application/vnd.powerbuilder6', + pbm: 'image/x-portable-bitmap', + pcap: 'application/vnd.tcpdump.pcap', + pcf: 'application/x-font-pcf', + pcl: 'application/vnd.hp-pcl', + pclxl: 'application/vnd.hp-pclxl', + pct: 'image/x-pict', + pcurl: 'application/vnd.curl.pcurl', + pcx: 'image/x-pcx', + pdb: 'application/x-pilot', + pde: 'text/x-processing', + pdf: 'application/pdf', + pem: 'application/x-x509-ca-cert', + pfa: 'application/x-font-type1', + pfb: 'application/x-font-type1', + pfm: 'application/x-font-type1', + pfr: 'application/font-tdpfr', + pfx: 'application/x-pkcs12', + pgm: 'image/x-portable-graymap', + pgn: 'application/x-chess-pgn', + pgp: 'application/pgp-encrypted', + php: 'application/x-httpd-php', + pic: 'image/x-pict', + pkg: 'application/octet-stream', + pki: 'application/pkixcmp', + pkipath: 'application/pkix-pkipath', + pkpass: 'application/vnd.apple.pkpass', + pl: 'application/x-perl', + plb: 'application/vnd.3gpp.pic-bw-large', + plc: 'application/vnd.mobius.plc', + plf: 'application/vnd.pocketlearn', + pls: 'application/pls+xml', + pm: 'application/x-perl', + pml: 'application/vnd.ctc-posml', + png: 'image/png', + pnm: 'image/x-portable-anymap', + portpkg: 'application/vnd.macports.portpkg', + pot: 'application/vnd.ms-powerpoint', + potm: 'application/vnd.ms-powerpoint.template.macroenabled.12', + potx: 'application/vnd.openxmlformats-officedocument.presentationml.template', + ppam: 'application/vnd.ms-powerpoint.addin.macroenabled.12', + ppd: 'application/vnd.cups-ppd', + ppm: 'image/x-portable-pixmap', + pps: 'application/vnd.ms-powerpoint', + ppsm: 'application/vnd.ms-powerpoint.slideshow.macroenabled.12', + ppsx: 'application/vnd.openxmlformats-officedocument.presentationml.slideshow', + ppt: 'application/vnd.ms-powerpoint', + pptm: 'application/vnd.ms-powerpoint.presentation.macroenabled.12', + pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + pqa: 'application/vnd.palm', + prc: 'model/prc', + pre: 'application/vnd.lotus-freelance', + prf: 'application/pics-rules', + provx: 'application/provenance+xml', + ps: 'application/postscript', + psb: 'application/vnd.3gpp.pic-bw-small', + psd: 'image/vnd.adobe.photoshop', + psf: 'application/x-font-linux-psf', + pskcxml: 'application/pskc+xml', + pti: 'image/prs.pti', + ptid: 'application/vnd.pvi.ptid1', + pub: 'application/x-mspublisher', + pvb: 'application/vnd.3gpp.pic-bw-var', + pwn: 'application/vnd.3m.post-it-notes', + pya: 'audio/vnd.ms-playready.media.pya', + pyo: 'model/vnd.pytha.pyox', + pyox: 'model/vnd.pytha.pyox', + pyv: 'video/vnd.ms-playready.media.pyv', + qam: 'application/vnd.epson.quickanime', + qbo: 'application/vnd.intu.qbo', + qfx: 'application/vnd.intu.qfx', + qps: 'application/vnd.publishare-delta-tree', + qt: 'video/quicktime', + qwd: 'application/vnd.quark.quarkxpress', + qwt: 'application/vnd.quark.quarkxpress', + qxb: 'application/vnd.quark.quarkxpress', + qxd: 'application/vnd.quark.quarkxpress', + qxl: 'application/vnd.quark.quarkxpress', + qxt: 'application/vnd.quark.quarkxpress', + ra: 'audio/x-realaudio', + ram: 'audio/x-pn-realaudio', + raml: 'application/raml+yaml', + rapd: 'application/route-apd+xml', + rar: 'application/x-rar-compressed', + ras: 'image/x-cmu-raster', + rcprofile: 'application/vnd.ipunplugged.rcprofile', + rdf: 'application/rdf+xml', + rdz: 'application/vnd.data-vision.rdz', + relo: 'application/p2p-overlay+xml', + rep: 'application/vnd.businessobjects', + res: 'application/x-dtbresource+xml', + rgb: 'image/x-rgb', + rif: 'application/reginfo+xml', + rip: 'audio/vnd.rip', + ris: 'application/x-research-info-systems', + rl: 'application/resource-lists+xml', + rlc: 'image/vnd.fujixerox.edmics-rlc', + rld: 'application/resource-lists-diff+xml', + rm: 'application/vnd.rn-realmedia', + rmi: 'audio/midi', + rmp: 'audio/x-pn-realaudio-plugin', + rms: 'application/vnd.jcp.javame.midlet-rms', + rmvb: 'application/vnd.rn-realmedia-vbr', + rnc: 'application/relax-ng-compact-syntax', + rng: 'application/xml', + roa: 'application/rpki-roa', + roff: 'text/troff', + rp9: 'application/vnd.cloanto.rp9', + rpm: 'application/x-redhat-package-manager', + rpss: 'application/vnd.nokia.radio-presets', + rpst: 'application/vnd.nokia.radio-preset', + rq: 'application/sparql-query', + rs: 'application/rls-services+xml', + rsat: 'application/atsc-rsat+xml', + rsd: 'application/rsd+xml', + rsheet: 'application/urc-ressheet+xml', + rss: 'application/rss+xml', + rtf: 'text/rtf', + rtx: 'text/richtext', + run: 'application/x-makeself', + rusd: 'application/route-usd+xml', + s: 'text/x-asm', + s3m: 'audio/s3m', + saf: 'application/vnd.yamaha.smaf-audio', + sass: 'text/x-sass', + sbml: 'application/sbml+xml', + sc: 'application/vnd.ibm.secure-container', + scd: 'application/x-msschedule', + scm: 'application/vnd.lotus-screencam', + scq: 'application/scvp-cv-request', + scs: 'application/scvp-cv-response', + scss: 'text/x-scss', + scurl: 'text/vnd.curl.scurl', + sda: 'application/vnd.stardivision.draw', + sdc: 'application/vnd.stardivision.calc', + sdd: 'application/vnd.stardivision.impress', + sdkd: 'application/vnd.solent.sdkm+xml', + sdkm: 'application/vnd.solent.sdkm+xml', + sdp: 'application/sdp', + sdw: 'application/vnd.stardivision.writer', + sea: 'application/x-sea', + see: 'application/vnd.seemail', + seed: 'application/vnd.fdsn.seed', + sema: 'application/vnd.sema', + semd: 'application/vnd.semd', + semf: 'application/vnd.semf', + senmlx: 'application/senml+xml', + sensmlx: 'application/sensml+xml', + ser: 'application/java-serialized-object', + setpay: 'application/set-payment-initiation', + setreg: 'application/set-registration-initiation', + 'sfd-hdstx': 'application/vnd.hydrostatix.sof-data', + sfs: 'application/vnd.spotfire.sfs', + sfv: 'text/x-sfv', + sgi: 'image/sgi', + sgl: 'application/vnd.stardivision.writer-global', + sgm: 'text/sgml', + sgml: 'text/sgml', + sh: 'application/x-sh', + shar: 'application/x-shar', + shex: 'text/shex', + shf: 'application/shf+xml', + shtml: 'text/html', + sid: 'image/x-mrsid-image', + sieve: 'application/sieve', + sig: 'application/pgp-signature', + sil: 'audio/silk', + silo: 'model/mesh', + sis: 'application/vnd.symbian.install', + sisx: 'application/vnd.symbian.install', + sit: 'application/x-stuffit', + sitx: 'application/x-stuffitx', + siv: 'application/sieve', + skd: 'application/vnd.koan', + skm: 'application/vnd.koan', + skp: 'application/vnd.koan', + skt: 'application/vnd.koan', + sldm: 'application/vnd.ms-powerpoint.slide.macroenabled.12', + sldx: 'application/vnd.openxmlformats-officedocument.presentationml.slide', + slim: 'text/slim', + slm: 'text/slim', + sls: 'application/route-s-tsid+xml', + slt: 'application/vnd.epson.salt', + sm: 'application/vnd.stepmania.stepchart', + smf: 'application/vnd.stardivision.math', + smi: 'application/smil+xml', + smil: 'application/smil+xml', + smv: 'video/x-smv', + smzip: 'application/vnd.stepmania.package', + snd: 'audio/basic', + snf: 'application/x-font-snf', + so: 'application/octet-stream', + spc: 'application/x-pkcs7-certificates', + spdx: 'text/spdx', + spf: 'application/vnd.yamaha.smaf-phrase', + spl: 'application/x-futuresplash', + spot: 'text/vnd.in3d.spot', + spp: 'application/scvp-vp-response', + spq: 'application/scvp-vp-request', + spx: 'audio/ogg', + sql: 'application/x-sql', + src: 'application/x-wais-source', + srt: 'application/x-subrip', + sru: 'application/sru+xml', + srx: 'application/sparql-results+xml', + ssdl: 'application/ssdl+xml', + sse: 'application/vnd.kodak-descriptor', + ssf: 'application/vnd.epson.ssf', + ssml: 'application/ssml+xml', + st: 'application/vnd.sailingtracker.track', + stc: 'application/vnd.sun.xml.calc.template', + std: 'application/vnd.sun.xml.draw.template', + stf: 'application/vnd.wt.stf', + sti: 'application/vnd.sun.xml.impress.template', + stk: 'application/hyperstudio', + stl: 'model/stl', + stpx: 'model/step+xml', + stpxz: 'model/step-xml+zip', + stpz: 'model/step+zip', + str: 'application/vnd.pg.format', + stw: 'application/vnd.sun.xml.writer.template', + styl: 'text/stylus', + stylus: 'text/stylus', + sub: 'text/vnd.dvb.subtitle', + sus: 'application/vnd.sus-calendar', + susp: 'application/vnd.sus-calendar', + sv4cpio: 'application/x-sv4cpio', + sv4crc: 'application/x-sv4crc', + svc: 'application/vnd.dvb.service', + svd: 'application/vnd.svd', + svg: 'image/svg+xml', + svgz: 'image/svg+xml', + swa: 'application/x-director', + swf: 'application/x-shockwave-flash', + swi: 'application/vnd.aristanetworks.swi', + swidtag: 'application/swid+xml', + sxc: 'application/vnd.sun.xml.calc', + sxd: 'application/vnd.sun.xml.draw', + sxg: 'application/vnd.sun.xml.writer.global', + sxi: 'application/vnd.sun.xml.impress', + sxm: 'application/vnd.sun.xml.math', + sxw: 'application/vnd.sun.xml.writer', + t: 'text/troff', + t3: 'application/x-t3vm-image', + t38: 'image/t38', + taglet: 'application/vnd.mynfc', + tao: 'application/vnd.tao.intent-module-archive', + tap: 'image/vnd.tencent.tap', + tar: 'application/x-tar', + tcap: 'application/vnd.3gpp2.tcap', + tcl: 'application/x-tcl', + td: 'application/urc-targetdesc+xml', + teacher: 'application/vnd.smart.teacher', + tei: 'application/tei+xml', + teicorpus: 'application/tei+xml', + tex: 'application/x-tex', + texi: 'application/x-texinfo', + texinfo: 'application/x-texinfo', + text: 'text/plain', + tfi: 'application/thraud+xml', + tfm: 'application/x-tex-tfm', + tfx: 'image/tiff-fx', + tga: 'image/x-tga', + thmx: 'application/vnd.ms-officetheme', + tif: 'image/tiff', + tiff: 'image/tiff', + tk: 'application/x-tcl', + tmo: 'application/vnd.tmobile-livetv', + toml: 'application/toml', + torrent: 'application/x-bittorrent', + tpl: 'application/vnd.groove-tool-template', + tpt: 'application/vnd.trid.tpt', + tr: 'text/troff', + tra: 'application/vnd.trueapp', + trig: 'application/trig', + trm: 'application/x-msterminal', + ts: 'video/mp2t', + tsd: 'application/timestamped-data', + tsv: 'text/tab-separated-values', + ttc: 'font/collection', + ttf: 'font/ttf', + ttl: 'text/turtle', + ttml: 'application/ttml+xml', + twd: 'application/vnd.simtech-mindmapper', + twds: 'application/vnd.simtech-mindmapper', + txd: 'application/vnd.genomatix.tuxedo', + txf: 'application/vnd.mobius.txf', + txt: 'text/plain', + u32: 'application/x-authorware-bin', + u3d: 'model/u3d', + u8dsn: 'message/global-delivery-status', + u8hdr: 'message/global-headers', + u8mdn: 'message/global-disposition-notification', + u8msg: 'message/global', + ubj: 'application/ubjson', + udeb: 'application/x-debian-package', + ufd: 'application/vnd.ufdl', + ufdl: 'application/vnd.ufdl', + ulx: 'application/x-glulx', + umj: 'application/vnd.umajin', + unityweb: 'application/vnd.unity', + uo: 'application/vnd.uoml+xml', + uoml: 'application/vnd.uoml+xml', + uri: 'text/uri-list', + uris: 'text/uri-list', + urls: 'text/uri-list', + usda: 'model/vnd.usda', + usdz: 'model/vnd.usdz+zip', + ustar: 'application/x-ustar', + utz: 'application/vnd.uiq.theme', + uu: 'text/x-uuencode', + uva: 'audio/vnd.dece.audio', + uvd: 'application/vnd.dece.data', + uvf: 'application/vnd.dece.data', + uvg: 'image/vnd.dece.graphic', + uvh: 'video/vnd.dece.hd', + uvi: 'image/vnd.dece.graphic', + uvm: 'video/vnd.dece.mobile', + uvp: 'video/vnd.dece.pd', + uvs: 'video/vnd.dece.sd', + uvt: 'application/vnd.dece.ttml+xml', + uvu: 'video/vnd.uvvu.mp4', + uvv: 'video/vnd.dece.video', + uvva: 'audio/vnd.dece.audio', + uvvd: 'application/vnd.dece.data', + uvvf: 'application/vnd.dece.data', + uvvg: 'image/vnd.dece.graphic', + uvvh: 'video/vnd.dece.hd', + uvvi: 'image/vnd.dece.graphic', + uvvm: 'video/vnd.dece.mobile', + uvvp: 'video/vnd.dece.pd', + uvvs: 'video/vnd.dece.sd', + uvvt: 'application/vnd.dece.ttml+xml', + uvvu: 'video/vnd.uvvu.mp4', + uvvv: 'video/vnd.dece.video', + uvvx: 'application/vnd.dece.unspecified', + uvvz: 'application/vnd.dece.zip', + uvx: 'application/vnd.dece.unspecified', + uvz: 'application/vnd.dece.zip', + vbox: 'application/x-virtualbox-vbox', + 'vbox-extpack': 'application/x-virtualbox-vbox-extpack', + vcard: 'text/vcard', + vcd: 'application/x-cdlink', + vcf: 'text/x-vcard', + vcg: 'application/vnd.groove-vcard', + vcs: 'text/x-vcalendar', + vcx: 'application/vnd.vcx', + vdi: 'application/x-virtualbox-vdi', + vds: 'model/vnd.sap.vds', + vhd: 'application/x-virtualbox-vhd', + vis: 'application/vnd.visionary', + viv: 'video/vnd.vivo', + vmdk: 'application/x-virtualbox-vmdk', + vob: 'video/x-ms-vob', + vor: 'application/vnd.stardivision.writer', + vox: 'application/x-authorware-bin', + vrml: 'model/vrml', + vsd: 'application/vnd.visio', + vsf: 'application/vnd.vsf', + vss: 'application/vnd.visio', + vst: 'application/vnd.visio', + vsw: 'application/vnd.visio', + vtf: 'image/vnd.valve.source.texture', + vtt: 'text/vtt', + vtu: 'model/vnd.vtu', + vxml: 'application/voicexml+xml', + w3d: 'application/x-director', + wad: 'application/x-doom', + wadl: 'application/vnd.sun.wadl+xml', + war: 'application/java-archive', + wasm: 'application/wasm', + wav: 'audio/x-wav', + wax: 'audio/x-ms-wax', + wbmp: 'image/vnd.wap.wbmp', + wbs: 'application/vnd.criticaltools.wbs+xml', + wbxml: 'application/vnd.wap.wbxml', + wcm: 'application/vnd.ms-works', + wdb: 'application/vnd.ms-works', + wdp: 'image/vnd.ms-photo', + weba: 'audio/webm', + webapp: 'application/x-web-app-manifest+json', + webm: 'video/webm', + webmanifest: 'application/manifest+json', + webp: 'image/webp', + wg: 'application/vnd.pmi.widget', + wgsl: 'text/wgsl', + wgt: 'application/widget', + wif: 'application/watcherinfo+xml', + wks: 'application/vnd.ms-works', + wm: 'video/x-ms-wm', + wma: 'audio/x-ms-wma', + wmd: 'application/x-ms-wmd', + wmf: 'image/wmf', + wml: 'text/vnd.wap.wml', + wmlc: 'application/vnd.wap.wmlc', + wmls: 'text/vnd.wap.wmlscript', + wmlsc: 'application/vnd.wap.wmlscriptc', + wmv: 'video/x-ms-wmv', + wmx: 'video/x-ms-wmx', + wmz: 'application/x-msmetafile', + woff: 'font/woff', + woff2: 'font/woff2', + wpd: 'application/vnd.wordperfect', + wpl: 'application/vnd.ms-wpl', + wps: 'application/vnd.ms-works', + wqd: 'application/vnd.wqd', + wri: 'application/x-mswrite', + wrl: 'model/vrml', + wsc: 'message/vnd.wfa.wsc', + wsdl: 'application/wsdl+xml', + wspolicy: 'application/wspolicy+xml', + wtb: 'application/vnd.webturbo', + wvx: 'video/x-ms-wvx', + x32: 'application/x-authorware-bin', + x3d: 'model/x3d+xml', + x3db: 'model/x3d+fastinfoset', + x3dbz: 'model/x3d+binary', + x3dv: 'model/x3d-vrml', + x3dvz: 'model/x3d+vrml', + x3dz: 'model/x3d+xml', + x_b: 'model/vnd.parasolid.transmit.binary', + x_t: 'model/vnd.parasolid.transmit.text', + xaml: 'application/xaml+xml', + xap: 'application/x-silverlight-app', + xar: 'application/vnd.xara', + xav: 'application/xcap-att+xml', + xbap: 'application/x-ms-xbap', + xbd: 'application/vnd.fujixerox.docuworks.binder', + xbm: 'image/x-xbitmap', + xca: 'application/xcap-caps+xml', + xcs: 'application/calendar+xml', + xdf: 'application/xcap-diff+xml', + xdm: 'application/vnd.syncml.dm+xml', + xdp: 'application/vnd.adobe.xdp+xml', + xdssc: 'application/dssc+xml', + xdw: 'application/vnd.fujixerox.docuworks', + xel: 'application/xcap-el+xml', + xenc: 'application/xenc+xml', + xer: 'application/patch-ops-error+xml', + xfdf: 'application/xfdf', + xfdl: 'application/vnd.xfdl', + xht: 'application/xhtml+xml', + xhtm: 'application/vnd.pwg-xhtml-print+xml', + xhtml: 'application/xhtml+xml', + xhvml: 'application/xv+xml', + xif: 'image/vnd.xiff', + xla: 'application/vnd.ms-excel', + xlam: 'application/vnd.ms-excel.addin.macroenabled.12', + xlc: 'application/vnd.ms-excel', + xlf: 'application/xliff+xml', + xlm: 'application/vnd.ms-excel', + xls: 'application/vnd.ms-excel', + xlsb: 'application/vnd.ms-excel.sheet.binary.macroenabled.12', + xlsm: 'application/vnd.ms-excel.sheet.macroenabled.12', + xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + xlt: 'application/vnd.ms-excel', + xltm: 'application/vnd.ms-excel.template.macroenabled.12', + xltx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.template', + xlw: 'application/vnd.ms-excel', + xm: 'audio/xm', + xml: 'text/xml', + xns: 'application/xcap-ns+xml', + xo: 'application/vnd.olpc-sugar', + xop: 'application/xop+xml', + xpi: 'application/x-xpinstall', + xpl: 'application/xproc+xml', + xpm: 'image/x-xpixmap', + xpr: 'application/vnd.is-xpr', + xps: 'application/vnd.ms-xpsdocument', + xpw: 'application/vnd.intercon.formnet', + xpx: 'application/vnd.intercon.formnet', + xsd: 'application/xml', + xsf: 'application/prs.xsf+xml', + xsl: 'application/xslt+xml', + xslt: 'application/xslt+xml', + xsm: 'application/vnd.syncml+xml', + xspf: 'application/xspf+xml', + xul: 'application/vnd.mozilla.xul+xml', + xvm: 'application/xv+xml', + xvml: 'application/xv+xml', + xwd: 'image/x-xwindowdump', + xyz: 'chemical/x-xyz', + xz: 'application/x-xz', + yaml: 'text/yaml', + yang: 'application/yang', + yin: 'application/yin+xml', + yml: 'text/yaml', + ymp: 'text/x-suse-ymp', + z1: 'application/x-zmachine', + z2: 'application/x-zmachine', + z3: 'application/x-zmachine', + z4: 'application/x-zmachine', + z5: 'application/x-zmachine', + z6: 'application/x-zmachine', + z7: 'application/x-zmachine', + z8: 'application/x-zmachine', + zaz: 'application/vnd.zzazz.deck+xml', + zip: 'application/zip', + zir: 'application/vnd.zul', + zirz: 'application/vnd.zul', + zmm: 'application/vnd.handheld-entertainment+xml', +} as const; diff --git a/packages/core/lib/utils/helpers.ts b/packages/core/lib/utils/helpers.ts index 8758167..dc573aa 100644 --- a/packages/core/lib/utils/helpers.ts +++ b/packages/core/lib/utils/helpers.ts @@ -7,6 +7,9 @@ import { Arr } from './array'; import { InternalLogger } from './logger'; import { Obj } from './object'; import { Str } from './string'; +import { readFileSync } from 'fs-extra'; +import { join } from 'path'; +import { findProjectRoot } from './path'; export const isEmpty = (value: any) => { if (Str.isString(value)) { @@ -134,3 +137,24 @@ export const getTime = () => { return `${formattedHours}:${formattedMinutes}:${formattedSeconds} ${ampm}`; }; + +export const isUndefined = (val: any) => { + return val === undefined; +}; + +export const isClass = (obj: any) => { + // First check if it's an object and not null + if (obj === null || typeof obj !== 'object') { + return false; + } + + // Check if the constructor is not the Object constructor + // This tells us if it was created by a class or Object literal + return Object.getPrototypeOf(obj).constructor.name !== 'Object'; +}; + +export const getPackageJson = () => { + return JSON.parse( + readFileSync(join(findProjectRoot(), 'package.json')).toString(), + ); +}; diff --git a/packages/core/lib/utils/index.ts b/packages/core/lib/utils/index.ts index 8539c59..68fd8d1 100644 --- a/packages/core/lib/utils/index.ts +++ b/packages/core/lib/utils/index.ts @@ -1,4 +1,3 @@ -export * from './context'; export * from './expParser'; export * from './packageLoader'; export * from './object'; diff --git a/packages/core/lib/utils/object.ts b/packages/core/lib/utils/object.ts index 0fc25f0..ace582d 100644 --- a/packages/core/lib/utils/object.ts +++ b/packages/core/lib/utils/object.ts @@ -1,4 +1,4 @@ -import { InvalidValue } from '../exceptions'; +import { InvalidValue } from '../exceptions/invalid-value'; import { Arr } from './array'; export class Obj { @@ -164,14 +164,15 @@ export class Obj { } static isObj(obj: any, throwError = false): boolean { - if (typeof obj === 'object' && !Array.isArray(obj) && obj !== null) { - return true; - } + let isObj = + obj !== null && + typeof obj === 'object' && + Object.getPrototypeOf(obj) === Object.prototype; - if (throwError) { + if (!isObj && throwError) { throw new InvalidValue('Passed value is not an object'); } - return false; + return isObj; } } diff --git a/packages/core/lib/utils/string.ts b/packages/core/lib/utils/string.ts index 51589b6..4144b90 100644 --- a/packages/core/lib/utils/string.ts +++ b/packages/core/lib/utils/string.ts @@ -416,7 +416,7 @@ export class Str { if (str === pattern) return true; let regex = '^'; for (const p of pattern) { - regex += p === '*' ? '.*' : p; + regex += p === '*' ? '.*' : p === '[' || p === ']' ? `\\${p}` : p; } const regexp = new RegExp(regex + '$', 'g'); return regexp.test(str); diff --git a/packages/core/lib/validator/index.ts b/packages/core/lib/validator/index.ts index f564f12..abf3e82 100644 --- a/packages/core/lib/validator/index.ts +++ b/packages/core/lib/validator/index.ts @@ -1,12 +1,7 @@ -import { - ExecutionContext, - SetMetadata, - UseGuards, - applyDecorators, - createParamDecorator, -} from '@nestjs/common'; -import { Request } from '../rest/foundation'; -import { IntentValidationGuard } from './validationGuard'; +import { applyDecorators } from '../reflections/apply-decorators'; +import { SetMetadata } from '../reflections/set-metadata'; +import { UseGuards } from '../rest'; +import { IntentValidationGuard } from './validation-guard'; export * from './validator'; export * from './decorators'; @@ -17,14 +12,3 @@ export function Validate(DTO: any) { UseGuards(IntentValidationGuard), ); } - -export const Dto = createParamDecorator( - (data: string, ctx: ExecutionContext) => { - const contextType = ctx['contextType']; - if (contextType === 'ws') { - return ctx.switchToWs().getClient()._dto; - } - const request = ctx.switchToHttp().getRequest() as Request; - return request.dto(); - }, -); diff --git a/packages/core/lib/validator/validation-guard.ts b/packages/core/lib/validator/validation-guard.ts new file mode 100644 index 0000000..bf1a805 --- /dev/null +++ b/packages/core/lib/validator/validation-guard.ts @@ -0,0 +1,23 @@ +import { Request } from '@intentjs/hyper-express'; +import { Injectable } from '../foundation/decorators'; +import { IntentGuard } from '../rest/foundation/guards/base-guard'; +import { ExecutionContext } from '../rest/http-server/contexts/execution-context'; + +@Injectable() +export class IntentValidationGuard extends IntentGuard { + constructor() { + super(); + } + + async guard(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest() as Request; + const reflector = context.getReflector(); + /** + * Check if a DTO already exists + */ + const dto = request.dto(); + const schema = reflector.getFromMethod('dtoSchema'); + await request.validate(schema); + return true; + } +} diff --git a/packages/core/lib/validator/validationGuard.ts b/packages/core/lib/validator/validationGuard.ts deleted file mode 100644 index bc663b1..0000000 --- a/packages/core/lib/validator/validationGuard.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; -import { Reflector } from '@nestjs/core'; -import { Request } from '../rest'; - -@Injectable() -export class IntentValidationGuard implements CanActivate { - constructor(private reflector: Reflector) {} - - async canActivate(context: ExecutionContext): Promise { - const request = context.switchToHttp().getRequest() as Request; - const schema = this.reflector.get('dtoSchema', context.getHandler()); - await request.validate(schema); - return true; - } -} diff --git a/packages/core/lib/validator/validator.ts b/packages/core/lib/validator/validator.ts index c0c56db..fb494be 100644 --- a/packages/core/lib/validator/validator.ts +++ b/packages/core/lib/validator/validator.ts @@ -1,9 +1,9 @@ -import { Type } from '@nestjs/common'; import { plainToInstance } from 'class-transformer'; import { ValidationError, validate } from 'class-validator'; import { ConfigService } from '../config/service'; -import { ValidationFailed } from '../exceptions/validationfailed'; +import { ValidationFailed } from '../exceptions/validation-failed'; import { Obj } from '../utils'; +import { Type } from '../interfaces/utils'; export class Validator { private meta: Record; @@ -25,16 +25,22 @@ export class Validator { return this; } - async validate(inputs: Record): Promise { + async validateRaw(inputs: Record): Promise { const schema: T = plainToInstance(this.dto, inputs); - if (Obj.isNotEmpty(this.meta)) { - this.injectMeta(schema); + const errors = await validate(schema as Record, { + stopAtFirstError: true, + }); + + if (errors.length > 0) { + await this.processErrorsFromValidation(errors); } - schema['$'] = this.meta; + return schema; + } - const errors = await validate(schema as Record, { + async validateDto(dto: T): Promise { + const errors = await validate(dto as Record, { stopAtFirstError: true, }); @@ -42,7 +48,7 @@ export class Validator { await this.processErrorsFromValidation(errors); } - return schema; + return dto; } /** diff --git a/packages/core/package.json b/packages/core/package.json index b9d41f8..b4caf2a 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@intentjs/core", - "version": "0.1.35", + "version": "0.1.35-next-5", "description": "Core module for Intent", "repository": { "type": "git", @@ -43,7 +43,6 @@ "@nestjs/testing": "^10.3.9", "@stylistic/eslint-plugin-ts": "^2.6.1", "@types/archy": "^0.0.36", - "@types/express": "^4.17.21", "@types/fs-extra": "^11.0.1", "@types/inquirer": "^9.0.7", "@types/jest": "^29.5.13", @@ -65,9 +64,9 @@ "typescript": "^5.5.2" }, "dependencies": { - "@nestjs/common": "^10.4.4", - "@nestjs/core": "^10.4.4", - "@nestjs/platform-express": "^10.4.1", + "@intentjs/hyper-express": "^0.0.4", + "@nestjs/common": "^10.4.8", + "@nestjs/core": "^10.4.8", "@react-email/components": "^0.0.25", "archy": "^1.0.0", "axios": "^1.7.7", @@ -78,13 +77,12 @@ "dotenv": "^16.4.5", "enquirer": "^2.4.1", "eta": "^3.5.0", - "express": "^4.21.0", "fs-extra": "^11.1.1", "helmet": "^7.1.0", "ioredis": "^5.3.2", "knex": "^3.1.0", + "live-directory": "^3.0.3", "ms": "^2.1.3", - "mute-stdout": "^2.0.0", "objection": "^3.1.4", "picocolors": "^1.1.0", "react-email": "^3.0.1", diff --git a/packages/hyper-express/.gitignore b/packages/hyper-express/.gitignore new file mode 100644 index 0000000..5ef0e8d --- /dev/null +++ b/packages/hyper-express/.gitignore @@ -0,0 +1,4 @@ +.npmrc +.vscode/ +node_modules/ +tests/content/large-files/ \ No newline at end of file diff --git a/packages/hyper-express/.gitmodules b/packages/hyper-express/.gitmodules new file mode 100644 index 0000000..a10cfa0 --- /dev/null +++ b/packages/hyper-express/.gitmodules @@ -0,0 +1,9 @@ +[submodule "middlewares/hyper-express-session"] + path = middlewares/hyper-express-session + url = git@github.com:kartikk221/hyper-express-session.git +[submodule "middlewares/hyper-express-body-parser"] + path = middlewares/hyper-express-body-parser + url = git@github.com:kartikk221/hyper-express-body-parser.git +[submodule "middlewares/hyper-express-serve-static"] + path = middlewares/hyper-express-serve-static + url = git@github.com:kartikk221/hyper-express-serve-static.git diff --git a/packages/hyper-express/.npmignore b/packages/hyper-express/.npmignore new file mode 100644 index 0000000..722b67d --- /dev/null +++ b/packages/hyper-express/.npmignore @@ -0,0 +1,5 @@ +tests/ +benchmarks/ +middlewares/ +docs/ +.npmrc \ No newline at end of file diff --git a/packages/hyper-express/.prettierrc b/packages/hyper-express/.prettierrc new file mode 100644 index 0000000..b4618d4 --- /dev/null +++ b/packages/hyper-express/.prettierrc @@ -0,0 +1,7 @@ +{ + "semi": true, + "tabWidth": 4, + "printWidth": 120, + "singleQuote": true, + "bracketSpacing": true +} diff --git a/packages/hyper-express/LICENSE b/packages/hyper-express/LICENSE new file mode 100644 index 0000000..421a191 --- /dev/null +++ b/packages/hyper-express/LICENSE @@ -0,0 +1,22 @@ +(The MIT License) + +Copyright (c) 2021-2021 Kartik Kumar <@kartikk221> + +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. \ No newline at end of file diff --git a/packages/hyper-express/README.md b/packages/hyper-express/README.md new file mode 100644 index 0000000..3263803 --- /dev/null +++ b/packages/hyper-express/README.md @@ -0,0 +1,60 @@ +# HyperExpress: High Performance Node.js Webserver +#### Powered by [`uWebSockets.js`](https://github.com/uNetworking/uWebSockets.js/) + +
+ +[![NPM version](https://img.shields.io/npm/v/hyper-express.svg?style=flat)](https://www.npmjs.com/package/hyper-express) +[![NPM downloads](https://img.shields.io/npm/dm/hyper-express.svg?style=flat)](https://www.npmjs.com/package/hyper-express) +[![GitHub issues](https://img.shields.io/github/issues/kartikk221/hyper-express)](https://github.com/kartikk221/hyper-express/issues) +[![GitHub stars](https://img.shields.io/github/stars/kartikk221/hyper-express)](https://github.com/kartikk221/hyper-express/stargazers) +[![GitHub license](https://img.shields.io/github/license/kartikk221/hyper-express)](https://github.com/kartikk221/hyper-express/blob/master/LICENSE) + +
+ +## Motivation +HyperExpress aims to be a simple yet performant HTTP & Websocket Server. Combined with the power of uWebsockets.js, a Node.js binding of uSockets written in C++, HyperExpress allows developers to unlock higher throughput for their web applications with their existing hardware. This can allow many web applications to become much more performant on optimized data serving endpoints without having to scale hardware. + +Some of the prominent highlights are: +- Simplified HTTP & Websocket API +- Server-Sent Events Support +- Multipart File Uploading Support +- Modular Routers & Middlewares Support +- Multiple Host/Domain Support Over SSL +- Limited Express.js API Compatibility Through Shared Methods/Properties + +See [`> [Benchmarks]`](https://web-frameworks-benchmark.netlify.app/result?l=javascript) for **performance metrics** against other webservers in real world deployments. + +## Documentation +HyperExpress **supports** the latest three LTS (Long-term Support) Node.js versions only and can be installed using Node Package Manager (`npm`). +``` +npm i hyper-express +``` + +- See [`> [Examples & Snippets]`](./docs/Examples.md) for small and **easy-to-use snippets** with HyperExpress. +- See [`> [Server]`](./docs/Server.md) for creating a webserver and working with the **Server** component. +- See [`> [Router]`](./docs/Router.md) for working with the modular **Router** component. +- See [`> [Request]`](./docs/Request.md) for working with the **Request** component made available through handlers. +- See [`> [Response]`](./docs/Response.md) for working with the **Response** component made available through handlers. +- See [`> [Websocket]`](./docs/Websocket.md) for working with **Websockets** in HyperExpress. +- See [`> [Middlewares]`](./docs/Middlewares.md) for working with global and route-specific **Middlewares** in HyperExpress. +- See [`> [SSEventStream]`](./docs/SSEventStream.md) for working with **Server-Sent Events** based streaming in HyperExpress. +- See [`> [MultipartField]`](./docs/MultipartField.md) for working with multipart requests and **File Uploading** in HyperExpress. +- See [`> [SessionEngine]`](https://github.com/kartikk221/hyper-express-session) for working with cookie based web **Sessions** in HyperExpress. +- See [`> [LiveDirectory]`](./docs/LiveDirectory.md) for implementing **static file/asset** serving functionality into HyperExpress. +- See [`> [HostManager]`](./docs/HostManager.md) for supporting requests over **muliple hostnames** in HyperExpress. + +## Encountering Problems? +- HyperExpress is mostly compatible with `Express` but not **100%** therefore you may encounter some middlewares not working out of the box. In this scenario, you must either write your own polyfill or omit the middleware to continue. +- The uWebsockets.js version header is disabled by default. You may opt-out of this behavior by setting an environment variable called `KEEP_UWS_HEADER` to a truthy value such as `1` or `true`. +- Still having problems? Open an [`> [Issue]`](https://github.com/kartikk221/hyper-express/issues) with details about what led up to the problem including error traces, route information etc etc. + +## Testing Changes +To run HyperExpress functionality tests locally on your machine, you must follow the steps below. +1. Clone the HyperExpress repository to your machine. +2. Initialize and pull any submodule(s) which are used throughout the tests. +3. Run `npm install` in the root directory. +4. Run `npm install` in the `/tests` directory. +5. Run `npm test` to run all tests with your local changes. + +## License +[MIT](./LICENSE) diff --git a/packages/hyper-express/benchmarks/benchmark.sh b/packages/hyper-express/benchmarks/benchmark.sh new file mode 100644 index 0000000..c91545e --- /dev/null +++ b/packages/hyper-express/benchmarks/benchmark.sh @@ -0,0 +1,26 @@ +# Define Execution Variables +HOST="localhost"; +PORT_START=3000; +PORT_END=3004; +NUM_OF_CONNECTIONS=2500; +DURATION_SECONDS=30; +PIPELINE_FACTOR=4; + +# Ensure "autocannon" is not installed, install it with NPM +if ! [ -x "$(command -v autocannon)" ]; then + echo 'Error: autocannon is not installed. Attempting to install with NPM.'; + npm install autocannon -g; +fi + +# Iterate a for loop from PORT_START to PORT_END +for ((PORT=$PORT_START; PORT<=$PORT_END; PORT++)) +do + # Execute the benchmark + echo "Benchmarking Webserver @ Port: $HOST:$PORT"; + + # Use the autocannon utility to benchmark + autocannon -c $NUM_OF_CONNECTIONS -d $DURATION_SECONDS -p $PIPELINE_FACTOR http://localhost:$PORT/; + + # Append a visual line to separate results + echo "----------------------------------------------------"; +done \ No newline at end of file diff --git a/packages/hyper-express/benchmarks/configuration.json b/packages/hyper-express/benchmarks/configuration.json new file mode 100644 index 0000000..0b17892 --- /dev/null +++ b/packages/hyper-express/benchmarks/configuration.json @@ -0,0 +1,5 @@ +{ + "hostname": "localhost", + "port_start": 3000, + "multi_core": false +} diff --git a/packages/hyper-express/benchmarks/index.js b/packages/hyper-express/benchmarks/index.js new file mode 100644 index 0000000..e884639 --- /dev/null +++ b/packages/hyper-express/benchmarks/index.js @@ -0,0 +1,102 @@ +import os from 'os'; +import fs from 'fs'; +import cluster from 'cluster'; +import fetch from 'node-fetch'; +import uWebsocketsJS from 'uWebSockets.js'; +import { log } from './utils.js'; + +// Load the server instances to be benchmarked +import uWebsockets from './setup/uwebsockets.js'; +import NanoExpress from './setup/nanoexpress.js'; +import HyperExpress from './setup/hyperexpress.js'; +import Fastify from './setup/fastify.js'; +import Express from './setup/express.js'; + +// Load the configuration from disk +const configuration = JSON.parse(fs.readFileSync('./configuration.json', 'utf8')); + +// Handle spawning of worker processes from the master process +const numCPUs = configuration.multi_core ? os.cpus().length : 1; +if (numCPUs > 1 && (cluster.isMaster || cluster.isPrimary)) { + for (let i = 0; i < numCPUs; i++) { + cluster.fork(); + } + log(`Forked ${numCPUs} workers for benchmarking on ${os.platform()}`); +} + +// Handle spawning of webservers for each worker process +let uws_socket; +if (numCPUs <= 1 || cluster.isWorker) { + // Perform startup tasks + log('Initializing Webservers...'); + (async () => { + try { + // Remember the initial port for HTTP request checks after all servers are started + const initial_port = configuration.port_start; + + // Initialize the uWebsockets server instance + uws_socket = await new Promise((resolve) => + uWebsockets.listen(configuration.hostname, configuration.port_start, resolve) + ); + log(`uWebsockets.js server listening on port ${configuration.port_start}`); + + // Initialize the NanoExpress server instance + configuration.port_start++; + await HyperExpress.listen(configuration.port_start, configuration.hostname); + log(`HyperExpress server listening on port ${configuration.port_start}`); + + // Initialize the NanoExpress server instance + configuration.port_start++; + await NanoExpress.listen(configuration.port_start); + log(`NanoExpress server listening on port ${configuration.port_start}`); + + // Initialize the Fastify server instance + configuration.port_start++; + Fastify.listen({ port: configuration.port_start, host: configuration.hostname }); + log(`Fastify server listening on port ${configuration.port_start}`); + + // Initialize the Express server instance + configuration.port_start++; + await new Promise((resolve) => Express.listen(configuration.port_start, configuration.hostname, resolve)); + log(`Express.js server listening on port ${configuration.port_start}`); + + // Make HTTP GET requests to all used ports to test the servers + log('Testing each webserver with a HTTP GET request...'); + for (let i = initial_port; i <= configuration.port_start; i++) { + const response = await fetch(`http://localhost:${i}/`); + if (response.status !== 200) + throw new Error(`HTTP request to port ${i} failed with status ${response.status}`); + log(`GET HTTP -> Port ${i} -> Status ${response.status} -> ${response.headers.get('content-type')}`); + } + + log( + 'All webservers are ready to receive request between ports ' + + initial_port + + ' - ' + + configuration.port_start + + '!', + false + ); + } catch (error) { + console.log(error); + process.exit(); + } + })(); +} + +['exit', 'SIGINT', 'SIGUSR1', 'SIGUSR2', 'SIGTERM'].forEach((type) => + process.once(type, () => { + // Close all the webserver instances + try { + if (uws_socket) uWebsocketsJS.us_listen_socket_close(uws_socket); + NanoExpress.close(); + HyperExpress.close(); + Fastify.close(); + } catch (error) { + console.log(error); + } + + // Exit the process + process.exit(); + }) +); diff --git a/packages/hyper-express/benchmarks/package-lock.json b/packages/hyper-express/benchmarks/package-lock.json new file mode 100644 index 0000000..d0391a8 --- /dev/null +++ b/packages/hyper-express/benchmarks/package-lock.json @@ -0,0 +1,1644 @@ +{ + "name": "benchmarks", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "benchmarks", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "express": "^5.0.1", + "fastify": "^5.0.0", + "nanoexpress": "^6.4.4", + "node-fetch": "^3.3.2", + "uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.49.0" + } + }, + "node_modules/@dalisoft/args": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@dalisoft/args/-/args-0.1.1.tgz", + "integrity": "sha512-PwmLEhnTyK6+AUwD0oqS57nLs1vpB17vEDlMc1i27zH8mrtFJmBmYfQv4FszoUHDHv3PtFk8M0X9yWxry2TSwA==", + "license": "MIT" + }, + "node_modules/@dalisoft/events": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@dalisoft/events/-/events-0.2.0.tgz", + "integrity": "sha512-2NS/0vS9eL8ZxhgCVZQgPS4uoqbcSk9FN3G8Eqakcej7wOpH72z1kmDEDGI5T+MOp9IueKT2qI0XUdXhKeK9GA==", + "license": "MIT", + "dependencies": { + "@dalisoft/args": "^0.1.1" + } + }, + "node_modules/@fastify/ajv-compiler": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-4.0.1.tgz", + "integrity": "sha512-DxrBdgsjNLP0YM6W5Hd6/Fmj43S8zMKiFJYgi+Ri3htTGAowPVG/tG1wpnWLMjufEnehRivUCKZ1pLDIoZdTuw==", + "license": "MIT", + "dependencies": { + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^3.0.0" + } + }, + "node_modules/@fastify/error": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.0.0.tgz", + "integrity": "sha512-OO/SA8As24JtT1usTUTKgGH7uLvhfwZPwlptRi2Dp5P4KKmJI3gvsZ8MIHnNwDs4sLf/aai5LzTyl66xr7qMxA==", + "license": "MIT" + }, + "node_modules/@fastify/fast-json-stringify-compiler": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-5.0.1.tgz", + "integrity": "sha512-f2d3JExJgFE3UbdFcpPwqNUEoHWmt8pAKf8f+9YuLESdefA0WgqxeT6DrGL4Yrf/9ihXNSKOqpjEmurV405meA==", + "license": "MIT", + "dependencies": { + "fast-json-stringify": "^6.0.0" + } + }, + "node_modules/@fastify/merge-json-schemas": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.1.1.tgz", + "integrity": "sha512-fERDVz7topgNjtXsJTTW1JKLy0rhuLRcquYqNR9rF7OcVpCa2OVW49ZPDIhaRRCaUuvVxI+N416xUoF76HNSXA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/abstract-logging": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", + "license": "MIT" + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/array-flatten": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-3.0.0.tgz", + "integrity": "sha512-zPMVc3ZYlGLNk4mpK1NzP2wg0ml9t7fUgDsayR5Y5rSzxQilzR9FGu/EH2jQOcKSAeAfWeylyW8juy3OkWRvNA==", + "license": "MIT" + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/avvio": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/avvio/-/avvio-9.0.0.tgz", + "integrity": "sha512-UbYrOXgE/I+knFG+3kJr9AgC7uNo8DG+FGGODpH9Bj1O1kL/QDjBXnTem9leD3VdQKtaHjV3O85DQ7hHh4IIHw==", + "license": "MIT", + "dependencies": { + "@fastify/error": "^4.0.0", + "fastq": "^1.17.1" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.0.1.tgz", + "integrity": "sha512-PagxbjvuPH6tv0f/kdVbFGcb79D236SLcDTs6DrQ7GizJ88S1UWP4nMXFEo/I4fdhGRGabvFfFjVGm3M7U8JwA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "3.1.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.5.2", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "^3.0.0", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/body-parser/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/body-parser/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/body-parser/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.1.tgz", + "integrity": "sha512-78KWk9T26NhzXtuL26cIJ8/qNHANyJ/ZYrmEXFzUmhZdjpBv+DlWlOANRTGBt48YcyslsLrj0bMLFTmXvLRCOw==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/express": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.0.1.tgz", + "integrity": "sha512-ORF7g6qGnD+YtUG9yx4DFoqCShNMmUKiXuT5oWMHiOvt/4WFbHC6yCwQMTSBMno7AqntNCAzzcnnjowRkTL9eQ==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.0.1", + "content-disposition": "^1.0.0", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "^1.2.1", + "debug": "4.3.6", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "^2.0.0", + "fresh": "2.0.0", + "http-errors": "2.0.0", + "merge-descriptors": "^2.0.0", + "methods": "~1.1.2", + "mime-types": "^3.0.0", + "on-finished": "2.4.1", + "once": "1.4.0", + "parseurl": "~1.3.3", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "router": "^2.0.0", + "safe-buffer": "5.2.1", + "send": "^1.1.0", + "serve-static": "^2.1.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "^2.0.0", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/express/node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fast-decode-uri-component": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", + "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stringify": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-6.0.0.tgz", + "integrity": "sha512-FGMKZwniMTgZh7zQp9b6XnBVxUmKVahQLQeRQHqwYmPDqDhcEKZ3BaQsxelFFI5PY7nN71OEeiL47/zUWcYe1A==", + "license": "MIT", + "dependencies": { + "@fastify/merge-json-schemas": "^0.1.1", + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-deep-equal": "^3.1.3", + "fast-uri": "^2.3.0", + "json-schema-ref-resolver": "^1.0.1", + "rfdc": "^1.2.0" + } + }, + "node_modules/fast-json-stringify/node_modules/fast-uri": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-2.4.0.tgz", + "integrity": "sha512-ypuAmmMKInk5q7XcepxlnUWDLWv4GFtaJqAzWKqn62IpQ3pejtr5dTVbt3vwqVaMKmkNR55sTT+CqUKIaT21BA==", + "license": "MIT" + }, + "node_modules/fast-query-parse": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-query-parse/-/fast-query-parse-3.1.0.tgz", + "integrity": "sha512-ThdC8A4Wi8naYwS1vUERRC3aNhAP5s9U0suMS7afVfQAYFjV/xVToQdzn0bCk4uQgn0no42X46sBI7DAnnCswQ==", + "license": "MIT", + "dependencies": { + "fast-decode-uri-component": "^1.0.1" + } + }, + "node_modules/fast-querystring": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", + "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", + "license": "MIT", + "dependencies": { + "fast-decode-uri-component": "^1.0.1" + } + }, + "node_modules/fast-redact": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz", + "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-uri": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.2.tgz", + "integrity": "sha512-GR6f0hD7XXyNJa25Tb9BuIdN0tdr+0BMi6/CJPH3wJO1JjNG3n/VsSw38AwRdKZABm8lGbPfakLRkYzx2V9row==", + "license": "MIT" + }, + "node_modules/fastify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.0.0.tgz", + "integrity": "sha512-Qe4dU+zGOzg7vXjw4EvcuyIbNnMwTmcuOhlOrOJsgwzvjEZmsM/IeHulgJk+r46STjdJS/ZJbxO8N70ODXDMEQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/ajv-compiler": "^4.0.0", + "@fastify/error": "^4.0.0", + "@fastify/fast-json-stringify-compiler": "^5.0.0", + "abstract-logging": "^2.0.1", + "avvio": "^9.0.0", + "fast-json-stringify": "^6.0.0", + "find-my-way": "^9.0.0", + "light-my-request": "^6.0.0", + "pino": "^9.0.0", + "process-warning": "^4.0.0", + "proxy-addr": "^2.0.7", + "rfdc": "^1.3.1", + "secure-json-parse": "^2.7.0", + "semver": "^7.6.0", + "toad-cache": "^3.7.0" + } + }, + "node_modules/fastify/node_modules/process-warning": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.0.tgz", + "integrity": "sha512-/MyYDxttz7DfGMMHiysAsFE4qF+pQYAA8ziO/3NcRVrQ5fSk+Mns4QZA/oRPFzvcqNoVJXQNWNAsdwBXLUkQKw==", + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/finalhandler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.0.0.tgz", + "integrity": "sha512-MX6Zo2adDViYh+GcxxB1dpO43eypOGUOL12rLCOTMQv/DfIbpSJUy4oQIIZhVZkH9e+bZWKMon0XHFEju16tkQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/find-my-way": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.1.0.tgz", + "integrity": "sha512-Y5jIsuYR4BwWDYYQ2A/RWWE6gD8a0FMgtU+HOq1WKku+Cwdz8M1v8wcAmRXXM1/iqtoqg06v+LjAxMYbCjViMw==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-querystring": "^1.0.0", + "safe-regex2": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.5.2.tgz", + "integrity": "sha512-kERHXvpSaB4aU3eANwidg79K8FlrN77m8G9V+0vOR3HYaRifrlwMEpT7ZBJqLSEIHnEgJTHcWK82wwLwwKwtag==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/json-schema-ref-resolver": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-1.0.1.tgz", + "integrity": "sha512-EJAj1pgHc1hxF6vo2Z3s69fMjO1INq6eGHXZ8Z6wCQeldCuwxGK9Sxf4/cScGn3FZubCVUehfWtcDM/PLteCQw==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/light-my-request": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.1.0.tgz", + "integrity": "sha512-+NFuhlOGoEwxeQfJ/pobkVFxcnKyDtiX847hLjuB/IzBxIl3q4VJeFI8uRCgb3AlTWL1lgOr+u5+8QdUcr33ng==", + "license": "BSD-3-Clause", + "dependencies": { + "cookie": "^0.7.0", + "process-warning": "^4.0.0", + "set-cookie-parser": "^2.6.0" + } + }, + "node_modules/light-my-request/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/light-my-request/node_modules/process-warning": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.0.tgz", + "integrity": "sha512-/MyYDxttz7DfGMMHiysAsFE4qF+pQYAA8ziO/3NcRVrQ5fSk+Mns4QZA/oRPFzvcqNoVJXQNWNAsdwBXLUkQKw==", + "license": "MIT" + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-db": { + "version": "1.53.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.53.0.tgz", + "integrity": "sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.0.tgz", + "integrity": "sha512-XqoSHeCGjVClAmoGFG3lVFqQFRIrTVw2OH3axRqAcfaw+gHWIfnASS92AV+Rl/mk0MupgZTRHQOjxY6YVnzK5w==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.53.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "license": "MIT" + }, + "node_modules/nanoexpress": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/nanoexpress/-/nanoexpress-6.4.4.tgz", + "integrity": "sha512-yyGHOl9koJf7C/a6Ojs6boKaZyrTneCG76vL+/SqDC1uRn9f/h6LL84PbVT22rX1SVQ5NB8v7mfoRrjbUPWpyQ==", + "license": "Apache-2.0", + "dependencies": { + "@dalisoft/events": "^0.2.0", + "ajv": "^8.17.1", + "cookie": "^0.6.0", + "fast-query-parse": "^3.0.0", + "uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.47.0" + }, + "engines": { + "node": ">=18.20.4" + }, + "funding": { + "type": "patreon", + "url": "https://www.patreon.com/dalisoft" + } + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/object-inspect": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/pino": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-9.2.0.tgz", + "integrity": "sha512-g3/hpwfujK5a4oVbaefoJxezLzsDgLcNJeITvC6yrfwYeT9la+edCK42j5QpEQSQCZgTKapXvnQIdgZwvRaZug==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.1.1", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^1.2.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^3.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.2.0.tgz", + "integrity": "sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==", + "license": "MIT", + "dependencies": { + "readable-stream": "^4.0.0", + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz", + "integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==", + "license": "MIT" + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-warning": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz", + "integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ret": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", + "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/router": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.0.0.tgz", + "integrity": "sha512-dIM5zVoG8xhC6rnSN8uoAgFARwTE7BQs8YwHEvK0VCmfxQXMaOuA1uiR1IPwsW7JyK5iTt7Od/TC9StasS2NPQ==", + "license": "MIT", + "dependencies": { + "array-flatten": "3.0.0", + "is-promise": "4.0.0", + "methods": "~1.1.2", + "parseurl": "~1.3.3", + "path-to-regexp": "^8.0.0", + "setprototypeof": "1.2.0", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-regex2": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-4.0.0.tgz", + "integrity": "sha512-Hvjfv25jPDVr3U+4LDzBuZPPOymELG3PYcSk5hcevooo1yxxamQL/bHs/GrEPGmMoMEwRrHVGiCA1pXi97B8Ew==", + "license": "MIT", + "dependencies": { + "ret": "~0.5.0" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", + "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/secure-json-parse": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", + "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==", + "license": "BSD-3-Clause" + }, + "node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.1.0.tgz", + "integrity": "sha512-v67WcEouB5GxbTWL/4NeToqcZiAWEq90N888fczVArY8A79J0L4FD7vj5hm3eUMua5EpoQ59wa/oovY6TLvRUA==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "destroy": "^1.2.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^0.5.2", + "http-errors": "^2.0.0", + "mime-types": "^2.1.35", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/send/node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/send/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/send/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.1.0.tgz", + "integrity": "sha512-A3We5UfEjG8Z7VkDv6uItWw6HY2bBSBJT1KtVESn6EOoOr2jAxNhxWCLY3jDE2WcuHXByWju74ck3ZgLwL8xmA==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.0.tgz", + "integrity": "sha512-lXLOiqpkUumhRdFF3k1osNXCy9akgx/dyPZ5p8qAg9seJzXr5ZrlqZuWIMuY6ejOsVLE6flJ5/h3lsn57fQ/PQ==", + "license": "MIT" + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sonic-boom": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.0.1.tgz", + "integrity": "sha512-hTSD/6JMLyT4r9zeof6UtuBDpjJ9sO08/nmS5djaA9eozT9oOlNdpXSnzcgj4FTqpk3nkLrs61l4gip9r1HCrQ==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/thread-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", + "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } + }, + "node_modules/toad-cache": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz", + "integrity": "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.0.tgz", + "integrity": "sha512-gd0sGezQYCbWSbkZr75mln4YBidWUN60+devscpLF5mtRDUpiaTvKpBNrdaCvel1NdR2k6vclXybU5fBd2i+nw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uWebSockets.js": { + "version": "20.44.0", + "resolved": "git+ssh://git@github.com/uNetworking/uWebSockets.js.git#8fa05571bf6ea95be8966ad313d9d39453e381ae", + "license": "Apache-2.0" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + } + } +} diff --git a/packages/hyper-express/benchmarks/package.json b/packages/hyper-express/benchmarks/package.json new file mode 100644 index 0000000..3e8f249 --- /dev/null +++ b/packages/hyper-express/benchmarks/package.json @@ -0,0 +1,20 @@ +{ + "name": "benchmarks", + "version": "1.0.0", + "description": "", + "main": "index.js", + "type": "module", + "scripts": { + "start": "node index.js", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "dependencies": { + "express": "^5.0.1", + "fastify": "^5.0.0", + "nanoexpress": "^6.4.4", + "node-fetch": "^3.3.2", + "uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.49.0" + } +} \ No newline at end of file diff --git a/packages/hyper-express/benchmarks/scenarios/simple_html_page.js b/packages/hyper-express/benchmarks/scenarios/simple_html_page.js new file mode 100644 index 0000000..5e20923 --- /dev/null +++ b/packages/hyper-express/benchmarks/scenarios/simple_html_page.js @@ -0,0 +1,23 @@ +export function get_simple_html_page({ server_name }) { + const date = new Date(); + return { + status: 200, + headers: { + 'unix-ms-ts': date.getTime().toString(), + 'cache-control': 'no-cache', + 'content-type': 'text/html; charset=utf-8', + 'server-name': server_name, + }, + body: ` + + + Welcome @ ${date.toLocaleDateString()} + + +

This is a simple HTML page.

+

This page was rendered at ${date.toLocaleString()} and delivered using '${server_name}' webserver.

+ + + `, + }; +} diff --git a/packages/hyper-express/benchmarks/setup/express.js b/packages/hyper-express/benchmarks/setup/express.js new file mode 100644 index 0000000..181f730 --- /dev/null +++ b/packages/hyper-express/benchmarks/setup/express.js @@ -0,0 +1,20 @@ +import Express from 'express'; +import { get_simple_html_page } from '../scenarios/simple_html_page.js'; + +// Initialize the Express app instance +const app = Express(); + +// Bind the 'simple_html_page' scenario route +app.get('/', (request, response) => { + // Generate the scenario payload + const { status, headers, body } = get_simple_html_page({ server_name: 'Express.js' }); + + // Write the status and headers + response.status(status); + Object.keys(headers).forEach((header) => response.header(header, headers[header])); + + // Write the body and end the response + return response.send(body); +}); + +export default app; diff --git a/packages/hyper-express/benchmarks/setup/fastify.js b/packages/hyper-express/benchmarks/setup/fastify.js new file mode 100644 index 0000000..78972bb --- /dev/null +++ b/packages/hyper-express/benchmarks/setup/fastify.js @@ -0,0 +1,20 @@ +import Fastify from 'fastify'; +import { get_simple_html_page } from '../scenarios/simple_html_page.js'; + +// Initialize the Express app instance +const app = Fastify(); + +// Bind the 'simple_html_page' scenario route +app.get('/', (request, response) => { + // Generate the scenario payload + const { status, headers, body } = get_simple_html_page({ server_name: 'Fastify' }); + + // Write the status and headers + response.status(status); + Object.keys(headers).forEach((header) => response.header(header, headers[header])); + + // Write the body and end the response + return response.send(body); +}); + +export default app; diff --git a/packages/hyper-express/benchmarks/setup/hyperexpress.js b/packages/hyper-express/benchmarks/setup/hyperexpress.js new file mode 100644 index 0000000..087936d --- /dev/null +++ b/packages/hyper-express/benchmarks/setup/hyperexpress.js @@ -0,0 +1,21 @@ +import HyperExpress from '../../index.js'; +import { get_simple_html_page } from '../scenarios/simple_html_page.js'; + +// Initialize the Express app instance +const app = new HyperExpress.Server(); + +// Generate the scenario payload +const { status, headers, body } = get_simple_html_page({ server_name: 'HyperExpress' }); + +// Bind the 'simple_html_page' scenario route +app.get('/', (request, response) => { + // Write the status and headers + response.status(status); + + for (const key in headers) response.header(key, headers[key]); + + // Write the body and end the response + response.send(body); +}); + +export default app; diff --git a/packages/hyper-express/benchmarks/setup/nanoexpress.js b/packages/hyper-express/benchmarks/setup/nanoexpress.js new file mode 100644 index 0000000..0884e73 --- /dev/null +++ b/packages/hyper-express/benchmarks/setup/nanoexpress.js @@ -0,0 +1,20 @@ +import NanoExpress from 'nanoexpress'; +import { get_simple_html_page } from '../scenarios/simple_html_page.js'; + +// Initialize the Express app instance +const app = NanoExpress(); + +// Bind the 'simple_html_page' scenario route +app.get('/', (request, response) => { + // Generate the scenario payload + const { status, headers, body } = get_simple_html_page({ server_name: 'NanoExpress' }); + + // Write the status and headers + response.status(status); + Object.keys(headers).forEach((header) => response.header(header, headers[header])); + + // Write the body and end the response + return response.send(body); +}); + +export default app; diff --git a/packages/hyper-express/benchmarks/setup/uwebsockets.js b/packages/hyper-express/benchmarks/setup/uwebsockets.js new file mode 100644 index 0000000..9bf77a2 --- /dev/null +++ b/packages/hyper-express/benchmarks/setup/uwebsockets.js @@ -0,0 +1,20 @@ +import uWebsockets from 'uWebSockets.js'; +import { get_simple_html_page } from '../scenarios/simple_html_page.js'; + +// Initialize an app instance which will be used to create the server +const app = uWebsockets.App(); + +// Bind the 'simple_html_page' scenario route +app.get('/', (response, request) => { + // Generate the scenario payload + const { status, headers, body } = get_simple_html_page({ server_name: 'uWebSockets.js' }); + + // Write the status and headers + response.writeStatus(`${status} OK`); + Object.keys(headers).forEach((header) => response.writeHeader(header, headers[header])); + + // Write the body and end the response + return response.end(body); +}); + +export default app; diff --git a/packages/hyper-express/benchmarks/utils.js b/packages/hyper-express/benchmarks/utils.js new file mode 100644 index 0000000..8695268 --- /dev/null +++ b/packages/hyper-express/benchmarks/utils.js @@ -0,0 +1,24 @@ +import cluster from 'cluster'; + +/** + * Logs a message to the console. + * Will only log if the current process is a worker and primary_only is set to false. + * + * @param {String} message + * @param {Boolean} [primary_only=true] + * @returns + */ +export function log(message, primary_only = true) { + if (primary_only && cluster.isWorker) return; + console.log(`[${process.pid}] ${message}`); +} + +/** + * Returns a Promise which is resolved after the given number of milliseconds. + * + * @param {Number} ms + * @returns {Promise} + */ +export function async_wait(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/packages/hyper-express/index.js b/packages/hyper-express/index.js new file mode 100644 index 0000000..b34020a --- /dev/null +++ b/packages/hyper-express/index.js @@ -0,0 +1,33 @@ +'use strict'; + +// Load uWebSockets.js and fundamental Server/Router classes +const uWebsockets = require('uWebSockets.js'); +const Server = require('./src/components/Server.js'); +const Router = require('./src/components/router/Router.js'); +const Request = require('./src/components/http/Request.js'); +const Response = require('./src/components/http/Response.js'); +const LiveFile = require('./src/components/plugins/LiveFile.js'); +const MultipartField = require('./src/components/plugins/MultipartField.js'); +const SSEventStream = require('./src/components/plugins/SSEventStream.js'); +const Websocket = require('./src/components/ws/Websocket.js'); + +// Disable the uWebsockets.js version header if not specified to be kept +if (!process.env['KEEP_UWS_HEADER']) { + try { + uWebsockets._cfg('999999990007'); + } catch (error) {} +} + +// Expose Server and Router classes along with uWebSockets.js constants +module.exports = { + Server, + Router, + Request, + Response, + LiveFile, + MultipartField, + SSEventStream, + Websocket, + compressors: uWebsockets, + express(...args) { return new Server(...args); }, +}; diff --git a/packages/hyper-express/package-lock.json b/packages/hyper-express/package-lock.json new file mode 100644 index 0000000..695b993 --- /dev/null +++ b/packages/hyper-express/package-lock.json @@ -0,0 +1,283 @@ +{ + "name": "@intentjs/hyper-express", + "version": "6.17.2-beta", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@intentjs/hyper-express", + "version": "6.17.2-beta", + "license": "MIT", + "dependencies": { + "busboy": "^1.6.0", + "cookie": "^1.0.1", + "cookie-signature": "^1.2.1", + "mime-types": "^2.1.35", + "negotiator": "^0.6.3", + "range-parser": "^1.2.1", + "type-is": "^1.6.18", + "typed-emitter": "^2.1.0", + "uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.49.0" + }, + "devDependencies": { + "@types/busboy": "^1.5.4", + "@types/express": "^5.0.0", + "@types/node": "^22.7.5", + "typescript": "^5.6.3" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/busboy": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/busboy/-/busboy-1.5.4.tgz", + "integrity": "sha512-kG7WrUuAKK0NoyxfQHsVE6j1m01s6kMma64E+OZenQABMQyTJop1DumUWcLwAQ2JzpefU7PDYoRDKl8uZosFjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz", + "integrity": "sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.0.tgz", + "integrity": "sha512-AbXMTZGt40T+KON9/Fdxx0B2WK5hsgxcfXJLr5bFpZ7b4JCex2WyQPTEKdXqfHiY5nKKBScZ7yCoO6Pvgxfvnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true + }, + "node_modules/@types/node": { + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", + "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/@types/qs": { + "version": "6.9.16", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.16.tgz", + "integrity": "sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.5.tgz", + "integrity": "sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==", + "dev": true, + "dependencies": { + "@types/http-errors": "*", + "@types/mime": "*", + "@types/node": "*" + } + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/cookie": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.1.tgz", + "integrity": "sha512-Xd8lFX4LM9QEEwxQpF9J9NTUh8pmdJO0cyRJhFiDoLTk2eH8FXlRv2IFGYVadZpqI3j8fhNrSdKCeYPxiAhLXw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.1.tgz", + "integrity": "sha512-78KWk9T26NhzXtuL26cIJ8/qNHANyJ/ZYrmEXFzUmhZdjpBv+DlWlOANRTGBt48YcyslsLrj0bMLFTmXvLRCOw==", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "optional": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "optional": true + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/typed-emitter/-/typed-emitter-2.1.0.tgz", + "integrity": "sha512-g/KzbYKbH5C2vPkaXGu8DJlHrGKHLsM25Zg9WuC9pMGfuvT+X25tZQWo5fK1BjBm8+UrVE9LDCvaY0CQk+fXDA==", + "optionalDependencies": { + "rxjs": "*" + } + }, + "node_modules/typescript": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/uWebSockets.js": { + "version": "20.49.0", + "resolved": "git+ssh://git@github.com/uNetworking/uWebSockets.js.git#442087c0a01bf146acb7386910739ec81df06700", + "license": "Apache-2.0" + } + } +} diff --git a/packages/hyper-express/package.json b/packages/hyper-express/package.json new file mode 100644 index 0000000..490e6e7 --- /dev/null +++ b/packages/hyper-express/package.json @@ -0,0 +1,62 @@ +{ + "name": "@intentjs/hyper-express", + "version": "0.0.4", + "description": "A fork of hyper-express to suit IntentJS requirements. High performance Node.js webserver with a simple-to-use API powered by uWebsockets.js under the hood.", + "main": "index.js", + "types": "./types/index.d.ts", + "scripts": { + "test": "node tests/index.js", + "publish:npm": "npm publish --access public", + "publish:next": "npm publish --access public --tag next" + }, + "repository": { + "type": "git", + "url": "https://github.com/intentjs/hyper-express.git" + }, + "keywords": [ + "uws", + "websockets", + "uwebsocketsjs", + "express", + "expressjs", + "fast", + "http-server", + "https-server", + "http", + "https", + "sse", + "events", + "streaming", + "stream", + "upload", + "file", + "multipart", + "ws", + "websocket", + "performance", + "router" + ], + "author": "Vinayak Sarawagi", + "license": "MIT", + "bugs": { + "url": "https://github.com/intentjs/hyper-express/issues" + }, + "homepage": "https://github.com/intentjs/hyper-express#readme", + "dependencies": { + "busboy": "^1.6.0", + "cookie": "^1.0.1", + "cookie-signature": "^1.2.1", + "mime-types": "^2.1.35", + "negotiator": "^0.6.3", + "range-parser": "^1.2.1", + "type-is": "^1.6.18", + "typed-emitter": "^2.1.0", + "uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.49.0" + }, + "devDependencies": { + "@types/busboy": "^1.5.4", + "@types/express": "^5.0.0", + "@types/node": "^22.7.5", + "typescript": "^5.6.3" + } +} diff --git a/packages/hyper-express/src/components/Server.js b/packages/hyper-express/src/components/Server.js new file mode 100644 index 0000000..da9b919 --- /dev/null +++ b/packages/hyper-express/src/components/Server.js @@ -0,0 +1,608 @@ +'use strict'; +const path = require('path'); +const fs = require('fs/promises'); +const uWebSockets = require('uWebSockets.js'); + +const Route = require('./router/Route.js'); +const Router = require('./router/Router.js'); +const Request = require('./http/Request.js'); +const Response = require('./http/Response.js'); +const HostManager = require('./plugins/HostManager.js'); +const WebsocketRoute = require('./ws/WebsocketRoute.js'); + +const { wrap_object, to_forward_slashes } = require('../shared/operators.js'); + +class Server extends Router { + #port; + #hosts; + #uws_instance; + #listen_socket; + #options = { + is_ssl: false, + auto_close: true, + fast_abort: false, + trust_proxy: false, + fast_buffers: false, + max_body_buffer: 16 * 1024, + max_body_length: 250 * 1024, + streaming: {}, + }; + + /** + * Server instance options. + * @returns {Object} + */ + _options = null; + + /** + * @param {Object} options Server Options + * @param {String=} options.cert_file_name Path to SSL certificate file to be used for SSL/TLS. + * @param {String=} options.key_file_name Path to SSL private key file to be used for SSL/TLS. + * @param {String=} options.passphrase Strong passphrase for SSL cryptographic purposes. + * @param {String=} options.dh_params_file_name Path to SSL Diffie-Hellman parameters file. + * @param {Boolean=} options.ssl_prefer_low_memory_usage Specifies uWebsockets to prefer lower memory usage while serving SSL. + * @param {Boolean=} options.fast_buffers Buffer.allocUnsafe is used when set to true for faster performance. + * @param {Boolean=} options.fast_abort Determines whether HyperExpress will abrubptly close bad requests. This can be much faster but the client does not receive an HTTP status code as it is a premature connection closure. + * @param {Boolean=} options.trust_proxy Specifies whether to trust incoming request data from intermediate proxy(s) + * @param {Number=} options.max_body_buffer Maximum body content to buffer in memory before a request data is handled. Behaves similar to `highWaterMark` in Node.js streams. + * @param {Number=} options.max_body_length Maximum body content length allowed in bytes. For Reference: 1kb = 1024 bytes and 1mb = 1024kb. + * @param {Boolean=} options.auto_close Whether to automatically close the server instance when the process exits. Default: true + * @param {Object} options.streaming Global content streaming options. + * @param {import('stream').ReadableOptions=} options.streaming.readable Global content streaming options for Readable streams. + * @param {import('stream').WritableOptions=} options.streaming.writable Global content streaming options for Writable streams. + */ + constructor(options = {}) { + // Only accept object as a parameter type for options + if (options == null || typeof options !== 'object') + throw new Error( + 'HyperExpress: HyperExpress.Server constructor only accepts an object type for the options parameter.' + ); + + // Initialize extended Router instance + super(); + super._is_app(true); + + // Store options locally for access throughout processing + wrap_object(this.#options, options); + + // Expose the options object for future use + this._options = this.#options; + try { + // Create underlying uWebsockets App or SSLApp to power HyperExpress + const { cert_file_name, key_file_name } = options; + this.#options.is_ssl = cert_file_name && key_file_name; // cert and key are required for SSL + if (this.#options.is_ssl) { + // Convert the certificate and key file names to absolute system paths + this.#options.cert_file_name = to_forward_slashes(path.resolve(cert_file_name)); + this.#options.key_file_name = to_forward_slashes(path.resolve(key_file_name)); + + // Create an SSL app with the provided SSL options + this.#uws_instance = uWebSockets.SSLApp(this.#options); + } else { + // Create a non-SSL app since no SSL options were provided + this.#uws_instance = uWebSockets.App(this.#options); + } + } catch (error) { + // Convert all the options to string values for logging purposes + const _options = Object.keys(options) + .map((key) => `options.${key}: "${options[key]}"`) + .join('\n'); + + // Throw error if uWebsockets.js fails to initialize + throw new Error( + `new HyperExpress.Server(): Failed to create new Server instance due to an invalid configuration in options.\n${_options}` + ); + } + + // Initialize the HostManager for this Server instance + this.#hosts = new HostManager(this); + } + + /** + * This object can be used to store properties/references local to this Server instance. + */ + locals = {}; + + /** + * @private + * This method binds a cleanup handler which automatically closes this Server instance. + */ + _bind_auto_close() { + const reference = this; + ['exit', 'SIGINT', 'SIGUSR1', 'SIGUSR2', 'SIGTERM'].forEach((type) => + process.once(type, () => reference.close()) + ); + } + + /** + * Starts HyperExpress webserver on specified port and host, or unix domain socket. + * + * @param {Number|String} first Required. Port or unix domain socket path to listen on. Example: 80 or "/run/listener.sock" + * @param {(String|function(import('uWebSockets.js').listen_socket):void)=} second Optional. Host or callback to be called when the server is listening. Default: "0.0.0.0" + * @param {(function(import('uWebSockets.js').us_listen_socket):void)=} third Optional. Callback to be called when the server is listening. + * @returns {Promise} Promise which resolves to the listen socket when the server is listening. + */ + async listen(first, second, third) { + let port; + let path; + + // Determine if first argument is a number or string castable to a port number + if (typeof first == 'number' || (+first > 0 && +first < 65536)) { + // Parse the port number + port = typeof first == 'string' ? +first : first; + } else if (typeof first == 'string') { + // Parse the path to a UNIX domain socket + path = first; + } + + let host = '0.0.0.0'; // Host by default is 0.0.0.0 + let callback; // Callback may be optionally provided as second or third argument + if (second) { + // If second argument is a function then it is the callback or else it is the host + if (typeof second === 'function') { + callback = second; + } else { + // Ensure the second argument is a string + if (typeof second == 'string') { + host = second; + } else { + throw new Error( + `HyperExpress.Server.listen(): The second argument must either be a callback function or a string as a hostname.` + ); + } + + // If we have a third argument and it is a function then it is the callback + if (third && typeof third === 'function') callback = third; + } + } + + // If the server is using SSL then verify that the provided SSL certificate and key files exist and are readable + if (this.#options.is_ssl) { + const { cert_file_name, key_file_name } = this.#options; + try { + // Verify that both the key and cert files exist and are readable + await Promise.all([fs.access(key_file_name), fs.access(cert_file_name)]); + } catch (error) { + throw new Error( + `HyperExpress.Server.listen(): The provided SSL certificate file at "${cert_file_name}" or private key file at "${key_file_name}" does not exist or is not readable.\n${error}` + ); + } + } + + // Bind the server to the specified port or unix domain socket with uWS.listen() or uWS.listen_unix() + const reference = this; + return await new Promise((resolve, reject) => { + // Define a callback to handle the listen socket from a listen event + const on_listen_socket = (listen_socket) => { + // Compile the Server instance to cache the routes and middlewares + reference._compile(); + + // Determine if we received a listen socket + if (listen_socket) { + // Store the listen socket for future closure + reference.#listen_socket = listen_socket; + + // Bind the auto close handler if enabled from constructor options + if (reference.#options.auto_close) reference._bind_auto_close(); + + // Serve the list socket over callback if provided + if (callback) callback(listen_socket); + + // Resolve the listen socket + resolve(listen_socket); + } else { + reject( + 'HyperExpress.Server.listen(): No Socket Received From uWebsockets.js likely due to an invalid host or busy port.' + ); + } + }; + + // Determine whether to bind on a port or unix domain socket with priority to port + if (port !== undefined) { + reference.#uws_instance.listen(host, port, on_listen_socket); + } else { + reference.#uws_instance.listen_unix(on_listen_socket, path); + } + }); + } + + #shutdown_promise; + /** + * Performs a graceful shutdown of the server and closes the listen socket once all pending requests have been completed. + * @param {uWebSockets.us_listen_socket=} listen_socket Optional + * @returns {Promise} + */ + shutdown(listen_socket) { + // If we already have a shutdown promise in flight, return it + if (this.#shutdown_promise) return this.#shutdown_promise; + + // If we have no pending requests, we can shutdown immediately + if (!this.#pending_requests_count) return Promise.resolve(this.close(listen_socket)); + + // Create a promise which resolves once all pending requests have been completed + const scope = this; + this.#shutdown_promise = new Promise((resolve) => { + // Bind a zero pending request handler to close the server + scope.#pending_requests_zero_handler = () => { + // Close the server and resolve the returned boolean + resolve(scope.close(listen_socket)); + }; + }); + + return this.#shutdown_promise; + } + + /** + * Stops/Closes HyperExpress webserver instance. + * + * @param {uWebSockets.us_listen_socket=} listen_socket Optional + * @returns {Boolean} + */ + close(listen_socket) { + // Fall back to self listen socket if none provided by user + const socket = listen_socket || this.#listen_socket; + if (socket) { + // Close the determined socket + uWebSockets.us_listen_socket_close(socket); + + // Nullify the local socket reference if it was used + if (!listen_socket) this.#listen_socket = null; + + return true; + } + return false; + } + + #routes_locked = false; + #handlers = { + on_not_found: (request, response) => response.status(404).send(), + on_error: (request, response, error) => { + // Log the error to the console + console.error(error); + + // Throw on default if user has not bound an error handler + return response.status(500).send('HyperExpress: Uncaught Exception Occured'); + }, + }; + + /** + * @typedef RouteErrorHandler + * @type {function(Request, Response, Error):void} + */ + + /** + * Sets a global error handler which will catch most uncaught errors across all routes/middlewares. + * + * @param {RouteErrorHandler} handler + */ + set_error_handler(handler) { + if (typeof handler !== 'function') throw new Error('HyperExpress: handler must be a function'); + this.#handlers.on_error = handler; + } + + /** + * @typedef RouteHandler + * @type {function(Request, Response):void} + */ + + /** + * Sets a global not found handler which will handle all requests that are unhandled by any registered route. + * + * @param {RouteHandler} handler + */ + set_not_found_handler(handler) { + if (typeof handler !== 'function') throw new Error('HyperExpress: handler must be a function'); + this.#handlers.on_not_found = handler; + } + + /** + * Publish a message to a topic in MQTT syntax to all WebSocket connections on this Server instance. + * You cannot publish using wildcards, only fully specified topics. + * + * @param {String} topic + * @param {String|Buffer|ArrayBuffer} message + * @param {Boolean=} is_binary + * @param {Boolean=} compress + * @returns {Boolean} + */ + publish(topic, message, is_binary, compress) { + return this.#uws_instance.publish(topic, message, is_binary, compress); + } + + /** + * Returns the number of subscribers to a topic across all WebSocket connections on this Server instance. + * + * @param {String} topic + * @returns {Number} + */ + num_of_subscribers(topic) { + return this.#uws_instance.numSubscribers(topic); + } + + /* Server Routes & Middlewares Logic */ + + #middlewares = { + '/': [], // This will contain global middlewares + }; + + #routes = { + any: {}, + get: {}, + post: {}, + del: {}, + head: {}, + options: {}, + patch: {}, + put: {}, + trace: {}, + upgrade: {}, + ws: {}, + }; + + #incremented_id = 0; + + /** + * Returns an incremented ID unique to this Server instance. + * + * @private + * @returns {Number} + */ + _get_incremented_id() { + return this.#incremented_id++; + } + + /** + * Binds route to uWS server instance and begins handling incoming requests. + * + * @private + * @param {Object} record { method, pattern, options, handler } + */ + _create_route(record) { + // Destructure record into route options + const { method, pattern, options, handler } = record; + + // Do not allow route creation once it is locked after a not found handler has been bound + if (this.#routes_locked === true) + throw new Error( + `HyperExpress: Routes/Routers must not be created or used after the Server.listen() has been called. [${method.toUpperCase()} ${pattern}]` + ); + + // Do not allow duplicate routes for performance/stability reasons + // We make an exception for 'upgrade' routes as they must replace the default route added by WebsocketRoute + if (method !== 'upgrade' && this.#routes[method][pattern]) + throw new Error( + `HyperExpress: Failed to create route as duplicate routes are not allowed. Ensure that you do not have any routers or routes that try to handle requests with the same pattern. [${method.toUpperCase()} ${pattern}]` + ); + + // Create a Route object to contain route information through handling process + const route = new Route({ + app: this, + method, + pattern, + options, + handler, + }); + + // Mark route as temporary if specified from options + if (options._temporary === true) route._temporary = true; + + // Handle websocket/upgrade routes separately as they follow a different lifecycle + switch (method) { + case 'ws': + // Create a WebsocketRoute which initializes uWS.ws() route + this.#routes[method][pattern] = new WebsocketRoute({ + app: this, + pattern, + handler, + options, + }); + break; + case 'upgrade': + // Throw an error if an upgrade route already exists that was not created by WebsocketRoute + const current = this.#routes[method][pattern]; + if (current && current._temporary !== true) + throw new Error( + `HyperExpress: Failed to create upgrade route as an upgrade route with the same pattern already exists and duplicate routes are not allowed. [${method.toUpperCase()} ${pattern}]` + ); + + // Overwrite the upgrade route that exists from WebsocketRoute with this custom route + this.#routes[method][pattern] = route; + + // Assign route to companion WebsocketRoute + const companion = this.#routes['ws'][pattern]; + if (companion) companion._set_upgrade_route(route); + break; + default: + // Store route in routes object for structural tracking + this.#routes[method][pattern] = route; + + // Bind the uWS route handler which pipes all incoming uWS requests to the HyperExpress request lifecycle + return this.#uws_instance[method](pattern, (response, request) => { + this._handle_uws_request(route, request, response, null); + }); + } + } + + /** + * Binds middleware to server instance and distributes over all created routes. + * + * @private + * @param {Object} record + */ + _create_middleware(record) { + // Destructure record from Router + const { pattern, middleware } = record; + + // Do not allow route creation once it is locked after a not found handler has been bound + if (this.#routes_locked === true) + throw new Error( + `HyperExpress: Routes/Routers must not be created or used after the Server.listen() has been called. [${method.toUpperCase()} ${pattern}]` + ); + + // Initialize middlewares array for specified pattern + if (this.#middlewares[pattern] == undefined) this.#middlewares[pattern] = []; + + // Create a middleware object with an appropriate priority + const object = { + id: this._get_incremented_id(), + pattern, + handler: middleware, + }; + + // Store middleware object in its pattern branch + this.#middlewares[pattern].push(object); + } + + /** + * Compiles the route and middleware structures for this instance for use in the uWS server. + * Note! This method will lock any future creation of routes or middlewares. + * @private + */ + _compile() { + // Bind the not found handler as a catchall route if the user did not already bind a global ANY catchall route + if (this.#handlers.on_not_found) { + const exists = this.#routes.any['/*'] !== undefined; + if (!exists) this.any('/*', (request, response) => this.#handlers.on_not_found(request, response)); + } + + // Iterate through all routes + Object.keys(this.#routes).forEach((method) => + Object.keys(this.#routes[method]).forEach((pattern) => this.#routes[method][pattern].compile()) + ); + + // Lock routes from further creation + this.#routes_locked = true; + } + + /* uWS -> Server Request/Response Handling Logic */ + + #pending_requests_count = 0; + #pending_requests_zero_handler = null; + + /** + * Resolves a single pending request and ticks sthe pending request handler if one exists. + */ + _resolve_pending_request() { + // Ensure we have at least one pending request + if (this.#pending_requests_count > 0) { + // Decrement the pending request count + this.#pending_requests_count--; + + // If we have no more pending requests and a zero pending request handler was set, execute it + if (this.#pending_requests_count === 0 && this.#pending_requests_zero_handler) + this.#pending_requests_zero_handler(); + } + } + + /** + * This method is used to handle incoming requests from uWS and pass them to the appropriate route through the HyperExpress request lifecycle. + * + * @private + * @param {Route} route + * @param {uWebSockets.HttpRequest} uws_request + * @param {uWebSockets.HttpResponse} uws_response + * @param {uWebSockets.us_socket_context_t=} socket + */ + _handle_uws_request(route, uws_request, uws_response, socket) { + // Construct the wrapper Request around uWS.HttpRequest + const request = new Request(route, uws_request); + request._raw_response = uws_response; + + // Construct the wrapper Response around uWS.Response + const response = new Response(uws_response); + response.route = route; + response._wrapped_request = request; + response._upgrade_socket = socket || null; + + // If we are in the process of gracefully shutting down, we must immediately close the request + if (this.#pending_requests_zero_handler) return response.close(); + + // Increment the pending request count + this.#pending_requests_count++; + + // Attempt to start the body parser for this request + // This method will return false If the request body is larger than the max_body_length + if (request._body_parser_run(response, route.max_body_length)) { + // Handle this request with the associated route + route.handle(request, response); + + // If by this point the response has not been sent then this is request is being asynchronously handled hence we must cork when the response is sent + if (!response.completed) response._cork = true; + } + } + + /* Safe Server Getters */ + + /** + * Returns the local server listening port of the server instance. + * @returns {Number} + */ + get port() { + // Initialize port if it does not exist yet + // Ensure there is a listening socket before returning port + if (this.#port === undefined) { + // Throw error if listening socket does not exist + if (!this.#listen_socket) + throw new Error( + 'HyperExpress: Server.port is not available as the server is not listening. Please ensure you called already Server.listen() OR have not yet called Server.close() when accessing this property.' + ); + + // Cache the resolved port + this.#port = uWebSockets.us_socket_local_port(this.#listen_socket); + } + + // Return port + return this.#port; + } + + /** + * Returns the server's internal uWS listening socket. + * @returns {uWebSockets.us_listen_socket=} + */ + get socket() { + return this.#listen_socket; + } + + /** + * Underlying uWS instance. + * @returns {uWebSockets.TemplatedApp} + */ + get uws_instance() { + return this.#uws_instance; + } + + /** + * Returns the Server Hostnames manager for this instance. + * Use this to support multiple hostnames on the same server with different SSL configurations. + * @returns {HostManager} + */ + get hosts() { + return this.#hosts; + } + + /** + * Server instance global handlers. + * @returns {Object} + */ + get handlers() { + return this.#handlers; + } + + /** + * Server instance routes. + * @returns {Object} + */ + get routes() { + return this.#routes; + } + + /** + * Server instance middlewares. + * @returns {Object} + */ + get middlewares() { + return this.#middlewares; + } +} + +module.exports = Server; diff --git a/packages/hyper-express/src/components/compatibility/ExpressRequest.js b/packages/hyper-express/src/components/compatibility/ExpressRequest.js new file mode 100644 index 0000000..c1fe39f --- /dev/null +++ b/packages/hyper-express/src/components/compatibility/ExpressRequest.js @@ -0,0 +1,208 @@ +'use strict'; +const Negotiator = require('negotiator'); +const parse_range = require('range-parser'); +const type_is = require('type-is'); +const is_ip = require('net').isIP; + +class ExpressRequest { + #negotiator; + + ExpressRequest() { + this.#negotiator = new Negotiator(this); + } + + /* Methods */ + get(name) { + let lowercase = name.toLowerCase(); + switch (lowercase) { + case 'referer': + // Continue execution to below case for catching of both spelling variations + case 'referrer': + return this.headers['referer'] || this.headers['referrer']; + default: + return this.headers[lowercase]; + } + } + + header(name) { + return this.get(name); + } + + accepts() { + let instance = accepts(this); + return instance.types.apply(instance, arguments); + } + + acceptsCharsets() { + charsets = flattened(charsets, arguments); + + // no charsets, return all requested charsets + if (!charsets || charsets.length === 0) { + return this.#negotiator.charsets(); + } + + return this.#negotiator.charsets(charsets)[0] || false; + } + + acceptsEncodings() { + encodings = flattened(encodings, arguments); + + // no encodings, return all requested encodings + if (!encodings || encodings.length === 0) { + return this.#negotiator.encodings(); + } + + return this.#negotiator.encodings(encodings)[0] || false; + } + + acceptsLanguages() { + languages = flattened(languages, arguments); + + // no languages, return all requested languages + if (!languages || languages.length === 0) { + return this.#negotiator.languages(); + } + + return this.#negotiator.languages(languages)[0] || false; + } + + range(size, options) { + let range = this.get('Range'); + if (!range) return; + return parse_range(size, range, options); + } + + param(name, default_value) { + // Parse three dataset candidates + let body = this.body; + let path_parameters = this.path_parameters; + let query_parameters = this.query_parameters; + + // First check path parameters, body, and finally query_parameters + if (null != path_parameters[name] && path_parameters.hasOwnProperty(name)) return path_parameters[name]; + if (null != body[name]) return body[name]; + if (null != query_parameters[name]) return query_parameters[name]; + + return default_value; + } + + is(types) { + // support flattened arguments + let arr = types; + if (!Array.isArray(types)) { + arr = new Array(arguments.length); + for (let i = 0; i < arr.length; i++) arr[i] = arguments[i]; + } + return type_is(this, arr); + } + + /* Properties */ + get baseUrl() { + return this.path; + } + + get originalUrl() { + return this.url; + } + + get fresh() { + this._throw_unsupported('fresh'); + } + + get params() { + return this.path_parameters; + } + + get hostname() { + // Retrieve the host header and determine if we can trust intermediary proxy servers + let host = this.get('X-Forwarded-Host'); + const trust_proxy = this.route.app._options.trust_proxy; + if (!host || !trust_proxy) { + // Use the 'Host' header as fallback + host = this.get('Host'); + } else { + // Note: X-Forwarded-Host is normally only ever a single value, but this is to be safe. + host = host.split(',')[0]; + } + + // If we don't have a host, return undefined + if (!host) return; + + // IPv6 literal support + let offset = host[0] === '[' ? host.indexOf(']') + 1 : 0; + let index = host.indexOf(':', offset); + return index !== -1 ? host.substring(0, index) : host; + } + + get ips() { + // Retrieve the client and proxy IP addresses + const client_ip = this.ip; + const proxy_ip = this.proxy_ip; + + // Determine if we can trust intermediary proxy servers and have a x-forwarded-for header + const trust_proxy = this.route.app._options.trust_proxy; + const x_forwarded_for = this.get('X-Forwarded-For'); + if (trust_proxy && x_forwarded_for) { + // Will split and return all possible IP addresses in the x-forwarded-for header (e.g. "client, proxy1, proxy2") + return x_forwarded_for.split(','); + } else { + // Returns all valid IP addresses available from uWS + return [client_ip, proxy_ip].filter((ip) => ip); + } + } + + get protocol() { + // Resolves x-forwarded-proto header if trust proxy is enabled + const trust_proxy = this.route.app._options.trust_proxy; + const x_forwarded_proto = this.get('X-Forwarded-Proto'); + if (trust_proxy && x_forwarded_proto) { + // Return the first protocol in the x-forwarded-proto header + // If the header contains a single value, the split will contain that value in the first index element anyways + return x_forwarded_proto.split(',')[0]; + } else { + // Use HyperExpress/uWS initially defined protocol as fallback + return this.route.app.is_ssl ? 'https' : 'http'; + } + } + + get query() { + return this.query_parameters; + } + + get secure() { + return this.protocol === 'https'; + } + + get signedCookies() { + this._throw_unsupported('signedCookies'); + } + + get stale() { + this._throw_unsupported('stale'); + } + + get subdomains() { + let hostname = this.hostname; + if (!hostname) return []; + + let offset = 2; + let subdomains = !is_ip(hostname) ? hostname.split('.').reverse() : [hostname]; + return subdomains.slice(offset); + } + + get xhr() { + return (this.get('X-Requested-With') || '').toLowerCase() === 'xmlhttprequest'; + } +} + +const flattened = function (arr, args) { + if (arr && !Array.isArray(arr)) { + arr = new Array(args.length); + for (var i = 0; i < arr.length; i++) { + arr[i] = args[i]; + } + } + return arr; +}; + +module.exports = ExpressRequest; diff --git a/packages/hyper-express/src/components/compatibility/ExpressResponse.js b/packages/hyper-express/src/components/compatibility/ExpressResponse.js new file mode 100644 index 0000000..693efd7 --- /dev/null +++ b/packages/hyper-express/src/components/compatibility/ExpressResponse.js @@ -0,0 +1,112 @@ +'use strict'; + +class ExpressResponse { + /* Methods */ + append(name, values) { + return this.header(name, values); + } + + setHeader(name, values) { + return this.append(name, values); + } + + writeHeaders(headers) { + Object.keys(headers).forEach((name) => this.header(name, headers[name])); + } + + setHeaders(headers) { + this.writeHeaders(headers); + } + + writeHeaderValues(name, values) { + values.forEach((value) => this.header(name, value)); + } + + getHeader(name) { + return this._headers[name]; + } + + removeHeader(name) { + delete this._headers[name]; + } + + setCookie(name, value, options) { + return this.cookie(name, value, null, options); + } + + hasCookie(name) { + return this._cookies && this._cookies[name] !== undefined; + } + + removeCookie(name) { + return this.cookie(name, null); + } + + clearCookie(name) { + return this.cookie(name, null); + } + + end(data) { + return this.send(data); + } + + format() { + this._throw_unsupported('format()'); + } + + get(name) { + let values = this._headers[name]; + if (values) return values.length == 0 ? values[0] : values; + } + + links(links) { + // Build chunks of links and combine into header spec + let chunks = []; + Object.keys(links).forEach((rel) => { + let url = links[rel]; + chunks.push(`<${url}>; rel="${rel}"`); + }); + + // Write the link header + this.header('link', chunks.join(', ')); + } + + location(path) { + return this.header('location', path); + } + + render() { + this._throw_unsupported('render()'); + } + + sendFile(path) { + return this.file(path); + } + + sendStatus(status_code) { + return this.status(status_code).send(); + } + + set(field, value) { + if (typeof field == 'object') { + const reference = this; + Object.keys(field).forEach((name) => { + let value = field[name]; + reference.header(name, value); + }); + } else { + this.header(field, value); + } + } + + vary(name) { + return this.header('vary', name); + } + + /* Properties */ + get headersSent() { + return this.initiated; + } +} + +module.exports = ExpressResponse; diff --git a/packages/hyper-express/src/components/compatibility/NodeRequest.js b/packages/hyper-express/src/components/compatibility/NodeRequest.js new file mode 100644 index 0000000..3d22592 --- /dev/null +++ b/packages/hyper-express/src/components/compatibility/NodeRequest.js @@ -0,0 +1,5 @@ +'use strict'; + +class NodeRequest {} + +module.exports = NodeRequest; diff --git a/packages/hyper-express/src/components/compatibility/NodeResponse.js b/packages/hyper-express/src/components/compatibility/NodeResponse.js new file mode 100644 index 0000000..c2c4ce4 --- /dev/null +++ b/packages/hyper-express/src/components/compatibility/NodeResponse.js @@ -0,0 +1,27 @@ +'use strict'; + +/** + * @typedef {Object} NodeResponseTypes + * @property {number} statusCode + * @property {string} statusMessage + */ +class NodeResponse { + /* Properties */ + get statusCode() { + return this._status_code; + } + + set statusCode(value) { + this._status_code = value; + } + + get statusMessage() { + return this._status_message; + } + + set statusMessage(value) { + this._status_message = value; + } +} + +module.exports = NodeResponse; diff --git a/packages/hyper-express/src/components/http/Request.js b/packages/hyper-express/src/components/http/Request.js new file mode 100644 index 0000000..4e47e7b --- /dev/null +++ b/packages/hyper-express/src/components/http/Request.js @@ -0,0 +1,1207 @@ +'use strict'; +const util = require('util'); +const cookie = require('cookie'); +const stream = require('stream'); +const busboy = require('busboy'); +const querystring = require('querystring'); +const signature = require('cookie-signature'); + +const MultipartField = require('../plugins/MultipartField.js'); +const NodeRequest = require('../compatibility/NodeRequest.js'); +const ExpressRequest = require('../compatibility/ExpressRequest.js'); +const { + inherit_prototype, + array_buffer_to_string, + copy_array_buffer_to_uint8array, +} = require('../../shared/operators.js'); +const { process_multipart_data } = require('../../shared/process-multipart.js'); +const UploadedFile = require('../../shared/uploaded-file.js'); + +class Request { + _locals; + _paused = false; + _request_ended = false; + _raw_request = null; + _raw_response = null; + _method = ''; + _url = ''; + _path = ''; + _query = ''; + _remote_ip = ''; + _remote_proxy_ip = ''; + _cookies; + _path_parameters; + _query_parameters; + _dto = undefined; + _user = undefined; + _all_input = undefined; + _body = undefined; + + /** + * The route that this request is being handled by. + */ + route = null; + + /** + * Underlying lazy initialized readable body stream. + * @private + */ + _readable = null; + + /** + * Returns whether all expected incoming request body chunks have been received. + * @returns {Boolean} + */ + received = true; // Assume there is no body data to stream + + /** + * Returns request headers from incoming request. + * @returns {Object.} + */ + headers = {}; + + /** + * Creates a new HyperExpress request instance that wraps a uWS.HttpRequest instance. + * + * @param {import('../router/Route.js')} route + * @param {import('uWebSockets.js').HttpRequest} raw_request + */ + constructor(route, raw_request) { + // Store reference to the route of this request and the raw uWS.HttpResponse instance for certain operations + this.route = route; + this._raw_request = raw_request; + + // Cache request properties from uWS.HttpRequest as it is stack allocated and will be deallocated after this function returns + this._query = raw_request.getQuery(); + this._path = route.path || raw_request.getUrl(); + this._method = route.method !== 'ANY' ? route.method : raw_request.getMethod(); + + // Cache request headers from uWS.HttpRequest as it is stack allocated and will be deallocated after this function returns + raw_request.forEach((key, value) => (this.headers[key] = value)); + + // Cache the path parameters from the route pattern if any as uWS.HttpRequest will be deallocated after this function returns + const num_path_parameters = route.path_parameters_key.length; + if (num_path_parameters) { + this._path_parameters = {}; + for (let i = 0; i < num_path_parameters; i++) { + const parts = route.path_parameters_key[i]; + this._path_parameters[parts[0]] = raw_request.getParameter(parts[1]); + } + } + } + + /* HyperExpress Methods */ + + /** + * Returns the raw uWS.HttpRequest instance. + * Note! This property is unsafe and should not be used unless you have no asynchronous code or you are accessing from the first top level synchronous middleware before any asynchronous code. + * @returns {import('uWebSockets.js').HttpRequest} + */ + get raw() { + return this._raw_request; + } + + /** + * Pauses the current request and flow of incoming body data. + * @returns {Request} + */ + pause() { + // Ensure there is content being streamed before pausing + // Ensure that the stream is currently not paused before pausing + if (!this._paused) { + this._paused = true; + this._raw_response.pause(); + if (this._readable) return this._super_pause(); + } + return this; + } + + /** + * Resumes the current request and flow of incoming body data. + * @returns {Request} + */ + resume() { + // Ensure there is content being streamed before resuming + // Ensure that the stream is currently paused before resuming + if (this._paused) { + this._paused = false; + this._raw_response.resume(); + if (this._readable) return this._super_resume(); + } + return this; + } + + /** + * Pipes the request body stream data to the provided destination stream with the provided set of options. + * + * @param {stream.Writable} destination + * @param {stream.WritableOptions} options + * @returns {Request} + */ + pipe(destination, options) { + // Pipe the arguments to the request body stream + this._super_pipe(destination, options); + + // Resume the request body stream as it will be in a paused state by default + return this._super_resume(); + } + + /** + * Securely signs a value with provided secret and returns the signed value. + * + * @param {String} string + * @param {String} secret + * @returns {String} String OR undefined + */ + sign(string, secret) { + return signature.sign(string, secret); + } + + /** + * Securely unsigns a value with provided secret and returns its original value upon successful verification. + * + * @param {String} signed_value + * @param {String} secret + * @returns {String=} String OR undefined + */ + unsign(signed_value, secret) { + let unsigned_value = signature.unsign(signed_value, secret); + if (unsigned_value !== false) return unsigned_value; + } + + /* Body Parsing */ + _body_parser_mode = 0; // 0 = none (awaiting mode), 1 = buffering (internal use), 2 = streaming (external use) + _body_limit_bytes = 0; + _body_received_bytes = 0; + _body_expected_bytes = -1; // We initialize this to -1 as we will use this to ensure the uWS.HttpResponse.onData() is only called once + _body_parser_flushing = false; + _body_chunked_transfer = false; + _body_parser_buffered; // This will hold the buffered chunks until the user decides to internally or externally consume the body data + _body_parser_passthrough; // This will be a passthrough chunk acceptor callback used by internal body parsers + + /** + * Begins parsing the incoming request body data within the provided limit in bytes. + * NOTE: This method will be a no-op if there is no expected body based on the content-length header. + * NOTE: This method can be called multiple times to update the bytes limit during the parsing process process. + * + * @private + * @param {import('./Response.js')} response + * @param {Number} bytes + * @returns {Boolean} Returns whether this request is within the bytes limit and should be handled further. + */ + _body_parser_run(response, limit_bytes) { + // Parse the content length into a number to ensure we have some body data to parse + // Even though it can be NaN, the > 0 check will handle this case and ignore NaN + // OR if the transfer-encoding header is chunked which means we will have to do a more inefficient chunked transfer + const content_length = Number(this.headers['content-length']); + const is_chunked_transfer = this.headers['transfer-encoding'] === 'chunked'; + if (content_length > 0 || is_chunked_transfer) { + // Determine if this is a first run meaning we have not began parsing the body yet + const is_first_run = this._body_expected_bytes === -1; + + // Update the limit and expected body bytes as these will be used to check if we are within the limit + this._body_limit_bytes = limit_bytes; + this._body_expected_bytes = is_chunked_transfer ? 0 : content_length; // We use 0 to indicate we do not know the content length with chunked transfers + + // We want to track if we are expecting a chunked transfer so depending logic does not treat the 0 expected bytes as an empty body + this._body_chunked_transfer = is_chunked_transfer; + + // Determine if this is a first time body parser run + if (is_first_run) { + // Set the request body to not received as we have some body data to parse + this.received = false; + + // Ensure future runs do not trigger the handling process + this._body_received_bytes = 0; + + // Initialize the array which will buffer the incoming chunks until a different parser mode is requested aka. user does something with the data + this._body_parser_buffered = []; + + // Bind the uWS.HttpResponse.onData() event handler to begin accepting incoming body data + this._raw_response.onData((chunk, is_last) => this._body_parser_on_chunk(response, chunk, is_last)); + } + + // Enforce the limit as we may have a different limit than the previous run + this._body_parser_enforce_limit(response); + } + + // Return whether the body parser is actively parsing the incoming body data + return !this._body_parser_flushing; + } + + /** + * Stops the body parser from accepting any more incoming body data. + * @private + */ + _body_parser_stop() { + // Return if we have no expected body length or already flushing the body + if (this._body_expected_bytes === -1 || this._body_parser_flushing) return; + + // Mark the body parser as flushing to prevent any more incoming body data from being accepted + this._body_parser_flushing = true; + + // Determine if we have a readable stream + if (this._readable) { + // Push an empty chunk to indicate the end of the stream + this.push(null); + + // Resume the readable stream to ensure in case it was paused to flush the buffered chunks + this.resume(); + } + } + + /** + * Checks if the body parser so far is within the bytes limit and triggers the limit handling if reached. + * + * @private + * @param {import('./Response.js')} response + * @returns {Boolean} Returns `true` when the body limit has been reached. + */ + _body_parser_enforce_limit(response) { + // Determine if we may have either received or are expecting more incoming bytes than the limit allows for + const incoming_bytes = Math.max(this._body_received_bytes, this._body_expected_bytes); + if (incoming_bytes > this._body_limit_bytes) { + // Stop the body parser from accepting any more incoming body data + this._body_parser_stop(); + + // Determine if we have not began sending a response yet and hence must send a response as soon as we can + if (!response.initiated) { + // If the server is instructed to do fast aborts, we will close the request immediately + if (this.route.app._options.fast_abort) { + response.close(); + } else if (this.received) { + // Otherwise, we will send a HTTP 413 Payload Too Large response once the request body has been fully flushed aka. received + response.status(413).send(); + } + } + + return true; + } + + return false; + } + + /** + * Processes incoming raw body data chunks from the uWS HttpResponse. + * + * @private + * @param {import('./Response.js')} response + * @param {ArrayBuffer} chunk + * @param {Boolean} is_last + */ + _body_parser_on_chunk(response, chunk, is_last) { + // If this chunk has no length and is not the last chunk, we will ignore it + if (!chunk.byteLength && !is_last) return; + + // Increment the received bytes counter by the byteLength of the incoming chunk + this._body_received_bytes += chunk.byteLength; + + // Determine if the body parser is active / not flushing + if (!this._body_parser_flushing) { + // Enforce the body parser limit as the number of incoming bytes may have exceeded the limit + const limited = this._body_parser_enforce_limit(response); + if (!limited) { + // Process this chunk depending on the current body parser mode + switch (this._body_parser_mode) { + // Awaiting mode - Awaiting the user to do something with the incoming body data + case 0: + // Buffer a COPIED Uint8Array chunk from the uWS volatile ArrayBuffer chunk + this._body_parser_buffered.push(copy_array_buffer_to_uint8array(chunk)); + + // If we have exceeded the Server.options.max_body_buffer number of buffered bytes, then pause the request to prevent more buffering + if (this._body_received_bytes > this.app._options.max_body_buffer) this.pause(); + break; + // Buffering mode - Internal use only + case 1: + // Pass through the uWS volatile ArrayBuffer chunk to the passthrough callback as a volatile Uint8Array chunk + this._body_parser_passthrough( + // If this is a chunked transfer, we need to COPY the chunk as any passthrough consumer will have no immediate way of processing + // hence this chunk needs to stick around across multiple cycles without being deallocated by uWS + this._body_chunked_transfer + ? copy_array_buffer_to_uint8array(chunk) + : new Uint8Array(chunk), + is_last, + ); + break; + // Streaming mode - External use only + case 2: + // Attempt to push a COPIED Uint8Array chunk from the uWS volatile ArrayBuffer chunk to the readable stream + // Pause the request if we have reached the highWaterMark to prevent backpressure + if (!this.push(copy_array_buffer_to_uint8array(chunk))) this.pause(); + + // If this is the last chunk, push a null chunk to indicate the end of the stream + if (is_last) this.push(null); + break; + } + } + } + + // Determine if this is the last chunk of the incoming body data to perform final closing operations + if (is_last) { + // Mark the request as fully received as we have flushed all incoming body data + this.received = true; + + // Emit the 'received' event that indicates how many bytes were received in total from the incoming body + if (this._readable) this.emit('received', this._body_received_bytes); + + // Enforce the body parser limit one last time in case the request is waiting for the body to be flushed before sending a response + if (this._body_parser_flushing) this._body_parser_enforce_limit(response); + } + } + + /** + * Flushes the buffered chunks to the appropriate body parser mode. + * @private + */ + _body_parser_flush_buffered() { + // Determine if we have any buffered chunks + if (this._body_parser_buffered) { + // Determine the body parser mode to flush the buffered chunks to + switch (this._body_parser_mode) { + // Buffering mode - Internal use only + case 1: + // Iterate over the buffered chunks and pass them to the passthrough callback + for (let i = 0; i < this._body_parser_buffered.length; i++) { + this._body_parser_passthrough( + this._body_parser_buffered[i], + i === this._body_parser_buffered.length - 1 ? this.received : false, + ); + } + break; + // Streaming mode - External use only + case 2: + // Iterate over the buffered chunks and push them to the readable stream + for (const chunk of this._body_parser_buffered) { + // Convert Uint8Array into a Buffer chunk + const buffer = Buffer.from(chunk); + + // Push the buffer to the readable stream + // We will ignore the return value as we are not handling backpressure here + this.push(buffer); + } + + // If the request has been received at this point already, we must also push a null chunk to indicate the end of the stream + if (this.received) this.push(null); + break; + } + } + + // Deallocate the buffered chunks array as they are no longer needed + this._body_parser_buffered = null; + + // Resume the request in case we had paused the request due to having reached the max_body_buffer for this request + this.resume(); + } + + /** + * This method is called when the underlying Readable stream is initialized and begins expecting incoming data. + * @private + */ + _body_parser_stream_init() { + // Set the body parser mode to stream mode + this._body_parser_mode = 2; + + // Overwrite the underlying readable _read handler to resume the request when more chunks are requested + // This will properly handle backpressure and prevent the request from being paused forever + this._readable._read = () => this.resume(); + + // Flush the buffered chunks to the readable stream if we have any + this._body_parser_flush_buffered(); + } + + _received_data_promise; + /** + * Returns a single Uint8Array buffer which contains all incoming body data. + * @private + * @returns {Promise} + */ + _body_parser_get_received_data() { + // Return the current promise if it exists + if (this._received_data_promise) return this._received_data_promise; + + // If this is not a chunked transfer and we have no expected body length, we will return an empty buffer as we have no body data to parse + if (!this._body_chunked_transfer && this._body_expected_bytes <= 0) return Promise.resolve(new Uint8Array(0)); + + // Create a new promise which will be resolved once all incoming body data has been received + this._received_data_promise = new Promise((resolve) => { + // Determine if this is a chunked transfer + if (this._body_chunked_transfer) { + // Since we don't know how many or how much each chunk will be, we have to store all the chunks + // After all the chunks have been received, we will concatenate them into a single Uint8Array + const chunks = []; + + // Define a passthrough callback which will be called for each incoming chunk + this._body_parser_passthrough = (chunk, is_last) => { + // Push the chunk to the chunks array + chunks.push(chunk); + + // If this is the last chunk, call the callback with the body buffer + if (is_last) { + // Initialize a new Uint8Array of size received bytes + let offset = 0; + const buffer = new Uint8Array(this._body_received_bytes); + for (const chunk of chunks) { + // Write the chunk into the body buffer at the current offset + buffer.set(chunk, offset); + offset += chunk.byteLength; + } + + // Resolve the promise with the body buffer + resolve(buffer); + } + }; + } else { + // Initialize the full size body Uint8Array buffer based on the expected body length + // We will copy all volatile chunk data onto this stable buffer for memory efficiency + const buffer = new Uint8Array(this._body_expected_bytes); + + // Define a passthrough callback which will be called for each incoming chunk + let offset = 0; + this._body_parser_passthrough = (chunk, is_last) => { + // Write the chunk into the body buffer at the current offset + buffer.set(chunk, offset); + + // Increment the offset by the byteLength of the incoming chunk + offset += chunk.byteLength; + + // If this is the last chunk, call the callback with the body buffer + if (is_last) resolve(buffer); + }; + } + + // Set the body parser mode to buffering mode as we want to receive all incoming chunks through the passthrough callback + this._body_parser_mode = 1; + + // Flush the buffered chunks so the passthrough callback receives all buffered data through its callback + this._body_parser_flush_buffered(); + }); + + // Return the data promise + return this._received_data_promise; + } + + _body_buffer; + _buffer_promise; + /** + * Returns the incoming request body as a Buffer. + * @returns {Promise} + */ + buffer() { + // Check cache and return if body has already been parsed + if (this._body_buffer) return Promise.resolve(this._body_buffer); + + // Initialize the buffer promise if it does not exist + this._buffer_promise = new Promise((resolve) => + this._body_parser_get_received_data().then((raw) => { + // Convert the Uint8Array buffer into a Buffer + this._body_buffer = Buffer.from(raw); + + // Resolve the buffer promise with the body buffer + resolve(this._body_buffer); + }), + ); + + // Return the buffer promise + return this._buffer_promise; + } + + /** + * Decodes the incoming request body as a String. + * @private + * @param {Uint8Array} uint8 + * @param {string} encoding + * @returns {string} + */ + _uint8_to_string(uint8, encoding = 'utf-8') { + const decoder = new util.TextDecoder(encoding); + return decoder.decode(uint8); + } + + _body_text; + _text_promise; + /** + * Downloads and parses the request body as a String. + * @returns {Promise} + */ + text() { + // Resolve from cache if available + if (this._body_text) return Promise.resolve(this._body_text); + + // Initialize the text promise if it does not exist + this._text_promise = new Promise((resolve) => + this._body_parser_get_received_data().then((raw) => { + // Decode the Uint8Array buffer into a String + this._body_text = this._uint8_to_string(raw); + + // Resolve the text promise with the body text + resolve(this._body_text); + }), + ); + + // Return the text promise + return this._text_promise; + } + + _body_binary; + _binary_promise; + /** + * Downloads and parses the request body as a String. + * @returns {Promise} + */ + binary() { + // Resolve from cache if available + if (this._body_binary) return Promise.resolve(this._body_binary); + + // Initialize the text promise if it does not exist + this._binary_promise = new Promise((resolve) => + this._body_parser_get_received_data().then((raw) => { + // Convert the Uint8Array buffer into a Buffer + this._body_binary = Buffer.from(raw); + + // Resolve the text promise with the body text + resolve(this._body_binary); + }), + ); + + // Return the text promise + return this._text_promise; + } + + _body_json; + _json_promise; + /** + * Downloads and parses the request body as a JSON object. + * Passing default_value as null will lead to the function throwing an exception if invalid JSON is received. + * + * @param {Any=} default_value Default: {} + * @returns {Promise} + */ + json(default_value = {}) { + // Return from cache if available + if (this._body_json) return Promise.resolve(this._body_json); + + // Initialize the json promise if it does not exist + this._json_promise = new Promise((resolve, reject) => + this._body_parser_get_received_data().then((raw) => { + // Decode the Uint8Array buffer into a String + const text = this._uint8_to_string(raw); + try { + // Parse the text as JSON + this._body_json = JSON.parse(text); + } catch (error) { + if (default_value) { + // Use the default value if provided + this._body_json = default_value; + } else { + reject(error); + } + } + + // Resolve the json promise with the body json + resolve(this._body_json); + }), + ); + + // Return the json promise + return this._json_promise; + } + + _body_urlencoded; + _urlencoded_promise; + /** + * Parses and resolves an Object of urlencoded values from body. + * @returns {Promise} + */ + urlencoded() { + // Return from cache if available + if (this._body_urlencoded) return Promise.resolve(this._body_urlencoded); + + // Initialize the urlencoded promise if it does not exist + this._urlencoded_promise = new Promise((resolve) => + this._body_parser_get_received_data().then((raw) => { + // Decode the Uint8Array buffer into a String + const text = this._uint8_to_string(raw); + + // Parse the text as urlencoded + this._body_urlencoded = querystring.parse(text); + + // Resolve the urlencoded promise with the body urlencoded + resolve(this._body_urlencoded); + }), + ); + + // Return the urlencoded promise + return this._urlencoded_promise; + } + + _multipart_promise; + /** + * Handles incoming multipart fields from uploader and calls user specified handler with MultipartField. + * + * @private + * @param {Function} handler + * @param {String} name + * @param {String|stream.Readable} value + * @param {Object} info + */ + async _on_multipart_field(handler, name, value, info) { + // Create a MultipartField instance with the incoming information + const field = new MultipartField(name, value, info); + + // Check if a field is being handled by the user across a different exeuction + if (this._multipart_promise instanceof Promise) { + // Pause the request to prevent more fields from being received + this.pause(); + + // Wait for this field to be handled + if (this._multipart_promise) await this._multipart_promise; + + // Resume the request to accept more fields + this.resume(); + } + + // Determine if the handler is a synchronous function and returns a promise + const output = handler(field); + if (output instanceof Promise) { + // Store the promise, so concurrent multipart fields can wait for it + this._multipart_promise = output; + + // Hold the current exectution context until the promise resolves + if (this._multipart_promise) await this._multipart_promise; + + // Clear the promise reference + this._multipart_promise = null; + } + + // Flush this field's file stream if it has not been consumed by the user in the handler execution + // This is neccessary as defined in the Busboy documentation to prevent holding up the processing + if (field.file && !field.file.stream.readableEnded) field.file.stream.resume(); + } + + /** + * @typedef {function(MultipartField):void} SyncMultipartHandler + */ + + /** + * @typedef {function(MultipartField):Promise} AsyncMultipartHandler + */ + + /** + * @typedef {('PARTS_LIMIT_REACHED'|'FILES_LIMIT_REACHED'|'FIELDS_LIMIT_REACHED')} MultipartLimitReject + */ + + /** + * Downloads and parses incoming body as a multipart form. + * This allows for easy consumption of fields, values and files. + * + * @param {busboy.BusboyConfig|SyncMultipartHandler|AsyncMultipartHandler} options + * @param {(SyncMultipartHandler|AsyncMultipartHandler)=} handler + * @returns {Promise} A promise which is resolved once all multipart fields have been processed + */ + multipart(options, handler) { + // Migrate options to handler if no options object is provided by user + if (typeof options == 'function') { + handler = options; + options = {}; + } + + // Make a shallow copy of the options object + options = Object.assign({}, options); + + // Inject the request headers into the busboy options if not provided + if (!options.headers) options.headers = this.headers; + + // Ensure the provided handler is a function type + if (typeof handler !== 'function') + throw new Error('HyperExpress: Request.multipart(handler) -> handler must be a Function.'); + + // Resolve instantly if we have no readable body stream + if (this.readableEnded) return Promise.resolve(); + + // Resolve instantly if we do not have a valid multipart content type header + const content_type = this.headers['content-type']; + if (!/^(multipart\/.+);(.*)$/i.test(content_type)) return Promise.resolve(); + + // Return a promise which will be resolved after all incoming multipart data has been processed + const reference = this; + return new Promise((resolve, reject) => { + // Create a Busboy instance which will perform + const uploader = busboy(options); + + // Create a function to finish the uploading process + let finished = false; + const finish = async (error) => { + // Ensure we are not already finished + if (finished) return; + finished = true; + + // Determine if the caught error should be silenced + let silent_error = false; + if (error instanceof Error) { + // Silence the BusBoy "Unexpected end of form" error + // This usually happens when the client abruptly closes the connection + if (error.message == 'Unexpected end of form') silent_error = true; + } + + // Resolve/Reject the promise depending on whether an error occurred + if (error && !silent_error) { + // Reject the promise if an error occurred + reject(error); + } else { + // Wait for any pending multipart handler exeuction to complete + if (reference._multipart_promise) await reference._multipart_promise; + + // Resolve the promise if no error occurred + resolve(); + } + + // Stop the body parser from accepting any more incoming body data + reference._body_parser_stop(); + + // Destroy the uploader instance + uploader.destroy(); + }; + + // Bind an 'error' event handler to emit errors + uploader.once('error', finish); + + // Bind limit event handlers to reject as error code constants + uploader.once('partsLimit', () => finish('PARTS_LIMIT_REACHED')); + uploader.once('filesLimit', () => finish('FILES_LIMIT_REACHED')); + uploader.once('fieldsLimit', () => finish('FIELDS_LIMIT_REACHED')); + + // Define a function to handle incoming multipart data + const on_field = (name, value, info) => { + // Catch and pipe any errors from the value readable stream to the finish function + if (value instanceof stream.Readable) value.once('error', finish); + + // Call the user defined handler with the incoming multipart field + // Catch and pipe any errors to the finish function + reference._on_multipart_field(handler, name, value, info).catch(finish); + }; + + // Bind a 'field' event handler to process each incoming field + uploader.on('field', on_field); + + // Bind a 'file' event handler to process each incoming file + uploader.on('file', on_field); + + // Bind a 'finish' event handler to resolve the upload promise + uploader.once('close', () => { + // Wait for any pending multipart handler exeuction to complete + if (reference._multipart_promise) { + // Wait for the pending promise to resolve + // Use an anonymous callback for the .then() to prevent finish() from receving a resolved value which would lead to an error finish + reference._multipart_promise.then(() => finish()).catch(finish); + } else { + finish(); + } + }); + + // Pipe the readable request stream into the busboy uploader + reference.pipe(uploader); + }); + } + + /* HyperExpress Properties */ + + /** + * Returns the request locals for this request. + * @returns {Object.} + */ + get locals() { + // Initialize locals object if it does not exist + if (!this._locals) this._locals = {}; + return this._locals; + } + + /** + * Returns the HyperExpress.Server instance this Request object originated from. + * @returns {import('../Server.js')} + */ + get app() { + return this.route.app; + } + + /** + * Returns whether this request is in a paused state and thus not consuming any body chunks. + * @returns {Boolean} + */ + get paused() { + return this._paused; + } + + /** + * Returns HTTP request method for incoming request in uppercase. + * @returns {String} + */ + get method() { + // Enforce uppercase for the returned method value + const uppercase = this._method.toUpperCase(); + + // For some reason, uWebsockets.js populates DELETE requests as DEL hence this translation + return uppercase === 'DEL' ? 'DELETE' : uppercase; + } + + /** + * Returns full request url for incoming request (path + query). + * @returns {String} + */ + get url() { + // Return from cache if available + if (this._url) return this._url; + + // Parse the incoming request url + this._url = this._path + (this._query ? '?' + this._query : ''); + + // Return the url + return this._url; + } + + /** + * Returns path for incoming request. + * @returns {String} + */ + get path() { + return this._path; + } + + /** + * Returns query for incoming request without the '?'. + * @returns {String} + */ + get path_query() { + return this._query; + } + + /** + * Returns request cookies from incoming request. + * @returns {Object.} + */ + get cookies() { + // Return from cache if already parsed once + if (this._cookies) return this._cookies; + + // Parse cookies from Cookie header and cache results + const header = this.headers['cookie']; + this._cookies = header ? cookie.parse(header) : {}; + + // Return the cookies + return this._cookies; + } + + /** + * Returns path parameters from incoming request. + * @returns {Object.} + */ + get path_parameters() { + return this._path_parameters || {}; + } + + /** + * Returns query parameters from incoming request. + * @returns {Object.} + */ + get query_parameters() { + // Return from cache if already parsed once + if (this._query_parameters) return this._query_parameters; + + // Parse query using querystring and cache results + this._query_parameters = querystring.parse(this._query); + return this._query_parameters; + } + + /** + * Returns remote IP address in string format from incoming request. + * Note! You cannot call this method after the response has been sent or ended. + * @returns {String} + */ + get ip() { + // Resolve IP from cache if already resolved + if (this._remote_ip) return this._remote_ip; + + // Ensure request has not ended yet + if (this._request_ended) + throw new Error('HyperExpress.Request.ip cannot be consumed after the Request/Response has ended.'); + + // Determine if we can trust intermediary proxy servers and have a x-forwarded-for header + const x_forwarded_for = this.get('X-Forwarded-For'); + const trust_proxy = this.route.app._options.trust_proxy; + if (trust_proxy && x_forwarded_for) { + // The first IP in the x-forwarded-for header is the client IP if we trust proxies + this._remote_ip = x_forwarded_for.split(',')[0]; + } else { + // Use the uWS detected connection IP address as a fallback + this._remote_ip = array_buffer_to_string(this._raw_response.getRemoteAddressAsText()); + } + + // Return Remote IP + return this._remote_ip; + } + + /** + * Returns remote proxy IP address in string format from incoming request. + * Note! You cannot call this method after the response has been sent or ended. + * @returns {String} + */ + get proxy_ip() { + // Resolve IP from cache if already resolved + if (this._remote_proxy_ip) return this._remote_proxy_ip; + + // Ensure request has not ended yet + if (this._request_ended) + throw new Error('HyperExpress.Request.proxy_ip cannot be consumed after the Request/Response has ended.'); + + // Parse and cache remote proxy IP from uWS + this._remote_proxy_ip = array_buffer_to_string(this._raw_response.getProxiedRemoteAddressAsText()); + + // Return Remote Proxy IP + return this._remote_proxy_ip; + } + + /** + * Throws an ERR_INCOMPATIBLE_CALL error with the provided property/method name. + * @private + */ + _throw_unsupported(name) { + throw new Error( + `ERR_INCOMPATIBLE_CALL: One of your middlewares or route logic tried to call Request.${name} which is unsupported with HyperExpress.`, + ); + } + + setDto(dto) { + this._dto = dto; + } + + dto() { + return this._dto; + } + + body() { + return this._body; + } + + _body; + _body_promise; + async processBody() { + if (this._body) return Promise.resolve(this._body); + const contentType = this.headers['content-type'] || ''; + this._body_promise = new Promise(async (resolve, reject) => { + let bodyData = undefined; + + if (contentType === 'application/json') { + bodyData = await this.json(); + } else if (contentType === 'application/x-www-form-urlencoded') { + bodyData = await this.urlencoded(); + } else if (contentType.includes('multipart/form-data')) { + bodyData = await process_multipart_data(this); + } else if (contentType === 'text/plain') { + bodyData = { $body: await this.text() }; + } else if (contentType === 'text/html' || contentType === 'application/xml') { + bodyData = { $body: (await this.buffer()).toString() }; + } else { + bodyData = { $body: await this.buffer() }; + } + + this._body = bodyData; + + resolve(this._body); + }); + + return this._body_promise; + } + + _all_input; + all() { + if (this._all_input) return this._all_input; + + this._all_input = { + ...(this.query_parameters || {}), + ...(this.path_parameters || {}), + ...(this._body || {}), + }; + + return this._all_input; + } + + input(key, defaultValue) { + const all = this.all(); + return all[key] ?? defaultValue; + } + + string(key, defaultValue) { + const all = this.all(); + return String(all[key] ?? defaultValue); + } + + number(key, defaultValue) { + const all = this.all(); + return Number(all[key] ?? defaultValue); + } + + boolean(key) { + const all = this.all(); + return [1, '1', true, 'true', 'yes', 'on'].includes(all[key]); + } + + has(...keys) { + const payload = this.all(); + for (const key of keys) { + if (!(key in payload)) return false; + } + + return true; + } + + hasAny(...keys) { + const payload = this.all(); + for (const key of keys) { + if (key in payload) return true; + } + + return false; + } + + missing(...keys) { + const payload = this.all(); + for (const key of keys) { + if (key in payload) return false; + } + + return true; + } + + hasHeader(name) { + return name in this.headers; + } + + bearerToken() { + const authHeader = this.headers['authorization']; + const asArray = authHeader?.split(' '); + if (asArray?.length) return asArray[1]; + return undefined; + } + + httpHost() { + return this.protocol; + } + + isHttp() { + return this.httpHost() === 'http'; + } + + isHttps() { + return this.httpHost() === 'https'; + } + + fullUrl() { + return this.url; + } + + isMethod(method) { + return this.method.toLowerCase() === method.toLowerCase(); + } + + contentType() { + return this.headers['content-type']; + } + + getAcceptableContentTypes() { + return this.headers['accept']; + } + + accepts() { + return (this.headers['accept'] || '').split(','); + } + + expectsJson() { + return this.accepts().includes('application/json'); + } + + setUser(user) { + this._user = user; + } + + user() { + return this._user; + } + + isPath(pathPattern) { + return this.path === pathPattern; + } + + hasHeaders(...keys) { + return keys.every((key) => key in this.headers); + } + + _validatorClass; + setValidator(cls) { + this._validatorClass = cls; + } + + async validate(schema) { + const payload = await this.all(); + const dto = this._dto; + const validator = this._validatorClass.compareWith(schema); + if (dto) { + await validator.validateDto(dto); + } else { + const dto = await validator.addMeta({}).validateRaw({ ...payload }); + this.setDto(dto); + } + return true; + } + + file(key) { + const dataAtKey = this._body[key]; + if (Array.isArray(dataAtKey)) { + for (const dataAtIndex of dataAtKey) { + if (dataAtIndex instanceof UploadedFile) return dataAtKey; + } + } else if (dataAtKey instanceof UploadedFile) { + return dataAtKey; + } + + return undefined; + } +} + +// Inherit the compatibility classes +inherit_prototype({ + from: [NodeRequest.prototype, ExpressRequest.prototype], + to: Request.prototype, + method: (type, name, original) => { + // Return an anonymous function which calls the original function with Request scope + return function () { + // Call the original function with the Request scope + return original.apply(this, arguments); + }; + }, +}); + +// Inherit the stream.Readable prototype and lazy initialize the stream on first call to inherited methods +inherit_prototype({ + from: stream.Readable.prototype, + to: Request.prototype, + override: (name) => '_super_' + name, // Prefix all overrides with _super_ + method: (type, name, original) => { + // Initialize a pass through method + const passthrough = function () { + // Determine if the underlying readable stream has not been initialized yet + if (this._readable === null) { + // Initialize the readable stream with the route's streaming configuration + this._readable = new stream.Readable(this.route.streaming.readable); + + // Trigger the readable stream initialization event + this._body_parser_stream_init(); + } + + // Return the original function with the readable stream as the context + return original.apply(this._readable, arguments); + }; + + return passthrough; + }, +}); + +module.exports = Request; diff --git a/packages/hyper-express/src/components/http/Response.js b/packages/hyper-express/src/components/http/Response.js new file mode 100644 index 0000000..3255bc1 --- /dev/null +++ b/packages/hyper-express/src/components/http/Response.js @@ -0,0 +1,1025 @@ +'use strict'; +const crypto = require('crypto'); +const cookie = require('cookie'); +const signature = require('cookie-signature'); +const status_codes = require('http').STATUS_CODES; +const mime_types = require('mime-types'); +const stream = require('stream'); + +const NodeResponse = require('../compatibility/NodeResponse.js'); +const ExpressResponse = require('../compatibility/ExpressResponse.js'); +const { inherit_prototype } = require('../../shared/operators.js'); + +const FilePool = {}; +const LiveFile = require('../plugins/LiveFile.js'); +const SSEventStream = require('../plugins/SSEventStream.js'); + +class Response { + _sse; + _locals; + route = null; + _corked = false; + _streaming = false; + _middleware_cursor; + _wrapped_request = null; + _upgrade_socket = null; + _raw_response = null; + + /** + * Returns the custom HTTP underlying status code of the response. + * @private + * @type {Number=} + */ + _status_code; + + /** + * Returns the custom HTTP underlying status code message of the response. + * @private + * @type {String=} + */ + _status_message; + + /** + * Contains underlying headers for the response. + * @private + * @type {Record} + */ + _cookies; + + /** + * Underlying lazy initialized writable body stream. + * @private + */ + _writable = null; + + /** + * Whether this response needs to cork before sending. + * @private + */ + _cork = false; + + /** + * Alias of aborted property as they both represent the same request state in terms of inaccessibility. + * @returns {Boolean} + */ + completed = false; + + /** + * Returns whether response has been initiated by writing the HTTP status code and headers. + * Note! No changes can be made to the HTTP status code or headers after a response has been initiated. + * @returns {Boolean} + */ + initiated = false; + + /** + * Creates a new HyperExpress response instance that wraps a uWS.HttpResponse instance. + * + * @param {import('uWebSockets.js').HttpResponse} raw_response + */ + constructor(raw_response) { + this._raw_response = raw_response; + + // Bind the abort handler as required by uWebsockets.js for each uWS.HttpResponse to allow for async processing + raw_response.onAborted(() => { + // If this request has already been completed then this request cannot be aborted again + if (this.completed) return; + this.completed = true; + + // Decrement the pending request count + this.route.app._resolve_pending_request(); + + // Stop the body parser from accepting any more data + this._wrapped_request._body_parser_stop(); + + // Ensure we have a writable/emitter instance to emit over + if (this._writable) { + // Emit an 'abort' event to signify that the client aborted the request + this.emit('abort', this._wrapped_request, this); + + // Emit an 'close' event to signify that the client has disconnected + this.emit('close', this._wrapped_request, this); + } + }); + } + + /* HyperExpress Methods */ + + /** + * Tracks middleware cursor position over a request's lifetime. + * This is so we can detect any double middleware iterations and throw an error. + * @private + * @param {Number} position - Cursor position + */ + _track_middleware_cursor(position) { + // Track and ensure each middleware cursor value is greater than previously tracked value for sequential progression + if (this._middleware_cursor === undefined || position > this._middleware_cursor) + return (this._middleware_cursor = position); + + // If position is not greater than last cursor then we likely have a double middleware execution + this.throw( + new Error( + 'ERR_DOUBLE_MIDDLEWARE_EXEUCTION_DETECTED: Please ensure you are not calling the next() iterator inside of an ASYNC middleware. You must only call next() ONCE per middleware inside of SYNCHRONOUS middlewares only!', + ), + ); + } + + /* Response Methods/Operators */ + + /** + * Alias of `uWS.HttpResponse.cork()` which allows for manual corking of the response. + * This is required by `uWebsockets.js` to maximize network performance with batched writes. + * + * @param {Function} handler + * @returns {Response} Response (Chainable) + */ + atomic(handler) { + // Cork the provided handler if the response is not finished yet + if (!this.completed) this._raw_response.cork(handler); + + // Make this chainable + return this; + } + + /** + * This method is used to set a custom response code. + * + * @param {Number} code Example: response.status(403) + * @param {String=} message Example: response.status(403, 'Forbidden') + * @returns {Response} Response (Chainable) + */ + status(code, message) { + // Set the numeric status code. Status text is appended before writing status to uws + this._status_code = code; + this._status_message = message; + return this; + } + + /** + * This method is used to set the response content type header based on the provided mime type. Example: type('json') + * + * @param {String} mime_type Mime type + * @returns {Response} Response (Chainable) + */ + type(mime_type) { + // Remove leading dot from mime type if present + if (mime_type[0] === '.') mime_type = mime_type.substring(1); + + // Determine proper mime type and send response + this.header('content-type', mime_types.contentType(mime_type) || 'text/plain'); + return this; + } + + /** + * This method can be used to write a response header and supports chaining. + * + * @param {String} name Header Name + * @param {String|String[]} value Header Value + * @param {Boolean=} overwrite If true, overwrites existing header value with same name + * @returns {Response} Response (Chainable) + */ + header(name, value, overwrite) { + // Enforce lowercase for header name + name = name.toLowerCase(); + + // Determine if this operation is an overwrite onto any existing header values + if (overwrite) { + // Overwrite the header value + this._headers[name] = value; + + // Check if some value(s) already exist for this header name + } else if (this._headers[name]) { + // Check if there are multiple current values for this header name + if (Array.isArray(this._headers[name])) { + // Check if the provided value is an array + if (Array.isArray(value)) { + // Concatenate the current and provided header values + this._headers[name] = this._headers[name].concat(value); + } else { + // Push the provided header value to the current header values array + this._headers[name].push(value); + } + } else { + // Convert the current header value to an array + this._headers[name] = [this._headers[name], value]; + } + } else { + // Write the header value + this._headers[name] = value; + } + + // Make chainable + return this; + } + + /** + * @typedef {Object} CookieOptions + * @property {String} domain + * @property {String} path + * @property {Number} maxAge + * @property {Boolean} secure + * @property {Boolean} httpOnly + * @property {Boolean|'none'|'lax'|'strict'} sameSite + * @property {String} secret + */ + + /** + * This method is used to write a cookie to incoming request. + * To delete a cookie, set the value to null. + * + * @param {String} name Cookie Name + * @param {String|null} value Cookie Value + * @param {Number=} expiry In milliseconds + * @param {CookieOptions=} options Cookie Options + * @param {Boolean=} sign_cookie Enables/Disables Cookie Signing + * @returns {Response} Response (Chainable) + */ + cookie(name, value, expiry, options, sign_cookie = true) { + // Determine if this is a delete operation and recursively call self with appropriate options + if (name && value === null) + return this.cookie(name, '', null, { + maxAge: 0, + }); + + // If an options object was not provided, shallow copy it to prevent mutation to the original object + // If an options object was not provided, create a new object with default options + options = options + ? { ...options } + : { + secure: true, + sameSite: 'none', + path: '/', + }; + + // Determine if a expiry duration was provided in milliseconds + if (typeof expiry == 'number') { + // Set the expires value of the cookie if one was not already defined + options.expires = options.expires || new Date(Date.now() + expiry); + + // Define a max age if one was not already defined + options.maxAge = options.maxAge || Math.round(expiry / 1000); + } + + // Sign cookie value if signing is enabled and a valid secret is provided + if (sign_cookie && typeof options.secret == 'string') { + options.encode = false; // Turn off encoding to prevent loss of signature structure + value = signature.sign(value, options.secret); + } + + // Initialize the cookies holder object if it does not exist + if (this._cookies == undefined) this._cookies = {}; + + // Store the seralized cookie value to be written during response + this._cookies[name] = cookie.serialize(name, value, options); + return this; + } + + /** + * This method is used to upgrade an incoming upgrade HTTP request to a Websocket connection. + * @param {Object=} context Store information about the websocket connection + */ + upgrade(context) { + // Do not allow upgrades if request is already completed + if (this.completed) return; + + // Ensure a upgrade_socket exists before upgrading ensuring only upgrade handler requests are handled + if (this._upgrade_socket == null) + this.throw( + new Error( + 'HyperExpress: You cannot upgrade a request that does not come from an upgrade handler. No upgrade socket was found.', + ), + ); + + // Resume the request in case it was paused + this._wrapped_request.resume(); + + // Cork the response if it has not been corked yet for when this was handled asynchonously + if (this._cork && !this._corked) { + this._corked = true; + return this.atomic(() => this.upgrade(context)); + } + + // Call uWS.Response.upgrade() method with user data, protocol headers and uWS upgrade socket + const headers = this._wrapped_request.headers; + this._raw_response.upgrade( + { + context, + }, + headers['sec-websocket-key'], + headers['sec-websocket-protocol'], + headers['sec-websocket-extensions'], + this._upgrade_socket, + ); + + // Mark request as complete so no more operations can be performed + this.completed = true; + + // Decrement the pending request count + this.route.app._resolve_pending_request(); + } + + /** + * Initiates response process by writing HTTP status code and then writing the appropriate headers. + * @private + * @returns {Boolean} + */ + _initiate_response() { + // Halt execution if response has already been initiated or completed + if (this.initiated) return false; + + // Emit the 'prepare' event to allow for any last minute response modifications + if (this._writable) this.emit('prepare', this._wrapped_request, this); + + // Mark the instance as initiated signifying that no more status/header based operations can be performed + this.initiated = true; + + // Resume the request in case it was paused + this._wrapped_request.resume(); + + // Write the appropriate status code to the response along with mapped status code message + if (this._status_code || this._status_message) + this._raw_response.writeStatus( + this._status_code + ' ' + (this._status_message || status_codes[this._status_code]), + ); + + // Iterate through all headers and write them to uWS + for (const name in this._headers) { + // If this is a custom content-length header, we need to skip it as we will write it later during the response send + if (name == 'content-length') continue; + + // Write the header value to uWS + const values = this._headers[name]; + if (Array.isArray(values)) { + // Write each individual header value to uWS as there are multiple headers + for (const value of values) { + this._raw_response.writeHeader(name, value); + } + } else { + // Write the single header value to uWS + this._raw_response.writeHeader(name, values); + } + } + + // Iterate through all cookies and write them to uWS + if (this._cookies) { + for (const name in this._cookies) { + this._raw_response.writeHeader('set-cookie', this._cookies[name]); + } + } + + // Signify that the response was successfully initiated + return true; + } + + _drain_handler = null; + /** + * Binds a drain handler which gets called with a byte offset that can be used to try a failed chunk write. + * You MUST perform a write call inside the handler for uWS chunking to work properly. + * You MUST return a boolean value indicating if the write was successful or not. + * Note! This method can only provie drain events to a single handler at any given time which means If you call this method again with a different handler, it will stop providing drain events to the previous handler. + * + * @param {function(number):boolean} handler Synchronous callback only + */ + drain(handler) { + // Determine if this is the first time the drain handler is being set + const is_first_time = this._drain_handler === null; + + // Store the handler which will be used to provide drain events to uWS + this._drain_handler = handler; + + // Bind a writable handler with a fallback return value to true as uWS expects a Boolean + if (is_first_time) + this._raw_response.onWritable((offset) => { + // Retrieve the write result from the handler + const output = this._drain_handler(offset); + + // Throw an exception if the handler did not return a boolean value as that is an improper implementation + if (typeof output !== 'boolean') + this.throw( + new Error( + 'HyperExpress: Response.drain(handler) -> handler must return a boolean value stating if the write was successful or not.', + ), + ); + + // Return the boolean value to uWS as required by uWS documentation + return output; + }); + } + + /** + * Writes the provided chunk to the client over uWS with backpressure handling if a callback is provided. + * + * @private + * @param {String|Buffer|ArrayBuffer} chunk + * @param {String} encoding + * @param {Function} callback + */ + _write(chunk, encoding, callback) { + // Spread the arguments to allow for a single object argument + if (chunk.chunk && chunk.encoding) { + // Pull out the chunk and encoding from the object argument + const temp = chunk; + chunk = temp.chunk; + encoding = temp.encoding; + + // Only use the callback from this specific chunk if one is not provided + // This is because we want to respect the iteratore callback from the _writev method + if (!callback) callback = temp.callback; + } + + // Ensure this request has not been completed yet + if (!this.completed) { + // If this response has not be marked as an active stream, mark it as one and bind a 'finish' event handler to send response once a piped stream has completed + if (!this._streaming) { + this._streaming = true; + this.once('finish', () => this.send()); + } + + // Attempt to write the chunk to the client with backpressure handling + this._stream_chunk(chunk).then(callback).catch(callback); + } else { + // Trigger callback to flush the chunk as the response has already been completed + callback(); + } + } + + /** + * Writes multiples chunks for the response to the client over uWS with backpressure handling if a callback is provided. + * + * @private + * @param {Array} chunks + * @param {Function} callback + * @param {number} index + */ + _writev(chunks, callback, index = 0) { + // Serve the chunk at the current index + this._write(chunks[index], null, (error) => { + // Pass the error to the callback if one was provided + if (error) return callback(error); + + // Trigger the specific callback for the chunk we just served if it was in object format + if (typeof chunks[index].callback == 'function') chunks[index].callback(); + + // Determine if we have more chunks after the chunk we just served + if (index < chunks.length - 1) { + // Recursively serve the remaining chunks + this._writev(chunks, callback, index + 1); + } else { + // Trigger the callback as all chunks have been served + callback(); + } + }); + } + + /** + * This method is used to end the current request and send response with specified body and headers. + * + * @param {String|Buffer|ArrayBuffer=} body Optional + * @param {Boolean=} close_connection + * @returns {Response} + */ + send(body, close_connection) { + // Ensure response connection is still active + if (!this.completed) { + // If this request has a writable stream with some data in it, we must schedule this send() as the last chunk after which the stream will be flushed + if (this._writable && this._writable.writableLength) { + // If we have some body data, queue it as the last chunk of the body to be written + if (body) this._writable.write(body); + + // Mark the writable stream as ended + this._writable.end(); + + // Return this to make the Router chainable + return this; + } + + // If the response has not been corked yet, cork it and wait for the next tick to send the response + if (this._cork && !this._corked) { + this._corked = true; + return this.atomic(() => this.send(body, close_connection)); + } + + // Initiate the response to begin writing the status code and headers + this._initiate_response(); + + // Determine if the request still has not fully received the whole request body yet + if (!this._wrapped_request.received) { + // Instruct the request to stop accepting any more data as a response is being sent + this._wrapped_request._body_parser_stop(); + + // Wait for the request to fully receive the whole request body before sending the response + return this._wrapped_request.once('received', () => + // Because 'received' will be emitted asynchronously, we need to cork the response to ensure the response is sent in the correct order + this.atomic(() => this.send(body, close_connection)), + ); + } + + // If we have no body and are not streaming and have a custom content-length header, we need to send a response without a body with the custom content-length header + const custom_length = this._headers['content-length']; + if (!(body !== undefined || this._streaming || !custom_length)) { + // We can only use one of the content-lengths, so we will use the last one if there are multiple + const content_length = + typeof custom_length == 'string' ? custom_length : custom_length[custom_length.length - 1]; + + // Send the response with the uWS.HttpResponse.endWithoutBody() method as we have no body data + // NOTE: This method is completely undocumented by uWS but exists in the source code to solve the problem of no body being sent with a custom content-length + this._raw_response.endWithoutBody(content_length, close_connection); + } else { + // Send the response with the uWS.HttpResponse.end(body, close_connection) method as we have some body data + this._raw_response.end(body, close_connection); + } + + // Emit the 'finish' event to signify that the response has been sent without streaming + if (this._writable && !this._streaming) this.emit('finish', this._wrapped_request, this); + + // Mark request as completed as it has been sent + this.completed = true; + + // Decrement the pending request count + this.route.app._resolve_pending_request(); + + // Emit the 'close' event to signify that the response has been completed + if (this._writable) this.emit('close', this._wrapped_request, this); + } + + // Make chainable + return this; + } + + /** + * Writes a given chunk to the client over uWS with the appropriate writing method. + * Note! This method uses `uWS.tryEnd()` when a `total_size` is provided. + * Note! This method uses `uWS.write()` when a `total_size` is not provided. + * + * @private + * @param {Buffer} chunk + * @param {Number=} total_size + * @returns {Array} [sent, finished] + */ + _uws_write_chunk(chunk, total_size) { + // The specific uWS method to stream the chunk to the client differs depending on if we have a total_size or not + let sent, finished; + if (total_size) { + // Attempt to stream the current chunk using uWS.tryEnd with a total size + const [ok, done] = this._raw_response.tryEnd(chunk, total_size); + sent = ok; + finished = done; + } else { + // Attempt to stream the current chunk uWS.write() + sent = this._raw_response.write(chunk); + + // Since we are streaming without a total size, we are not finished + finished = false; + } + + // Return the sent and finished booleans + return [sent, finished]; + } + + /** + * Stream an individual chunk to the client with backpressure handling. + * Delivers with chunked transfer and without content-length header when no total_size is specified. + * Delivers with chunk writes and content-length header when a total_size is specified. + * Calls the `callback` once the chunk has been fully sent to the client. + * + * @private + * @param {Buffer} chunk + * @param {Number=} total_size + * @returns {Promise} + */ + _stream_chunk(chunk, total_size) { + // If the request has already been completed, we can resolve the promise immediately as we cannot write to the client anymore + if (this.completed) return Promise.resolve(); + + // Return a Promise which resolves once the chunk has been fully sent to the client + return new Promise((resolve) => + this.atomic(() => { + // Ensure the client is still connected after the cork + if (this.completed) return resolve(); + + // Initiate the response to ensure status code & headers get written first if they have not been written yet + this._initiate_response(); + + // Remember the initial write offset for future backpressure sliced chunks + // Write the chunk to the client using the appropriate uWS chunk writing method + const write_offset = this.write_offset; + const [sent] = this._uws_write_chunk(chunk, total_size); + if (sent) { + // The chunk was fully sent, we can resolve the promise + resolve(); + } else { + // Bind a drain handler to relieve backpressure + // Note! This callback may be called as many times as neccessary to send a full chunk when using the tryEnd method + this.drain((offset) => { + // Check if the response has been completed / connection has been closed since we can no longer write to the client + // When total_size is not provided, the chunk has been fully sent already via uWS.write() + // Only when total_size is provided we need to retry to send the ramining chunk since we have used uWS.tryEnd() and + // that does not guarantee that the whole chunk has been sent when stream is drained + if (this.completed || !total_size) { + resolve(); + return true; + } + + // Attempt to write the remaining chunk to the client + const remaining = chunk.slice(offset - write_offset); + const [flushed] = this._uws_write_chunk(remaining, total_size); + if (flushed) resolve(); + + // Return the flushed boolean as not flushed means we are still waiting for more drain events from uWS + return flushed; + }); + } + }), + ); + } + + /** + * This method is used to serve a readable stream as response body and send response. + * By default, this method will use chunked encoding transfer to stream data. + * If your use-case requires a content-length header, you must specify the total payload size. + * + * @param {stream.Readable} readable A Readable stream which will be consumed as response body + * @param {Number=} total_size Total size of the Readable stream source in bytes (Optional) + * @returns {Promise} a Promise which resolves once the stream has been fully consumed and response has been sent + */ + async stream(readable, total_size) { + // Ensure readable is an instance of a stream.Readable + if (!(readable instanceof stream.Readable)) + this.throw( + new Error('HyperExpress: Response.stream(readable, total_size) -> readable must be a Readable stream.'), + ); + + // Do not allow streaming if response has already been aborted or completed + if (!this.completed) { + // Bind an 'close' event handler which will destroy the consumed stream if request is closed + this.once('close', () => (!readable.destroyed ? readable.destroy() : null)); + + // Define a while loop to consume chunks from the readable stream until it is fully consumed or the response has been completed + while (!this.completed && !(readable.readableEnded || readable.destroyed)) { + // Attempt to read a chunk from the readable stream + let chunk = readable.read(); + if (!chunk) { + // Wait for the readable stream to emit a 'readable' event if no chunk was available in our initial read attempt + await new Promise((resolve) => { + // Bind a 'end' handler in case the readable stream ends before emitting a 'readable' event + readable.once('end', resolve); + + // Bind a 'readable' handler to resolve the promise once a chunk is available to read + readable.once('readable', () => { + // Unbind the 'end' handler as we have a chunk available to read + readable.removeListener('end', resolve); + + // Resolve the promise to continue the while loop + resolve(); + }); + }); + + // Attempt to read a chunk from the readable stream again + chunk = readable.read(); + } + + // Stream the chunk to the client + if (chunk) await this._stream_chunk(chunk, total_size); + } + + // If we had no total size and the response is still not completed, we need to end the response + // This is because no total size means we served with chunked encoding and we need to end the response as it is a unbounded stream + if (!this.completed) { + // Determine if we have a total size or not + if (total_size) { + // We must call the decrement method as a conventional close would not be detected + this.route.app._resolve_pending_request(); + } else { + // We must manually close the response as this stream operation is unbounded + this.send(); + } + } + } + } + + /** + * Instantly aborts/closes current request without writing a status response code. + * Use this to instantly abort a request where a proper response with an HTTP status code is not neccessary. + */ + close() { + // Ensure request has already not been completed + if (!this.completed) { + // Mark request as completed + this.completed = true; + + // Decrement the pending request count + this.route.app._resolve_pending_request(); + + // Stop the body parser from accepting any more data + this._wrapped_request._body_parser_stop(); + + // Resume the request in case it was paused + this._wrapped_request.resume(); + + // Close the underlying uWS request + this._raw_response.close(); + } + } + + /** + * This method is used to redirect an incoming request to a different url. + * + * @param {String} url Redirect URL + * @returns {Boolean} Boolean + */ + redirect(url) { + if (!this.completed) return this.status(302).header('location', url).send(); + return false; + } + + /** + * This method is an alias of send() method except it accepts an object and automatically stringifies the passed payload object. + * + * @param {Object} body JSON body + * @returns {Boolean} Boolean + */ + json(body) { + return this.header('content-type', 'application/json', true).send(JSON.stringify(body)); + } + + /** + * This method is an alias of send() method except it accepts an object + * and automatically stringifies the passed payload object with a callback name. + * Note! This method uses 'callback' query parameter by default but you can specify 'name' to use something else. + * + * @param {Object} body + * @param {String=} name + * @returns {Boolean} Boolean + */ + jsonp(body, name) { + let query_parameters = this._wrapped_request.query_parameters; + let method_name = query_parameters['callback'] || name; + return this.header('content-type', 'application/javascript', true).send( + `${method_name}(${JSON.stringify(body)})`, + ); + } + + /** + * This method is an alias of send() method except it automatically sets + * html as the response content type and sends provided html response body. + * + * @param {String} body + * @returns {Boolean} Boolean + */ + html(body) { + return this.header('content-type', 'text/html', true).send(body); + } + + /** + * @private + * Sends file content with appropriate content-type header based on file extension from LiveFile. + * + * @param {LiveFile} live_file + * @param {function(Object):void} callback + */ + async _send_file(live_file, callback) { + // Wait for LiveFile to be ready before serving + if (!live_file.is_ready) await live_file.ready(); + + // Write appropriate extension type if one has not been written yet + this.type(live_file.extension); + + // Send response with file buffer as body + this.send(live_file.buffer); + + // Execute callback with cache pool, so user can expire as they wish. + if (callback) setImmediate(() => callback(FilePool)); + } + + /** + * This method is an alias of send() method except it sends the file at specified path. + * This method automatically writes the appropriate content-type header if one has not been specified yet. + * This method also maintains its own cache pool in memory allowing for fast performance. + * Avoid using this method to a send a large file as it will be kept in memory. + * + * @param {String} path + * @param {function(Object):void=} callback Executed after file has been served with the parameter being the cache pool. + */ + file(path, callback) { + // Send file from local cache pool if available + if (FilePool[path]) return this._send_file(FilePool[path], callback); + + // Create new LiveFile instance in local cache pool for new file path + FilePool[path] = new LiveFile({ + path, + }); + + // Assign error handler to live file + FilePool[path].on('error', (error) => this.throw(error)); + + // Serve file as response + this._send_file(FilePool[path], callback); + } + + /** + * Writes approriate headers to signify that file at path has been attached. + * + * @param {String} path + * @param {String=} name + * @returns {Response} + */ + attachment(path, name) { + // Attach a blank content-disposition header when no filename is defined + if (path == undefined) return this.header('Content-Disposition', 'attachment'); + + // Parses path in to file name and extension to write appropriate attachment headers + let chunks = path.split('/'); + let final_name = name || chunks[chunks.length - 1]; + let name_chunks = final_name.split('.'); + let extension = name_chunks[name_chunks.length - 1]; + return this.header('content-disposition', `attachment; filename="${final_name}"`).type(extension); + } + + /** + * Writes appropriate attachment headers and sends file content for download on user browser. + * This method combined Response.attachment() and Response.file() under the hood, so be sure to follow the same guidelines for usage. + * + * @param {String} path + * @param {String=} filename + */ + download(path, filename) { + return this.attachment(path, filename).file(path); + } + + #thrown = false; + /** + * This method allows you to throw an error which will be caught by the global error handler. + * + * @param {Error} error + * @returns {Response} + */ + throw(error) { + // If we have already thrown an error, ignore further throws + if (this.#thrown) return this; + this.#thrown = true; + + // If the error is not an instance of Error, wrap it in an Error object that + if (!(error instanceof Error)) error = new Error(`ERR_CAUGHT_NON_ERROR_TYPE: ${error}`); + + // Trigger the global error handler + this.route.app.handlers.on_error(this._wrapped_request, this, error); + + // Return this response instance + return this; + } + + /* HyperExpress Properties */ + + /** + * Returns the request locals for this request. + * @returns {Object.} + */ + get locals() { + // Initialize locals object if it does not exist + if (!this._locals) this._locals = {}; + return this._locals; + } + + /** + * Returns the underlying raw uWS.Response object. + * Note! Utilizing any of uWS.Response's methods after response has been sent will result in an invalid discarded access error. + * @returns {import('uWebSockets.js').Response} + */ + get raw() { + return this._raw_response; + } + + /** + * Returns the HyperExpress.Server instance this Response object originated from. + * + * @returns {import('../Server.js')} + */ + get app() { + return this.route.app; + } + + /** + * Returns current state of request in regards to whether the source is still connected. + * @returns {Boolean} + */ + get aborted() { + return this.completed; + } + + /** + * Upgrade socket context for upgrade requests. + * @returns {import('uWebSockets.js').ux_socket_context} + */ + get upgrade_socket() { + return this._upgrade_socket; + } + + /** + * Returns a "Server-Sent Events" connection object to allow for SSE functionality. + * This property will only be available for GET requests as per the SSE specification. + * + * @returns {SSEventStream=} + */ + get sse() { + // Return a new SSE instance if one has not been created yet + if (this._wrapped_request.method === 'GET') { + // Initialize the SSE instance if one has not been created yet + if (this._sse === undefined) { + this._sse = new SSEventStream(); + + // Provide the response object to the SSE instance + this._sse._response = this; + } + + // Return the SSE instance + return this._sse; + } + } + + /** + * Returns the current response body content write offset in bytes. + * Use in conjunction with the drain() offset handler to retry writing failed chunks. + * Note! This method will return `-1` after the Response has been completed and the connection has been closed. + * @returns {Number} + */ + get write_offset() { + return this.completed ? -1 : this._raw_response.getWriteOffset(); + } + + /** + * Throws a descriptive error when an unsupported ExpressJS property/method is invocated. + * @private + * @param {String} name + */ + _throw_unsupported(name) { + throw new Error( + `ERR_INCOMPATIBLE_CALL: One of your middlewares or route logic tried to call Response.${name} which is unsupported with HyperExpress.`, + ); + } + + text(data) { + this.type('text'); + this.send(data); + return this; + } + + notFound() { + this.status(404); + return this; + } +} + +// Store the descriptors of the original HyperExpress.Response class +const descriptors = Object.getOwnPropertyDescriptors(Response.prototype); + +// Inherit the compatibility classes +inherit_prototype({ + from: [NodeResponse.prototype, ExpressResponse.prototype], + to: Response.prototype, + method: (type, name, original) => { + // Initialize a passthrough method for each descriptor + const passthrough = function () { + // Call the original function with the Request scope + return original.apply(this, arguments); + }; + + // Return the passthrough function + return passthrough; + }, +}); + +// Inherit the stream.Writable prototype and lazy initialize the stream on first call to any inherited method +inherit_prototype({ + from: stream.Writable.prototype, + to: Response.prototype, + override: (name) => '_super_' + name, // Prefix all overrides with _super_ + method: (type, name, original) => { + // Initialize a pass through method + const passthrough = function () { + // Lazy initialize the writable stream on local scope + if (this._writable === null) { + // Initialize the writable stream + this._writable = new stream.Writable(this.route.streaming.writable); + + // Bind the natively implemented _write and _writev methods + // Ensure the Response scope is passed to these methods + this._writable._write = descriptors['_write'].value.bind(this); + this._writable._writev = descriptors['_writev'].value.bind(this); + } + + // Return the original function with the writable stream as the context + return original.apply(this._writable, arguments); + }; + + // Otherwise, simply return the passthrough method + return passthrough; + }, +}); + +module.exports = Response; diff --git a/packages/hyper-express/src/components/plugins/HostManager.js b/packages/hyper-express/src/components/plugins/HostManager.js new file mode 100644 index 0000000..9069381 --- /dev/null +++ b/packages/hyper-express/src/components/plugins/HostManager.js @@ -0,0 +1,74 @@ +'use strict'; +const EventEmitter = require('events'); + +class HostManager extends EventEmitter { + #app; + #hosts = {}; + + constructor(app) { + // Initialize event emitter + super(); + + // Store app reference + this.#app = app; + + // Bind a listener which emits 'missing' events from uWS when a host is not found + this.#app.uws_instance.missingServerName((hostname) => this.emit('missing', hostname)); + } + + /** + * @typedef {Object} HostOptions + * @property {String=} passphrase Strong passphrase for SSL cryptographic purposes. + * @property {String=} cert_file_name Path to SSL certificate file to be used for SSL/TLS. + * @property {String=} key_file_name Path to SSL private key file to be used for SSL/TLS. + * @property {String=} dh_params_file_name Path to file containing Diffie-Hellman parameters. + * @property {Boolean=} ssl_prefer_low_memory_usage Whether to prefer low memory usage over high performance. + */ + + /** + * Registers the unique host options to use for the specified hostname for incoming requests. + * + * @param {String} hostname + * @param {HostOptions} options + * @returns {HostManager} + */ + add(hostname, options) { + // Store host options + this.#hosts[hostname] = options; + + // Register the host server with uWS + this.#app.uws_instance.addServerName(hostname, options); + + // Return this instance + return this; + } + + /** + * Un-Registers the unique host options to use for the specified hostname for incoming requests. + * + * @param {String} hostname + * @returns {HostManager} + */ + remove(hostname) { + // Remove host options + delete this.#hosts[hostname]; + + // Un-Register the host server with uWS + this.#app.uws_instance.removeServerName(hostname); + + // Return this instance + return this; + } + + /* HostManager Getters & Properties */ + + /** + * Returns all of the registered hostname options. + * @returns {Object.} + */ + get registered() { + return this.#hosts; + } +} + +module.exports = HostManager; diff --git a/packages/hyper-express/src/components/plugins/LiveFile.js b/packages/hyper-express/src/components/plugins/LiveFile.js new file mode 100644 index 0000000..56a4c0a --- /dev/null +++ b/packages/hyper-express/src/components/plugins/LiveFile.js @@ -0,0 +1,172 @@ +'use strict'; +const FileSystem = require('fs'); +const EventEmitter = require('events'); +const { wrap_object, async_wait } = require('../../shared/operators.js'); + +class LiveFile extends EventEmitter { + #name; + #watcher; + #extension; + #buffer; + #content; + #last_update; + #options = { + path: '', + retry: { + every: 300, + max: 3, + }, + }; + + constructor(options) { + // Initialize EventEmitter instance + super(); + + // Wrap options object with provided object + wrap_object(this.#options, options); + + // Determine the name of the file + const chunks = options.path.split('/'); + this.#name = chunks[chunks.length - 1]; + + // Determine the extension of the file + this.#extension = this.#options.path.split('.'); + this.#extension = this.#extension[this.#extension.length - 1]; + + // Initialize file watcher to keep file updated in memory + this.reload(); + this._initiate_watcher(); + } + + /** + * @private + * Initializes File Watcher to reload file on changes + */ + _initiate_watcher() { + // Create FileWatcher that trigger reload method + this.#watcher = FileSystem.watch(this.#options.path, () => this.reload()); + } + + #reload_promise; + #reload_resolve; + #reload_reject; + + /** + * Reloads buffer/content for file asynchronously with retry policy. + * + * @private + * @param {Boolean} fresh + * @param {Number} count + * @returns {Promise} + */ + reload(fresh = true, count = 0) { + const reference = this; + if (fresh) { + // Reuse promise if there if one pending + if (this.#reload_promise instanceof Promise) return this.#reload_promise; + + // Create a new promise for fresh lookups + this.#reload_promise = new Promise((resolve, reject) => { + reference.#reload_resolve = resolve; + reference.#reload_reject = reject; + }); + } + + // Perform filesystem lookup query + FileSystem.readFile(this.#options.path, async (error, buffer) => { + // Pipe filesystem error through promise + if (error) { + reference._flush_ready(); + return reference.#reload_reject(error); + } + + // Perform retries in accordance with retry policy + // This is to prevent empty reads on atomicity based modifications from third-party programs + const { every, max } = reference.#options.retry; + if (buffer.length == 0 && count < max) { + await async_wait(every); + return reference.reload(false, count + 1); + } + + // Update instance buffer/content/last_update variables + reference.#buffer = buffer; + reference.#content = buffer.toString(); + reference.#last_update = Date.now(); + + // Cleanup reload promises and methods + reference.#reload_resolve(); + reference._flush_ready(); + reference.#reload_resolve = null; + reference.#reload_reject = null; + reference.#reload_promise = null; + }); + + return this.#reload_promise; + } + + #ready_promise; + #ready_resolve; + + /** + * Flushes pending ready promise. + * @private + */ + _flush_ready() { + if (typeof this.#ready_resolve == 'function') { + this.#ready_resolve(); + this.#ready_resolve = null; + } + this.#ready_promise = true; + } + + /** + * Returns a promise which resolves once first reload is complete. + * + * @returns {Promise} + */ + ready() { + // Return true if no ready promise exists + if (this.#ready_promise === true) return Promise.resolve(); + + // Create a Promise if one does not exist for ready event + if (this.#ready_promise === undefined) + this.#ready_promise = new Promise((resolve) => (this.#ready_resolve = resolve)); + + return this.#ready_promise; + } + + /* LiveFile Getters */ + get is_ready() { + return this.#ready_promise === true; + } + + get name() { + return this.#name; + } + + get path() { + return this.#options.path; + } + + get extension() { + return this.#extension; + } + + get content() { + return this.#content; + } + + get buffer() { + return this.#buffer; + } + + get last_update() { + return this.#last_update; + } + + get watcher() { + return this.#watcher; + } +} + +module.exports = LiveFile; diff --git a/packages/hyper-express/src/components/plugins/MultipartField.js b/packages/hyper-express/src/components/plugins/MultipartField.js new file mode 100644 index 0000000..13c05f1 --- /dev/null +++ b/packages/hyper-express/src/components/plugins/MultipartField.js @@ -0,0 +1,132 @@ +'use strict'; +const stream = require('stream'); +const FileSystem = require('fs'); + +class MultipartField { + #name; + #encoding; + #mime_type; + #file; + #value; + #truncated; + + constructor(name, value, info) { + // Store general information about this field + this.#name = name; + this.#encoding = info.encoding; + this.#mime_type = info.mimeType; + + // Determine if this field is a file or a normal field + if (value instanceof stream.Readable) { + // Store this file's supplied name and data stream + this.#file = { + name: info.filename, + stream: value, + }; + } else { + // Store field value and truncation information + this.#value = value; + this.#truncated = { + name: info.nameTruncated, + value: info.valueTruncated, + }; + } + } + + /* MultipartField Methods */ + + /** + * Saves this multipart file content to the specified path. + * Note! You must specify the file name and extension in the path itself. + * + * @param {String} path Path with file name to which you would like to save this file. + * @param {stream.WritableOptions} options Writable stream options + * @returns {Promise} + */ + write(path, options) { + // Throw an error if this method is called on a non file field + if (this.file === undefined) + throw new Error( + 'HyperExpress.Request.MultipartField.write(path, options) -> This method can only be called on a field that is a file type.' + ); + + // Return a promise which resolves once write stream has finished + const reference = this; + return new Promise((resolve, reject) => { + const writable = FileSystem.createWriteStream(path, options); + writable.on('close', resolve); + writable.on('error', reject); + reference.file.stream.pipe(writable); + }); + } + + /* MultipartField Properties */ + + /** + * Field name as specified in the multipart form. + * @returns {String} + */ + get name() { + return this.#name; + } + + /** + * Field encoding as specified in the multipart form. + * @returns {String} + */ + get encoding() { + return this.#encoding; + } + + /** + * Field mime type as specified in the multipart form. + * @returns {String} + */ + get mime_type() { + return this.#mime_type; + } + + /** + * @typedef {Object} MultipartFile + * @property {String=} name If supplied, this file's name as supplied by sender. + * @property {stream.Readable} stream Readable stream to consume this file's data. + */ + + /** + * Returns file information about this field if it is a file type. + * Note! This property will ONLY be defined if this field is a file type. + * + * @returns {MultipartFile} + */ + get file() { + return this.#file; + } + + /** + * Returns field value if this field is a non-file type. + * Note! This property will ONLY be defined if this field is a non-file type. + * + * @returns {String} + */ + get value() { + return this.#value; + } + + /** + * @typedef {Object} Truncations + * @property {Boolean} name Whether this field's name was truncated. + * @property {Boolean} value Whether this field's value was truncated. + */ + + /** + * Returns information about truncations in this field. + * Note! This property will ONLY be defined if this field is a non-file type. + * + * @returns {Truncations} + */ + get truncated() { + return this.#truncated; + } +} + +module.exports = MultipartField; diff --git a/packages/hyper-express/src/components/plugins/SSEventStream.js b/packages/hyper-express/src/components/plugins/SSEventStream.js new file mode 100644 index 0000000..d2b1bc4 --- /dev/null +++ b/packages/hyper-express/src/components/plugins/SSEventStream.js @@ -0,0 +1,116 @@ +'use strict'; +class SSEventStream { + _response; + + #wrote_headers = false; + /** + * @private + * Ensures the proper SSE headers are written to the client to initiate the SSE stream. + * @returns {Boolean} Whether the headers were written + */ + _initiate_sse_stream() { + // If the response has already been initiated, we cannot write headers anymore + if (this._response.initiated) return false; + + // If we have already written headers, we cannot write again + if (this.#wrote_headers) return false; + this.#wrote_headers = true; + + // Write the headers for the SSE stream to the client + this._response + .header('content-type', 'text/event-stream') + .header('cache-control', 'no-cache') + .header('connection', 'keep-alive') + .header('x-accel-buffering', 'no'); + + // Return true to signify that we have written headers + return true; + } + + /** + * @private + * Internal method to write data to the response stream. + * @returns {Boolean} Whether the data was written + */ + _write(data) { + // Initialize the SSE stream + this._initiate_sse_stream(); + + // Write the data to the response stream + return this._response.write(data); + } + + /** + * Opens the "Server-Sent Events" connection to the client. + * + * @returns {Boolean} + */ + open() { + // We simply send a comment-type message to the client to indicate that the connection has been established + // The "data" can be anything as it will not be handled by the client EventSource object + return this.comment('open'); + } + + /** + * Closes the "Server-Sent Events" connection to the client. + * + * @returns {Boolean} + */ + close() { + // Ends the connection by sending the final empty message + return this._response.send(); + } + + /** + * Sends a comment-type message to the client that will not be emitted by EventSource. + * This can be useful as a keep-alive mechanism if messages might not be sent regularly. + * + * @param {String} data + * @returns {Boolean} + */ + comment(data) { + // Prefix the message with a colon character to signify a comment + return this._write(`: ${data}\n`); + } + + /** + * Sends a message to the client based on the specified event and data. + * Note! You must retry failed messages if you receive a false output from this method. + * + * @param {String} id + * @param {String=} event + * @param {String=} data + * @returns {Boolean} + */ + send(id, event, data) { + // Parse arguments into overloaded parameter translations + const _id = id && event && data ? id : undefined; + const _event = id && event ? (_id ? event : id) : undefined; + const _data = data || event || id; + + // Build message parts to prepare a payload + const parts = []; + if (_id) parts.push(`id: ${_id}`); + if (_event) parts.push(`event: ${_event}`); + if (_data) parts.push(`data: ${_data}`); + + // Push an empty line to indicate the end of the message + parts.push('', ''); + + // Write the string based payload to the client + return this._write(parts.join('\n')); + } + + /* SSEConnection Properties */ + + /** + * Whether this Server-Sent Events stream is still active. + * + * @returns {Boolean} + */ + get active() { + return !this._response.completed; + } +} + +module.exports = SSEventStream; diff --git a/packages/hyper-express/src/components/router/Route.js b/packages/hyper-express/src/components/router/Route.js new file mode 100644 index 0000000..1c77cb2 --- /dev/null +++ b/packages/hyper-express/src/components/router/Route.js @@ -0,0 +1,210 @@ +'use strict'; +const { parse_path_parameters } = require('../../shared/operators.js'); + +class Route { + id = null; + app = null; + path = ''; + method = ''; + pattern = ''; + handler = null; + options = null; + streaming = null; + max_body_length = null; + path_parameters_key = null; + + /** + * Constructs a new Route object. + * @param {Object} options + * @param {import('../Server.js')} options.app - The server instance. + * @param {String} options.method - The HTTP method. + * @param {String} options.pattern - The route pattern. + * @param {Function} options.handler - The route handler. + */ + constructor({ app, method, pattern, options, handler }) { + this.id = app._get_incremented_id(); + this.app = app; + this.pattern = pattern; + this.handler = handler; + this.options = options; + this.method = method.toUpperCase(); + this.streaming = options.streaming || app._options.streaming || {}; + this.max_body_length = options.max_body_length || app._options.max_body_length; + this.path_parameters_key = parse_path_parameters(pattern); + + // Translate to HTTP DELETE + if (this.method === 'DEL') this.method = 'DELETE'; + + // Cache the expected request path for this route if it is not a wildcard route + // This will be used to optimize performance for determining incoming request paths + const wildcard = pattern.includes('*') || this.path_parameters_key.length > 0; + if (!wildcard) this.path = pattern; + } + + /** + * @typedef {Object} Middleware + * @property {Number} id - Unique identifier for this middleware based on it's registeration order. + * @property {String} pattern - The middleware pattern. + * @property {function} handler - The middleware handler function. + * @property {Boolean=} match - Whether to match the middleware pattern against the request path. + */ + + /** + * Binds middleware to this route and sorts middlewares to ensure execution order. + * + * @param {Middleware} middleware + */ + use(middleware) { + // Store and sort middlewares to ensure proper execution order + this.options.middlewares.push(middleware); + } + + /** + * Handles an incoming request through this route. + * + * @param {import('../http/Request.js')} request The HyperExpress request object. + * @param {import('../http/Response.js')} response The HyperExpress response object. + * @param {Number=} cursor The middleware cursor. + */ + handle(request, response, cursor = 0) { + // Do not handle the request if the response has been sent aka. the request is no longer active + if (response.completed) return; + + // Retrieve the middleware for the current cursor, track the cursor if there is a valid middleware + let iterator; + const middleware = this.options.middlewares[cursor]; + if (middleware) { + // Determine if this middleware requires path matching + if (middleware.match) { + // Check if the middleware pattern matches that starting of the request path + if (request.path.startsWith(middleware.pattern)) { + // Ensure that the character after the middleware pattern is either a trailing slash or out of bounds of string + const trailing = request.path[middleware.pattern.length]; + if (trailing !== '/' && trailing !== undefined) { + // This handles cases where "/docs" middleware will incorrectly match "/docs-JSON" for example + return this.handle(request, response, cursor + 1); + } + } else { + // Since the middleware pattern does not match the start of the request path, skip this middleware + return this.handle(request, response, cursor + 1); + } + } + + // Track the middleware cursor to prevent double execution + response._track_middleware_cursor(cursor); + + // Initialize the iterator for this middleware + iterator = (error) => { + // If an error occured, pipe it to the error handler + if (error instanceof Error) return response.throw(error); + + // Handle this request again with an incremented cursor to execute the next middleware or route handler + this.handle(request, response, cursor + 1); + }; + } + + // Determine if this is an async handler which can explicitly throw uncaught errors + const is_async_handler = (middleware ? middleware.handler : this.handler).constructor.name === 'AsyncFunction'; + if (is_async_handler) { + // Execute the middleware or route handler within a promise to catch and pipe synchronous errors + new Promise(async (resolve) => { + try { + if (middleware) { + // Execute the middleware or route handler with the iterator + await middleware.handler(request, response, iterator); + + // Call the iterator anyways in case the middleware never calls the next() iterator + iterator(); + } else { + await this.handler(request, response); + } + } catch (error) { + // Catch and pipe any errors to the error handler + response.throw(error); + } + + // Resolve promise to ensure it is properly cleaned up from memory + resolve(); + }); + } else { + // Execute the middleware or route handler within a protected try/catch to catch and pipe synchronous errors + try { + let output; + if (middleware) { + output = middleware.handler(request, response, iterator); + } else { + output = this.handler(request, response); + } + + // Determine if a Promise instance was returned by the handler + if (typeof output?.then === 'function') { + // If this is a middleware, we must try to call iterator after returned promise resolves + if (middleware) output.then(iterator); + + // Catch and pipe any errors to the global error handler + output.catch((error) => response.throw(error)); + } + } catch (error) { + // Catch and pipe any errors to the error handler + response.throw(error); + } + } + } + + /** + * Compiles the route's internal components and caches for incoming requests. + */ + compile() { + // Initialize a fresh array of middlewares + const middlewares = []; + const pattern = this.pattern; + + // Determine wildcard properties about this route + const is_wildcard = pattern.endsWith('*'); + const wildcard_path = pattern.substring(0, pattern.length - 1); + + // Iterate through the global/local middlewares and connect them to this route if eligible + const app_middlewares = this.app.middlewares; + Object.keys(app_middlewares).forEach((pattern) => + app_middlewares[pattern].forEach((middleware) => { + // A route can be a direct child when a route's pattern has more path depth than the middleware with a matching start + // A route can be an indirect child when it is a wildcard and the middleware's pattern is a direct parent of the route child + const direct_child = pattern.startsWith(middleware.pattern); + const indirect_child = middleware.pattern.startsWith(wildcard_path); + if (direct_child || (is_wildcard && indirect_child)) { + // Create shallow copy of the middleware + const record = Object.assign({}, middleware); + + // Set the match property based on whether this is a direct child + record.match = direct_child; + + // Push the middleware + middlewares.push(record); + } + }) + ); + + // Find the largest ID from the current middlewares + const offset = middlewares.reduce((max, middleware) => (middleware.id > max ? middleware.id : max), 0); + + // Push the route-specific middlewares to the array at the end + if (Array.isArray(this.options.middlewares)) + this.options.middlewares.forEach((middleware) => + middlewares.push({ + id: this.id + offset, + pattern, + handler: middleware, + match: false, // Route-specific middlewares do not need to be matched + }) + ); + + // Sort the middlewares by their id in ascending order + // This will ensure that middlewares are executed in the order they were registered throughout the application + middlewares.sort((a, b) => a.id - b.id); + + // Write the middlewares property with the sorted array + this.options.middlewares = middlewares; + } +} + +module.exports = Route; diff --git a/packages/hyper-express/src/components/router/Router.js b/packages/hyper-express/src/components/router/Router.js new file mode 100644 index 0000000..30ee49a --- /dev/null +++ b/packages/hyper-express/src/components/router/Router.js @@ -0,0 +1,483 @@ +'use strict'; +const { merge_relative_paths } = require('../../shared/operators.js'); + +/** + * @typedef {import('../compatibility/NodeRequest.js')} NodeRequest + * @typedef {import('../compatibility/NodeResponse.js').NodeResponseTypes} NodeResponse + * @typedef {import('../compatibility/ExpressRequest.js')} ExpressRequest + * @typedef {import('../compatibility/ExpressResponse.js')} ExpressResponse + * @typedef {import('../http/Request.js')} NativeRequest + * @typedef {import('../http/Response.js')} NativeResponse + * @typedef {NativeRequest & NodeRequest & ExpressRequest & import('stream').Stream} Request + * @typedef {NativeResponse & NodeResponse & ExpressResponse & import('stream').Stream} Response + * @typedef {function(Request, Response, Function):any|Promise} MiddlewareHandler + */ + +class Router { + #is_app = false; + #context_pattern; + #subscribers = []; + #records = { + routes: [], + middlewares: [], + }; + + constructor() {} + + /** + * Used by the server to declare self as an app instance. + * + * @private + * @param {Boolean} value + */ + _is_app(value) { + this.#is_app = value; + } + + /** + * Sets context pattern for this router which will auto define the pattern of each route called on this router. + * This is called by the .route() returned Router instance which allows for omission of pattern to be passed to route() method. + * Example: Router.route('/something/else').get().post().delete() etc will all be bound to pattern '/something/else' + * @private + * @param {string} path + */ + _set_context_pattern(path) { + this.#context_pattern = path; + } + + /** + * Registers a route in the routes array for this router. + * + * @private + * @param {String} method Supported: any, get, post, delete, head, options, patch, put, trace + * @param {String} pattern Example: "/api/v1" + * @param {Object} options Route processor options (Optional) + * @param {Function} handler Example: (request, response) => {} + * @returns {this} Chainable instance + */ + _register_route() { + // The first argument will always be the method (in lowercase) + const method = arguments[0]; + + // The pattern, options and handler must be dynamically parsed depending on the arguments provided and router behavior + let pattern, options, handler; + + // Iterate through the remaining arguments to find the above values and also build an Array of middleware / handler callbacks + // The route handler will be the last one in the array + const callbacks = []; + for (let i = 1; i < arguments.length; i++) { + const argument = arguments[i]; + + // The second argument should be the pattern. If it is a string, it is the pattern. If it is anything else and we do not have a context pattern, throw an error as that means we have no pattern. + if (i === 1) { + if (typeof argument === 'string') { + if (this.#context_pattern) { + // merge the provided pattern with the context pattern + pattern = merge_relative_paths(this.#context_pattern, argument); + } else { + // The path is as is + pattern = argument; + } + + // Continue to the next argument as this is not the pattern but we have a context pattern + continue; + } else if (!this.#context_pattern) { + throw new Error( + 'HyperExpress.Router: Route pattern is required unless created from a chainable route instance using Route.route() method.' + ); + } else { + // The path is the context pattern + pattern = this.#context_pattern; + } + } + + // Look for options, middlewares and handler in the remaining arguments + if (typeof argument == 'function') { + // Scenario: Single function + callbacks.push(argument); + } else if (Array.isArray(argument)) { + // Scenario: Array of functions + callbacks.push(...argument); + } else if (argument && typeof argument == 'object') { + // Scenario: Route options object + options = argument; + } + } + + // Write the route handler and route options object with fallback to the default options + handler = callbacks.pop(); + options = { + streaming: {}, + middlewares: [], + ...(options || {}), + }; + + // Make a shallow copy of the options object to avoid mutating the original + options = Object.assign({}, options); + + // Enforce a leading slash on the pattern if it begins with a catchall star + // This is because uWebsockets.js does not treat non-leading slashes as catchall stars + if (pattern.startsWith('*')) pattern = '/' + pattern; + + // Parse the middlewares into a new array to prevent mutating the original + const middlewares = []; + + // Push all the options provided middlewares into the middlewares array + if (Array.isArray(options.middlewares)) middlewares.push(...options.middlewares); + + // Push all the callback provided middlewares into the middlewares array + if (callbacks.length > 0) middlewares.push(...callbacks); + + // Write the middlewares into the options object + options.middlewares = middlewares; + + // Initialize the record object which will hold information about this route + const record = { + method, + pattern, + options, + handler, + }; + + // Store record for future subscribers + this.#records.routes.push(record); + + // Create route if this is a Server extended Router instance (ROOT) + if (this.#is_app) return this._create_route(record); + + // Alert all subscribers of the new route that was created + this.#subscribers.forEach((subscriber) => subscriber('route', record)); + + // Return this to make the Router chainable + return this; + } + + /** + * Registers a middleware from use() method and recalibrates. + * + * @private + * @param {String} pattern + * @param {Function} middleware + */ + _register_middleware(pattern, middleware) { + const record = { + pattern: pattern.endsWith('/') ? pattern.slice(0, -1) : pattern, // Do not allow trailing slash in middlewares + middleware, + }; + + // Store record for future subscribers + this.#records.middlewares.push(record); + + // Create middleware if this is a Server extended Router instance (ROOT) + if (this.#is_app) return this._create_middleware(record); + + // Alert all subscribers of the new middleware that was created + this.#subscribers.forEach((subscriber) => subscriber('middleware', record)); + } + + /** + * Registers a router from use() method and recalibrates. + * + * @private + * @param {String} pattern + * @param {this} router + */ + _register_router(pattern, router) { + const reference = this; + router._subscribe((event, object) => { + switch (event) { + case 'records': + // Destructure records from router + const { routes, middlewares } = object; + + // Register routes from router locally with adjusted pattern + routes.forEach((record) => + reference._register_route( + record.method, + merge_relative_paths(pattern, record.pattern), + record.options, + record.handler + ) + ); + + // Register middlewares from router locally with adjusted pattern + return middlewares.forEach((record) => + reference._register_middleware(merge_relative_paths(pattern, record.pattern), record.middleware) + ); + case 'route': + // Register route from router locally with adjusted pattern + return reference._register_route( + object.method, + merge_relative_paths(pattern, object.pattern), + object.options, + object.handler + ); + case 'middleware': + // Register middleware from router locally with adjusted pattern + return reference._register_middleware( + merge_relative_paths(pattern, object.patch), + object.middleware + ); + } + }); + } + + /* Router public methods */ + + /** + * Subscribes a handler which will be invocated with changes. + * + * @private + * @param {*} handler + */ + _subscribe(handler) { + // Pipe all records on first subscription to synchronize + handler('records', this.#records); + + // Register subscriber handler for future updates + this.#subscribers.push(handler); + } + + /** + * Registers middlewares and router instances on the specified pattern if specified. + * If no pattern is specified, the middleware/router instance will be mounted on the '/' root path by default of this instance. + * + * @param {...(String|MiddlewareHandler|Router)} args (request, response, next) => {} OR (request, response) => new Promise((resolve, reject) => {}) + * @returns {this} Chainable instance + */ + use() { + // If we have a context pattern, then this is a contextual Chainable and should not allow middlewares or routers to be bound to it + if (this.#context_pattern) + throw new Error( + 'HyperExpress.Router.use() -> Cannot bind middlewares or routers to a contextual router created using Router.route() method.' + ); + + // Parse a pattern for this use call with a fallback to the local-global scope aka. '/' pattern + const pattern = arguments[0] && typeof arguments[0] == 'string' ? arguments[0] : '/'; + + // Validate that the pattern value does not contain any wildcard or path parameter prefixes which are not allowed + if (pattern.indexOf('*') > -1 || pattern.indexOf(':') > -1) + throw new Error( + 'HyperExpress: Server/Router.use() -> Wildcard "*" & ":parameter" prefixed paths are not allowed when binding middlewares or routers using this method.' + ); + + // Register each candidate individually depending on the type of candidate value + for (let i = 0; i < arguments.length; i++) { + const candidate = arguments[i]; + if (typeof candidate == 'function') { + // Scenario: Single function + this._register_middleware(pattern, candidate); + } else if (Array.isArray(candidate)) { + // Scenario: Array of functions + candidate.forEach((middleware) => this._register_middleware(pattern, middleware)); + } else if (typeof candidate == 'object' && candidate.constructor.name === 'Router') { + // Scenario: Router instance + this._register_router(pattern, candidate); + } else if (candidate && typeof candidate == 'object' && typeof candidate.middleware == 'function') { + // Scenario: Inferred middleware for third-party middlewares which support the Middleware.middleware property + this._register_middleware(pattern, candidate.middleware); + } + } + + // Return this to make the Router chainable + return this; + } + + /** + * @typedef {Object} RouteOptions + * @property {Number} max_body_length Overrides the global maximum body length specified in Server constructor options. + * @property {Array.} middlewares Route specific middlewares + * @property {Object} streaming Global content streaming options. + * @property {import('stream').ReadableOptions} streaming.readable Global content streaming options for Readable streams. + * @property {import('stream').WritableOptions} streaming.writable Global content streaming options for Writable streams. + */ + + /** + * Returns a Chainable instance which can be used to bind multiple method routes or middlewares on the same path easily. + * Example: `Router.route('/api/v1').get(getHandler).post(postHandler).delete(destroyHandler)` + * Example: `Router.route('/api/v1').use(middleware).user(middleware2)` + * @param {String} pattern + * @returns {this} A Chainable instance with a context pattern set to this router's pattern. + */ + route(pattern) { + // Ensure that the pattern is a string + if (!pattern || typeof pattern !== 'string') + throw new Error('HyperExpress.Router.route(pattern) -> pattern must be a string.'); + + // Create a new router instance with the context pattern set to the provided pattern + const router = new Router(); + router._set_context_pattern(pattern); + this.use(router); + + // Return the router instance to allow for chainable bindings + return router; + } + + /** + * Creates an HTTP route that handles any HTTP method requests. + * Note! ANY routes do not support route specific middlewares. + * + * @param {String} pattern + * @param {...(RouteOptions|MiddlewareHandler)} args + */ + any() { + return this._register_route('any', ...arguments); + } + + /** + * Alias of any() method. + * Creates an HTTP route that handles any HTTP method requests. + * Note! ANY routes do not support route specific middlewares. + * + * @param {String} pattern + * @param {...(RouteOptions|MiddlewareHandler)} args + */ + all() { + // Alias of any() method + return this.any(...arguments); + } + + /** + * Creates an HTTP route that handles GET method requests. + * + * @param {String} pattern + * @param {...(RouteOptions|MiddlewareHandler)} args + */ + get() { + return this._register_route('get', ...arguments); + } + + /** + * Creates an HTTP route that handles POST method requests. + * + * @param {String} pattern + * @param {...(RouteOptions|MiddlewareHandler)} args + */ + post() { + return this._register_route('post', ...arguments); + } + + /** + * Creates an HTTP route that handles PUT method requests. + * + * @param {String} pattern + * @param {...(RouteOptions|MiddlewareHandler)} args + */ + put() { + return this._register_route('put', ...arguments); + } + + /** + * Creates an HTTP route that handles DELETE method requests. + * + * @param {String} pattern + * @param {...(RouteOptions|MiddlewareHandler)} args + */ + delete() { + return this._register_route('del', ...arguments); + } + + /** + * Creates an HTTP route that handles HEAD method requests. + * + * @param {String} pattern + * @param {...(RouteOptions|MiddlewareHandler)} args + */ + head() { + return this._register_route('head', ...arguments); + } + + /** + * Creates an HTTP route that handles OPTIONS method requests. + * + * @param {String} pattern + * @param {...(RouteOptions|MiddlewareHandler)} args + */ + options() { + return this._register_route('options', ...arguments); + } + + /** + * Creates an HTTP route that handles PATCH method requests. + * + * @param {String} pattern + * @param {...(RouteOptions|MiddlewareHandler)} args + */ + patch() { + return this._register_route('patch', ...arguments); + } + + /** + * Creates an HTTP route that handles TRACE method requests. + * + * @param {String} pattern + * @param {...(RouteOptions|MiddlewareHandler)} args + */ + trace() { + return this._register_route('trace', ...arguments); + } + + /** + * Creates an HTTP route that handles CONNECT method requests. + * + * @param {String} pattern + * @param {...(RouteOptions|MiddlewareHandler)} args + */ + connect() { + return this._register_route('connect', ...arguments); + } + + /** + * Intercepts and handles upgrade requests for incoming websocket connections. + * Note! You must call response.upgrade(data) at some point in this route to open a websocket connection. + * + * @param {String} pattern + * @param {...(RouteOptions|MiddlewareHandler)} args + */ + upgrade() { + return this._register_route('upgrade', ...arguments); + } + + /** + * @typedef {Object} WSRouteOptions + * @property {('String'|'Buffer'|'ArrayBuffer')} message_type Specifies data type in which to provide incoming websocket messages. Default: 'String' + * @property {Number} compression Specifies preset for permessage-deflate compression. Specify one from HyperExpress.compressors.PRESET + * @property {Number} idle_timeout Specifies interval to automatically timeout/close idle websocket connection in seconds. Default: 32 + * @property {Number} max_backpressure Specifies maximum websocket backpressure allowed in character length. Default: 1024 * 1024 + * @property {Number} max_payload_length Specifies maximum length allowed on incoming messages. Default: 32 * 1024 + */ + + /** + * @typedef WSRouteHandler + * @type {function(import('../ws/Websocket.js')):void} + */ + + /** + * @param {String} pattern + * @param {WSRouteOptions|WSRouteHandler} options + * @param {WSRouteHandler} handler + */ + ws(pattern, options, handler) { + return this._register_route('ws', pattern, options, handler); + } + + /* Route getters */ + + /** + * Returns All routes in this router in the order they were registered. + * @returns {Array} + */ + get routes() { + return this.#records.routes; + } + + /** + * Returns all middlewares in this router in the order they were registered. + * @returns {Array} + */ + get middlewares() { + return this.#records.middlewares; + } +} + +module.exports = Router; diff --git a/packages/hyper-express/src/components/ws/Websocket.js b/packages/hyper-express/src/components/ws/Websocket.js new file mode 100644 index 0000000..e8d1ffc --- /dev/null +++ b/packages/hyper-express/src/components/ws/Websocket.js @@ -0,0 +1,423 @@ +'use strict'; +const EventEmitter = require('events'); +const { Readable, Writable } = require('stream'); +const { array_buffer_to_string } = require('../../shared/operators.js'); + +const FRAGMENTS = { + FIRST: 'FIRST', + MIDDLE: 'MIDDLE', + LAST: 'LAST', +}; + +class Websocket extends EventEmitter { + #ws; + #ip; + #context; + #stream; + #closed = false; + + constructor(ws) { + // Initialize event emitter + super(); + + // Parse information about websocket connection + this.#ws = ws; + this.#context = ws.context || {}; + this.#ip = array_buffer_to_string(ws.getRemoteAddressAsText()); + } + + /* EventEmitter overrides */ + + /** + * Binds an event listener to this `Websocket` instance. + * See the Node.js `EventEmitter` documentation for more details on this extended method. + * @param {('message'|'close'|'drain'|'ping'|'pong')} eventName + * @param {Function} listener + * @returns {Websocket} + */ + on(eventName, listener) { + // Pass all events to EventEmitter + super.on(eventName, listener); + return this; + } + + /** + * Binds a `one-time` event listener to this `Websocket` instance. + * See the Node.js `EventEmitter` documentation for more details on this extended method. + * @param {('message'|'close'|'drain'|'ping'|'pong')} eventName + * @param {Function} listener + * @returns {Websocket} + */ + once(eventName, listener) { + // Pass all events to EventEmitter + super.once(eventName, listener); + return this; + } + + /** + * Alias of uWS.cork() method. Accepts a callback with multiple operations for network efficiency. + * + * @param {Function} callback + * @returns {Websocket} + */ + atomic(callback) { + return this.#ws ? this.#ws.cork(callback) : this; + } + + /** + * Sends a message to websocket connection. + * Returns true if message was sent successfully. + * Returns false if message was not sent due to buil up backpressure. + * + * @param {String|Buffer|ArrayBuffer} message + * @param {Boolean=} is_binary + * @param {Boolean=} compress + * @returns {Boolean} + */ + send(message, is_binary, compress) { + // Send message through uWS connection + if (this.#ws) return this.#ws.send(message, is_binary, compress); + return false; + } + + /** + * Sends a ping control message. + * Returns Boolean depending on backpressure similar to send(). + * + * @param {String|Buffer|ArrayBuffer=} message + * @returns {Boolean} + */ + ping(message) { + // Send ping OPCODE message through uWS connection + return this.#ws ? this.#ws.ping(message) : false; + } + + /** + * Destroys this polyfill Websocket component and derefernces the underlying ws object + * @private + */ + _destroy() { + this.#ws = null; + this.#closed = true; + } + + /** + * Gracefully closes websocket connection by sending specified code and short message. + * + * @param {Number=} code + * @param {(String|Buffer|ArrayBuffer)=} message + */ + close(code, message) { + // Close websocket using uWS.end() method which gracefully closes connections + if (this.#ws) this.#ws.end(code, message); + } + + /** + * Forcefully closes websocket connection. + * No websocket close code/message is sent. + * This will immediately emit the 'close' event. + */ + destroy() { + if (this.#ws) this.#ws.close(); + } + + /** + * Returns whether this `Websocket` is subscribed to the specified topic. + * + * @param {String} topic + * @returns {Boolean} + */ + is_subscribed(topic) { + return this.#ws ? this.#ws.isSubscribed(topic) : false; + } + + /** + * Subscribe to a topic in MQTT syntax. + * MQTT syntax includes things like "root/child/+/grandchild" where "+" is a wildcard and "root/#" where "#" is a terminating wildcard. + * + * @param {String} topic + * @returns {Boolean} + */ + subscribe(topic) { + return this.#ws ? this.#ws.subscribe(topic) : false; + } + + /** + * Unsubscribe from a topic. + * Returns true on success, if the WebSocket was subscribed. + * + * @param {String} topic + * @returns {Boolean} + */ + unsubscribe(topic) { + return this.#ws ? this.#ws.unsubscribe(topic) : false; + } + + /** + * Publish a message to a topic in MQTT syntax. + * You cannot publish using wildcards, only fully specified topics. + * + * @param {String} topic + * @param {String|Buffer|ArrayBuffer} message + * @param {Boolean=} is_binary + * @param {Boolean=} compress + */ + publish(topic, message, is_binary, compress) { + return this.#ws ? this.#ws.publish(topic, message, is_binary, compress) : false; + } + + #buffered_fragment; + /** + * Buffers the provided fragment and returns the last buffered fragment. + * + * @param {String|Buffer|ArrayBuffer} fragment + * @returns {String|Buffer|ArrayBuffer|undefined} + */ + _buffer_fragment(fragment) { + const current = this.#buffered_fragment; + this.#buffered_fragment = fragment; + return current; + } + + /** + * Initiates fragment based message writing with uWS and writes appropriate chunk based on provided type parameter. + * + * @param {String} type + * @param {String|Buffer|ArrayBuffer} chunk + * @param {Boolean=} is_binary + * @param {Boolean=} compress + * @param {Function=} callback + * @returns {Boolean} + */ + _write(type, chunk, is_binary, compress, callback) { + // Ensure websocket still exists before attempting to write + if (this.#ws) { + // Attempt to send this fragment using the appropriate fragment method from uWS + let sent; + switch (type) { + case FRAGMENTS.FIRST: + sent = this.#ws.sendFirstFragment(chunk, is_binary, compress); + break; + case FRAGMENTS.MIDDLE: + sent = this.#ws.sendFragment(chunk, is_binary, compress); + break; + case FRAGMENTS.LAST: + sent = this.#ws.sendLastFragment(chunk, is_binary, compress); + break; + default: + throw new Error('Websocket._write() -> Invalid Fragment type constant provided.'); + } + + if (sent) { + // Invoke the callback if chunk was sent successfully + if (callback) callback(); + } else { + // Wait for this connection to drain before retrying this chunk + this.once('drain', () => this._write(type, chunk, is_binary, compress, callback)); + } + + // Return the sent status for consumer + return sent; + } + + // Throw an error with NOT_CONNECTED message to be caught by executor + throw new Error('Websocket is no longer connected.'); + } + + /** + * Streams the provided chunk while pausing the stream being consumed during backpressure. + * + * @param {Readable} stream + * @param {String} type + * @param {Buffer|ArrayBuffer} chunk + * @param {Boolean} is_binary + */ + _stream_chunk(stream, type, chunk, is_binary) { + // Break execution if connection is no longer connected + if (this.#ws === null) return; + + // Attempt to write this chunk + const sent = this._write(type, chunk, is_binary); + if (!sent) { + // Pause the readable stream as we failed to write this chunk + stream.pause(); + + // Wait for this connection to be drained before trying again + this.once('drain', () => this._stream_chunk(stream, type, chunk, is_binary)); + } else if (stream.isPaused()) { + // Resume the stream if it has been paused and we sent a chunk successfully + stream.resume(); + } + } + + /** + * This method is used to stream a message to the receiver. + * Note! The data is by default streamed as Binary due to how partial fragments are sent. + * This is done to prevent processing errors depending on client's receiver's incoming fragment processing strategy. + * + * @param {Readable} readable A Readable stream which will be consumed as message + * @param {Boolean=} is_binary Whether data being streamed is in binary. Default: true + * @returns {Promise} + */ + stream(readable, is_binary = true) { + // Ensure readable is an instance of a stream.Readable + if (!(readable instanceof Readable)) + throw new Error('Websocket.stream(readable) -> readable must be a Readable stream.'); + + // Prevent multiple streams from taking place + if (this.#stream) + throw new Error( + 'Websocket.stream(readable) -> You may not stream data while another stream operation is active on this websocket. Make sure you are not already streaming or piping a stream to this websocket.' + ); + + // Return a promise which resolves once stream has finished + const scope = this; + return new Promise((resolve) => { + // Store the readable as the pending stream for this connection + scope.#stream = readable; + + // Bind a listener for the 'data' event to consume chunks + let is_first = true; // By default, we will send the first chunk as a fragment + readable.on('data', (chunk) => { + // Check to see if we have a fragment to send post buffering + const fragment = scope._buffer_fragment(chunk); + if (fragment) { + // Stream the retrieved current fragment + scope._stream_chunk(readable, is_first ? FRAGMENTS.FIRST : FRAGMENTS.MIDDLE, fragment, is_binary); + + // If this was the first chunk, invert the is_first boolean + if (is_first) is_first = false; + } + }); + + // Create a callback for ending the readable consumption + const end_stream = () => { + // Retrieve the last buffered fragment to send as last or only chunk + const fragment = scope._buffer_fragment(); + + // If we streamed no individual fragments aka. the is_first flag was set to true, then we did no streaming and can simply send the last fragment as a message + if (is_first) { + scope.#ws.send(fragment, is_binary); + } else { + // Stream the final chunk as last fragment + scope._stream_chunk(scope.#stream, FRAGMENTS.LAST, fragment, is_binary); + } + + // Clean up the readable + scope.#stream = undefined; + resolve(); + }; + + // Bind listeners to end the framented write procedure + readable.once('end', end_stream); + }); + } + + /* Websocket Getters */ + + /** + * Underlying uWS.Websocket object + */ + get raw() { + return this.#ws; + } + + /** + * Returns IP address of this websocket connection. + * @returns {String} + */ + get ip() { + return this.#ip; + } + + /** + * Returns context values from the response.update(context) connection upgrade call. + * @returns {Object} + */ + get context() { + return this.#context; + } + + /** + * Returns whether is websocket connection is closed. + * @returns {Boolean} + */ + get closed() { + return this.#closed; + } + + /** + * Returns the bytes buffered in backpressure. + * This is similar to the bufferedAmount property in the browser counterpart. + * @returns {Number} + */ + get buffered() { + return this.#ws ? this.#ws.getBufferedAmount() : 0; + } + + /** + * Returns a list of topics this websocket is subscribed to. + * @returns {Array} + */ + get topics() { + return this.#ws ? this.#ws.getTopics() : []; + } + + /** + * Returns a Writable stream associated with this response to be used for piping streams. + * Note! You can only retrieve/use only one writable at any given time. + * + * @returns {Writable} + */ + get writable() { + // Prevent multiple streaming operations from taking place + const scope = this; + if (this.#stream) + throw new Error( + 'Websocket.writable -> You may only access and utilize one writable stream at any given time. Make sure you are not already streaming or piping a stream to this websocket.' + ); + + // Create a new writable stream object which will write with the _write method + let is_first = true; + this.#stream = new Writable({ + write: (chunk, encoding, callback) => { + // Buffer the incoming chunk as a fragment + const fragment = scope._buffer_fragment(chunk); + + // Check to see if we have a fragment to send post buffering + if (fragment) { + // Write the current retrieved fragment + scope._write(is_first ? FRAGMENTS.FIRST : FRAGMENTS.MIDDLE, fragment, true, false, callback); + + // Invert the is_first boolean after first fragment + if (is_first) is_first = false; + } else { + // Trigger the callback even if don't have a fragment to continue consuming + callback(); + } + }, + }); + + // Create a callback for ending the writable usage + const end_stream = () => { + // Retrieve the last buffered fragment to write as last or only chunk + const fragment = scope._buffer_fragment(); + + if (is_first) { + scope.#ws.send(fragment, true, false); + scope.#ws.stream = undefined; + } else { + // Write the final empty chunk as last fragment and cleanup the writable + scope._write(FRAGMENTS.LAST, fragment, true, false, () => (scope.#stream = undefined)); + } + }; + + // Bind listeners to end the fragmented write procedure + this.#stream.on('finish', end_stream); + + // Return the writable stream + return this.#stream; + } +} + +module.exports = Websocket; diff --git a/packages/hyper-express/src/components/ws/WebsocketRoute.js b/packages/hyper-express/src/components/ws/WebsocketRoute.js new file mode 100644 index 0000000..f0cd944 --- /dev/null +++ b/packages/hyper-express/src/components/ws/WebsocketRoute.js @@ -0,0 +1,198 @@ +'use strict'; +const uWebsockets = require('uWebSockets.js'); + +const Route = require('../router/Route.js'); +const Websocket = require('./Websocket.js'); +const { wrap_object, array_buffer_to_string } = require('../../shared/operators.js'); + +class WebsocketRoute extends Route { + #upgrade_with; + #message_parser; + options = { + idle_timeout: 32, + message_type: 'String', + compression: uWebsockets.DISABLED, + max_backpressure: 1024 * 1024, + max_payload_length: 32 * 1024, + }; + + constructor({ app, pattern, handler, options }) { + // Initialize the super Router class + super({ app, method: 'ws', pattern, options, handler }); + + // Wrap local default options with user specified options + wrap_object(this.options, options); + this.#message_parser = this._get_message_parser(this.options.message_type); + + // Load companion upgrade route and initialize uWS.ws() route + this._load_companion_route(); + this._create_uws_route(); + } + + /** + * Returns a parser that automatically converts uWS ArrayBuffer to specified data type. + * @private + * @returns {Function} + */ + _get_message_parser(type) { + switch (type) { + case 'String': + // Converts ArrayBuffer -> String + return (array_buffer) => array_buffer_to_string(array_buffer); + case 'Buffer': + // Converts & Copies ArrayBuffer -> Buffer + // We concat (copy) because ArrayBuffer from uWS is deallocated after initial synchronous execution + return (array_buffer) => Buffer.concat([Buffer.from(array_buffer)]); + case 'ArrayBuffer': + // Simply return the ArrayBuffer from uWS handler + return (array_buffer) => array_buffer; + default: + // Throw error on invalid type + throw new Error( + "Server.ws(options) -> options.message_type must be one of ['String', 'Buffer', 'ArrayBuffer']" + ); + } + } + + /** + * Loads a companion upgrade route from app routes object. + * @private + */ + _load_companion_route() { + const companion = this.app.routes['upgrade'][this.pattern]; + if (companion) { + // Use existing companion route as it is a user assigned route + this.#upgrade_with = companion; + } else { + // Create and use a temporary default route to allow for healthy upgrade request cycle + // Default route will upgrade all incoming requests automatically + this.app._create_route({ + method: 'upgrade', + pattern: this.pattern, + handler: (request, response) => response.upgrade(), // By default, upgrade all incoming requests + options: { + _temporary: true, // Flag this route as temporary so it will get overwritten by user specified upgrade route + }, + }); + + // Store created route locally as Server will not call _set_upgrade_route + // This is because this WebsocketRoute has not been created synchronously yet + this.#upgrade_with = this.app.routes['upgrade'][this.pattern]; + } + } + + /** + * Sets the upgrade route for incoming upgrade request to traverse through HyperExpress request lifecycle. + * @private + * @param {Route} route + */ + _set_upgrade_route(route) { + this.#upgrade_with = route; + } + + /** + * Creates a uWS.ws() route that will power this WebsocketRoute instance. + * @private + */ + _create_uws_route() { + // Destructure and convert HyperExpress options to uWS.ws() route options + const { compression, idle_timeout, max_backpressure, max_payload_length } = this.options; + const uws_options = { + compression, + idleTimeout: idle_timeout, + maxBackpressure: max_backpressure, + maxPayloadLength: max_payload_length, + }; + + // Create middleman upgrade route that pipes upgrade requests to HyperExpress request handler + uws_options.upgrade = (uws_response, uws_request, socket_context) => + this.app._handle_uws_request(this.#upgrade_with, uws_request, uws_response, socket_context); + + // Bind middleman routes to pipe uws events into poly handlers + uws_options.open = (ws) => this._on_open(ws); + uws_options.drain = (ws) => this._on_drain(ws); + uws_options.ping = (ws, message) => this._on_ping(ws, message); + uws_options.pong = (ws, message) => this._on_pong(ws, message); + uws_options.close = (ws, code, message) => this._on_close(ws, code, message); + uws_options.message = (ws, message, isBinary) => this._on_message(ws, message, isBinary); + + // Create uWebsockets instance route + this.app.uws_instance.ws(this.pattern, uws_options); + } + + /** + * Handles 'open' event from uWebsockets.js + * @private + * @param {uWS.Websocket} ws + */ + _on_open(ws) { + // Create and attach HyperExpress.Websocket polyfill component to uWS websocket + ws.poly = new Websocket(ws); + + // Trigger WebsocketRoute handler on new connection open so user can listen for events + this.handler(ws.poly); + } + + /** + * Handles 'ping' event from uWebsockets.js + * @private + * @param {uWS.Websocket} ws + * @param {ArrayBuffer=} message + */ + _on_ping(ws, message = '') { + // Emit 'ping' event on websocket poly component + ws.poly.emit('ping', this.#message_parser(message)); + } + + /** + * Handles 'pong' event from uWebsockets.js + * @private + * @param {uWS.Websocket} ws + * @param {ArrayBuffer=} message + */ + _on_pong(ws, message = '') { + // Emit 'pong' event on websocket poly component + ws.poly.emit('pong', this.#message_parser(message)); + } + + /** + * Handles 'drain' event from uWebsockets.js + * @private + * @param {uWS.Websocket} ws + */ + _on_drain(ws) { + // Emit 'drain' event on websocket poly component + ws.poly.emit('drain'); + } + + /** + * Handles 'message' event from uWebsockets.js + * @private + * @param {uWS.Websocket} ws + * @param {ArrayBuffer} message + * @param {Boolean} is_binary + */ + _on_message(ws, message = '', is_binary) { + // Emit 'message' event with parsed message from uWS + ws.poly.emit('message', this.#message_parser(message), is_binary); + } + + /** + * Handles 'close' event from uWebsockets.js + * @param {uWS.Websocket} ws + * @param {Number} code + * @param {ArrayBuffer} message + */ + _on_close(ws, code, message = '') { + // Mark websocket poly component as closed + ws.poly._destroy(); + + // Emit 'close' event with parsed message + ws.poly.emit('close', code, this.#message_parser(message)); + + // De-reference the attached polyfill Websocket component so it can ne garbage collected + delete ws.poly; + } +} + +module.exports = WebsocketRoute; diff --git a/packages/hyper-express/src/shared/operators.js b/packages/hyper-express/src/shared/operators.js new file mode 100644 index 0000000..0e00a7b --- /dev/null +++ b/packages/hyper-express/src/shared/operators.js @@ -0,0 +1,197 @@ +'use strict'; +/** + * Writes values from focus object onto base object. + * + * @param {Object} obj1 Base Object + * @param {Object} obj2 Focus Object + */ +function wrap_object(original, target) { + Object.keys(target).forEach((key) => { + if (typeof target[key] == 'object') { + if (Array.isArray(target[key])) return (original[key] = target[key]); // lgtm [js/prototype-pollution-utility] + if (original[key] === null || typeof original[key] !== 'object') original[key] = {}; + wrap_object(original[key], target[key]); + } else { + original[key] = target[key]; + } + }); +} + +/** + * This method parses route pattern into an array of expected path parameters. + * + * @param {String} pattern + * @returns {Array} [[key {String}, index {Number}], ...] + */ + +function parse_path_parameters(pattern) { + let results = []; + let counter = 0; + if (pattern.indexOf('/:') > -1) { + let chunks = pattern.split('/').filter((chunk) => chunk.length > 0); + for (let index = 0; index < chunks.length; index++) { + let current = chunks[index]; + if (current.startsWith(':') && current.length > 2) { + results.push([current.substring(1), counter]); + counter++; + } + } + } + return results; +} + +/** + * This method converts ArrayBuffers to a string. + * + * @param {ArrayBuffer} array_buffer + * @param {String} encoding + * @returns {String} String + */ + +function array_buffer_to_string(array_buffer, encoding = 'utf8') { + return Buffer.from(array_buffer).toString(encoding); +} + +/** + * Copies an ArrayBuffer to a Uint8Array. + * Note! This method is supposed to be extremely performant as it is used by the Body parser. + * @param {ArrayBuffer} array_buffer + */ +function copy_array_buffer_to_uint8array(array_buffer) { + const source = new Uint8Array(array_buffer); + return new Uint8Array(source.subarray(0, source.length)); +} + +/** + * Returns a promise which is resolved after provided delay in milliseconds. + * + * @param {Number} delay + * @returns {Promise} + */ +function async_wait(delay) { + return new Promise((resolve, reject) => setTimeout((res) => res(), delay, resolve)); +} + +/** + * Merges provided relative paths into a singular relative path. + * + * @param {String} base_path + * @param {String} new_path + * @returns {String} path + */ +function merge_relative_paths(base_path, new_path) { + // handle both roots merger case + if (base_path == '/' && new_path == '/') return '/'; + + // Inject leading slash to new_path + if (!new_path.startsWith('/')) new_path = '/' + new_path; + + // handle base root merger case + if (base_path == '/') return new_path; + + // handle new path root merger case + if (new_path == '/') return base_path; + + // strip away leading slash from base path + if (base_path.endsWith('/')) base_path = base_path.substr(0, base_path.length - 1); + + // Merge path and add a slash in between if new_path does not have a starting slash + return `${base_path}${new_path}`; +} + +/** + * Returns all property descriptors of an Object including extended prototypes. + * + * @param {Object} prototype + */ +function get_all_property_descriptors(prototype) { + // Retrieve initial property descriptors + const descriptors = Object.getOwnPropertyDescriptors(prototype); + + // Determine if we have a parent prototype with a custom name + const parent = Object.getPrototypeOf(prototype); + if (parent && parent.constructor.name !== 'Object') { + // Merge and return property descriptors along with parent prototype + return Object.assign(descriptors, get_all_property_descriptors(parent)); + } + + // Return property descriptors + return descriptors; +} + +/** + * Inherits properties, getters, and setters from one prototype to another with the ability to optionally define middleman methods. + * + * @param {Object} options + * @param {Object|Array} options.from - The prototype to inherit from + * @param {Object} options.to - The prototype to inherit to + * @param {function(('FUNCTION'|'GETTER'|'SETTER'), string, function):function=} options.method - The method to inherit. Parameters are: type, name, method. + * @param {function(string):string=} options.override - The method name to override the original with. Parameters are: name. + * @param {Array} options.ignore - The property names to ignore + */ +function inherit_prototype({ from, to, method, override, ignore = ['constructor'] }) { + // Recursively call self if the from prototype is an Array of prototypes + if (Array.isArray(from)) return from.forEach((f) => inherit_prototype({ from: f, to, override, method, ignore })); + + // Inherit the descriptors from the "from" prototype to the "to" prototype + const to_descriptors = get_all_property_descriptors(to); + const from_descriptors = get_all_property_descriptors(from); + Object.keys(from_descriptors).forEach((name) => { + // Ignore the properties specified in the ignore array + if (ignore.includes(name)) return; + + // Destructure the descriptor function properties + const { value, get, set } = from_descriptors[name]; + + // Determine if this descriptor name would be an override + // Override the original name with the provided name resolver for overrides + if (typeof override == 'function' && to_descriptors[name]?.value) name = override(name) || name; + + // Determine if the descriptor is a method aka. a function + if (typeof value === 'function') { + // Inject a middleman method into the "to" prototype + const middleman = method('FUNCTION', name, value); + if (middleman) { + // Define the middleman method on the "to" prototype + Object.defineProperty(to, name, { + configurable: true, + enumerable: true, + writable: true, + value: middleman, + }); + } + } else { + // Initialize a definition object + const definition = {}; + + // Initialize a middleman getter method + if (typeof get === 'function') definition.get = method('GETTER', name, get); + + // Initialize a middleman setter method + if (typeof set === 'function') definition.set = method('SETTER', name, set); + + // Inject the definition into the "to" prototype + if (definition.get || definition.set) Object.defineProperty(to, name, definition); + } + }); +} + +/** + * Converts Windows path backslashes to forward slashes. + * @param {string} string + * @returns {string} + */ +function to_forward_slashes(string) { + return string.split('\\').join('/'); +} + +module.exports = { + parse_path_parameters, + array_buffer_to_string, + wrap_object, + async_wait, + inherit_prototype, + merge_relative_paths, + to_forward_slashes, + copy_array_buffer_to_uint8array, +}; diff --git a/packages/hyper-express/src/shared/process-multipart.js b/packages/hyper-express/src/shared/process-multipart.js new file mode 100644 index 0000000..6e7c465 --- /dev/null +++ b/packages/hyper-express/src/shared/process-multipart.js @@ -0,0 +1,60 @@ +const os = require('os'); +const path = require('path'); +const UploadedFile = require('./uploaded-file'); + +async function process_multipart_data(req) { + const fields = {}; + const tempDirectory = os.tmpdir(); + const generateTempFilename = (filename) => `${Date.now()}-${filename}`; + const advancedPattern = /(\w+)(\[\d*\])+/; + + await req.multipart(async (field) => { + const isArray = field.name.match(advancedPattern); + const strippedFieldName = isArray?.length ? isArray[1] : field.name; + const existingFieldValue = fields[strippedFieldName]; + + if (Array.isArray(isArray) && !existingFieldValue) { + fields[strippedFieldName] = []; + } + + if (field.file) { + const tempFileName = generateTempFilename(field.file.name); + const tempFilePath = path.join(tempDirectory, tempFileName); + let fileSize = 0; + + field.file.stream.on('data', (chunk) => { + fileSize += chunk.length; + }); + + await field.write(tempFilePath); + + const uploadedFile = new UploadedFile( + field.file.name, + fileSize, + field.mime_type, + tempFileName, + tempFilePath, + ); + + const existingFieldValue = fields[strippedFieldName]; + if (Array.isArray(existingFieldValue)) { + fields[strippedFieldName] = existingFieldValue.concat(uploadedFile); + } else { + fields[strippedFieldName] = uploadedFile; + } + } else { + const existingFieldValue = fields[strippedFieldName]; + if (Array.isArray(existingFieldValue)) { + fields[strippedFieldName] = existingFieldValue.concat(field.value); + } else { + fields[strippedFieldName] = field.value; + } + } + }); + + return fields; +} + +module.exports = { + process_multipart_data, +}; diff --git a/packages/hyper-express/src/shared/uploaded-file.js b/packages/hyper-express/src/shared/uploaded-file.js new file mode 100644 index 0000000..2cbb4f0 --- /dev/null +++ b/packages/hyper-express/src/shared/uploaded-file.js @@ -0,0 +1,39 @@ +const fs = require('fs'); + +class UploadedFile { + _filename; + _sizeInBytes; + _mimeType; + _tempName; + _tempPath; + + constructor(filename, size, mimeType, tempName, tempPath) { + this._filename = filename; + this._sizeInBytes = size; + this._mimeType = mimeType; + this._tempName = tempName; + this._tempPath = tempPath; + } + + get filename() { + return this._filename; + } + + get sizeInBytes() { + return this._sizeInBytes; + } + + get mimeType() { + return this._mimeType; + } + + get extension() { + return this.filename; + } + + async toBuffer() { + return fs.readFileSync(this.tempPath); + } +} + +module.exports = UploadedFile; diff --git a/packages/hyper-express/tests/components/Server.js b/packages/hyper-express/tests/components/Server.js new file mode 100644 index 0000000..a57320a --- /dev/null +++ b/packages/hyper-express/tests/components/Server.js @@ -0,0 +1,110 @@ +const { server, HyperExpress } = require('../configuration.js'); +const { log, assert_log } = require('../scripts/operators.js'); + +// Create a test HyperExpress instance +const TEST_SERVER = new HyperExpress.Server({ + fast_buffers: true, + max_body_length: 1000 * 1000 * 7, +}); + +// Set some value into the locals object to be checked in the future +// through the Request/Response app property +TEST_SERVER.locals.some_reference = { + some_data: true, +}; + +// Bind error handler for catch-all logging +TEST_SERVER.set_error_handler((request, response, error) => { + // Handle expected errors with their appropriate callbacks + if (typeof request.expected_error == 'function') { + request.expected_error(error); + } else { + // Treat as global error and log to console + log( + 'UNCAUGHT_ERROR_REQUEST', + `${request.method} | ${request.url}\n ${JSON.stringify(request.headers, null, 2)}` + ); + console.log(error); + response.send('Uncaught Error Occured'); + } +}); + +function not_found_handler(request, response) { + // Handle dynamic middleware executions to the requester + if (Array.isArray(request.middleware_executions)) { + request.middleware_executions.push('not-found'); + return response.json(request.middleware_executions); + } + + // Return a 404 response + return response.status(404).send('Not Found'); +} + +// Bind not found handler for unexpected incoming requests +TEST_SERVER.set_not_found_handler((request, response) => { + console.warn( + 'This handler should not actually be called as one of the tests binds a Server.all("*") route which should prevent this handler from ever being ran.' + ); + not_found_handler(request, response); +}); + +// Bind a test route which returns a response with a delay +// This will be used to simulate long running requests +TEST_SERVER.get('/echo/:delay', async (request, response) => { + // Wait for the specified delay and return a response + const delay = Number(request.path_parameters.delay) || 0; + await new Promise((resolve) => setTimeout(resolve, delay)); + return response.send(delay.toString()); +}); + +async function test_server_shutdown() { + let group = 'SERVER'; + + // Make a fetch request to the echo endpoint with a delay of 100ms + const delay = 100; + const started_at = Date.now(); + + // Send the request and time the response + const response = await fetch(`${server.base}/echo/${delay}`); + + // Begin the server shutdown process and time the shutdown + let shutdown_time_ms = 0; + const shutdown_promise = TEST_SERVER.shutdown(); + shutdown_promise.then(() => (shutdown_time_ms = Date.now() - started_at)); + + // Send a second fetch which should be immediately closed + let response2_error; + try { + const response2 = await fetch(`${server.base}/echo/${delay}`); + } catch (error) { + response2_error = error; + } + + // Begin processing the response body + const body = await response.text(); + const request_time_ms = Date.now() - started_at; + + // Wait for the server shutdown to complete + await shutdown_promise; + + // Verify middleware functionalitiy and property binding + assert_log( + group, + 'Graceful Shutdown Test In ' + (Date.now() - started_at) + 'ms', + // Ensure that the response body matches the delay + // Ensure that the request time is greater than the delay (The handler artificially waited for the delay) + // Ensure that the shutdown time is greater than the delay (The server shutdown took longer than the delay) + // Ensure that response2 failed over network as the server shutdown was in process which would immediately close the request + () => + body === delay.toString() && + request_time_ms >= delay && + shutdown_time_ms >= delay && + response2_error !== undefined + ); +} + +module.exports = { + TEST_SERVER, + not_found_handler, + test_server_shutdown, +}; diff --git a/packages/hyper-express/tests/components/features/HostManager.js b/packages/hyper-express/tests/components/features/HostManager.js new file mode 100644 index 0000000..0f17205 --- /dev/null +++ b/packages/hyper-express/tests/components/features/HostManager.js @@ -0,0 +1,58 @@ +const { assert_log } = require('../../scripts/operators.js'); +const { TEST_SERVER } = require('../Server.js'); + +function test_hostmanager_object() { + let group = 'Server'; + let candidate = 'HyperExpress.HostManager'; + + // Retrieve the host manager + const manager = TEST_SERVER.hosts; + + // Define random host configurations + const hostnames = [ + [ + 'example.com', + { + passphrase: 'passphrase', + }, + ], + [ + 'google.com', + { + passphrase: 'passphrase', + }, + ], + ]; + + // Add the host names to the host manager + for (const [hostname, options] of hostnames) { + manager.add(hostname, options); + } + + // Assert that the host manager contains the host names + for (const [hostname, options] of hostnames) { + assert_log( + group, + candidate + ` - Host Registeration Test For ${hostname}`, + () => JSON.stringify(manager.registered[hostname]) === JSON.stringify(options) + ); + } + + // Remove the host names from the host manager + for (const [hostname, options] of hostnames) { + manager.remove(hostname); + } + + // Assert that the host manager does not contain the host names + for (const [hostname, options] of hostnames) { + assert_log( + group, + candidate + ` - Host Un-Registeration Test For ${hostname}`, + () => !(hostname in manager.registered) + ); + } +} + +module.exports = { + test_hostmanager_object, +}; diff --git a/packages/hyper-express/tests/components/features/LiveFile.js b/packages/hyper-express/tests/components/features/LiveFile.js new file mode 100644 index 0000000..b62e4f1 --- /dev/null +++ b/packages/hyper-express/tests/components/features/LiveFile.js @@ -0,0 +1,49 @@ +const { assert_log } = require('../../scripts/operators.js'); +const { fetch, server } = require('../../configuration.js'); +const { TEST_SERVER } = require('../Server.js'); +const fs = require('fs'); +const path = require('path'); +const endpoint = '/tests/response/send-file'; +const endpoint_url = server.base + endpoint; +const test_file_path = path.resolve(__dirname, '../../../tests/content/test.html'); + +// Create Backend HTTP Route +TEST_SERVER.get(endpoint, async (request, response) => { + // We purposely delay 100ms so cached vs. uncached does not rely too much on system disk + return response.download(test_file_path, 'something.html'); +}); + +async function test_livefile_object() { + let group = 'RESPONSE'; + let candidate = 'HyperExpress.Response'; + + // Read the test file into memory + const test_file_string = fs.readFileSync(test_file_path).toString(); + + // Perform fetch request + const response = await fetch(endpoint_url); + const body = await response.text(); + + // Test initial content type and length test for file + const headers = response.headers.raw(); + const content_type = headers['content-type']; + const content_length = headers['content-length']; + assert_log(group, candidate + '.file()', () => { + return ( + content_type == 'text/html; charset=utf-8' && + content_length == test_file_string.length.toString() && + body.length == test_file_string.length + ); + }); + + // Test Content-Disposition header to validate .attachment() + assert_log( + group, + `${candidate}.attachment() & ${candidate}.download()`, + () => headers['content-disposition'][0] == 'attachment; filename="something.html"' + ); +} + +module.exports = { + test_livefile_object: test_livefile_object, +}; diff --git a/packages/hyper-express/tests/components/http/Request.js b/packages/hyper-express/tests/components/http/Request.js new file mode 100644 index 0000000..56dccf4 --- /dev/null +++ b/packages/hyper-express/tests/components/http/Request.js @@ -0,0 +1,363 @@ +const { log, assert_log, random_string } = require('../../scripts/operators.js'); +const { HyperExpress, fetch, server } = require('../../configuration.js'); +const { test_request_multipart } = require('./scenarios/request_multipart.js'); +const { test_request_stream_pipe } = require('./scenarios/request_stream.js'); +const { test_request_chunked_stream } = require('./scenarios/request_chunked_stream.js'); +const { test_request_body_echo_test } = require('./scenarios/request_body_echo_test.js'); +const { test_request_uncaught_rejections } = require('./scenarios/request_uncaught_rejections.js'); +const { test_request_router_paths_test } = require('./scenarios/request_router_paths_test.js'); +const { test_request_chunked_json } = require('./scenarios/request_chunked_json.js'); +const fs = require('fs'); +const _path = require('path'); +const crypto = require('crypto'); +const router = new HyperExpress.Router(); +const endpoint = '/tests/request/:param1/:param2'; +const route_specific_endpoint = '/tests/request-route/'; +const middleware_delay = 100 + Math.floor(Math.random() * 150); +const signature_value = random_string(10); +const signature_secret = random_string(10); +const middleware_property = random_string(10); +const base = server.base; + +// Bind a middlewares for simulating artificial delay on request endpoint +const global_middleware_1 = (request, response, next) => { + // We only want this middleware to run for this request endpoint + if (request.headers['x-middleware-test'] === 'true') { + request.mproperty = middleware_property; + return setTimeout((n) => n(), middleware_delay, next); + } + + return next(); +}; +router.use(global_middleware_1); + +// Test Promise returning middlewares support +const global_middleware_2 = (request, response) => { + return new Promise((resolve, reject) => { + // We only want this middleware to run for this request endpoint + if (request.headers['x-middleware-test-2'] === 'true') { + request.mproperty2 = middleware_property; + } + + resolve(); + }); +}; +router.use(global_middleware_2); + +let last_endpoint_mproperty; +let last_endpoint_mproperty2; +let last_endpoint_mproperty3; + +const route_specific_middleware = (request, response, next) => { + // We only want this middleware to run for this request endpoint + if (request.headers['x-middleware-test-3'] === 'true') { + request.mproperty3 = middleware_property; + return setTimeout((n) => n(), middleware_delay, next); + } + + return next(); +}; + +// Load scenarios and bind router to test server +const { test_middleware_double_iteration } = require('./scenarios/middleware_double_iteration.js'); +const { test_middleware_iteration_error } = require('./scenarios/middleware_iteration_error.js'); +const { test_middleware_uncaught_async_error } = require('./scenarios/middleware_uncaught_async_error.js'); +const { test_middleware_layered_iterations } = require('./scenarios/middleware_layered_iteration.js'); +const { test_middleware_dynamic_iteration } = require('./scenarios/middleware_dynamic_iteration.js'); +const { test_middleware_execution_order } = require('./scenarios/middleware_execution_order.js'); +const { TEST_SERVER } = require('../Server.js'); +TEST_SERVER.use(router); + +// Create a temporary specific middleware route +router.get( + route_specific_endpoint, + { + middlewares: [route_specific_middleware], + }, + (request, response) => { + // Store mproperty if exists on request object + if (request.mproperty3) last_endpoint_mproperty3 = request.mproperty3; + + let body_error; + try { + request.body; + } catch (error) { + body_error = error; + } + + return response.json({ + success: true, + body_error: body_error !== undefined, + }); + } +); + +// Create Backend HTTP Route with expected body of urlencoded to test request.body property +router.any(endpoint, async (request, response) => { + // Parse the incoming request body as text, json, and urlencoded to test all formats + let text = await request.text(); + let json = await request.json(); + let urlencoded = await request.urlencoded(); + + // Store mproperty if exists on request object to check for middleware + if (request.mproperty) last_endpoint_mproperty = request.mproperty; + if (request.mproperty2) last_endpoint_mproperty2 = request.mproperty; + + // Return all possible information about incoming request + return response.json({ + locals: request.app.locals, + method: request.method, + url: request.url, + path: request.path, + path_query: request.path_query, + headers: request.headers, + path_parameters: request.path_parameters, + query_parameters: request.query_parameters, + ip: request.ip, + proxy_ip: request.proxy_ip, + cookies: request.cookies, + signature_check: + request.unsign(request.sign(signature_value, signature_secret), signature_secret) === signature_value, + body: { + text, + json, + urlencoded, + }, + }); +}); + +function crypto_random(length) { + return new Promise((resolve, reject) => + crypto.randomBytes(Math.round(length / 2), (error, buffer) => { + if (error) return reject(error); + resolve(buffer.toString('hex')); + }) + ); +} + +async function test_request_object() { + // Prepare Test Candidates + log('REQUEST', 'Testing HyperExpress.Request Object...'); + + const body_size = 10 * 1024 * 1024; + log( + 'REQUEST', + `Generating A Large ${body_size.toLocaleString()} Characters Size Body To Simulate Too-Large Large Payload...` + ); + + let group = 'REQUEST'; + let candidate = 'HyperExpress.Request'; + let start_time = Date.now(); + let test_method = 'POST'; + let param1 = random_string(10); + let param2 = random_string(10); + let query1 = random_string(10); + let query2 = random_string(10); + let query = `?query1=${query1}&query2=${query2}`; + let too_large_body_value = await crypto_random(body_size); + let body_test_value = too_large_body_value.substr(0, too_large_body_value.length / 2); + let fetch_body = JSON.stringify({ + test_value: body_test_value, + }); + let header_test_value = random_string(10); + let header_test_cookie = { + name: random_string(10), + value: random_string(10), + }; + + // Prepare HTTP Request Information + let path = `/tests/request/${param1}/${param2}`; + let url = path + query; + let options = { + method: test_method, + headers: { + 'x-test-value': header_test_value, + 'content-type': 'application/json', + cookie: `${header_test_cookie.name}=${header_test_cookie.value}`, + 'x-middleware-test': 'true', + 'x-middleware-test-2': 'true', + }, + body: fetch_body, + }; + + // Perform Too Large Body Rejection Test + const too_large_response = await fetch(base + url, { + method: test_method, + body: too_large_body_value, + }); + + // Assert no uwebsockets version header to be found + assert_log(group, 'No uWebsockets Version Header', () => !too_large_response.headers.get('uwebsockets')); + + // Assert rejection status code as 413 Too Large Payload + assert_log(group, 'Too Large Body 413 HTTP Code Reject', () => too_large_response.status === 413); + + // Perform a too large body test with transfer-encoding: chunked + const temp_file_path = _path.resolve(_path.join(__dirname, '../../../tests/content/too-large-file.temp')); + fs.writeFileSync(temp_file_path, too_large_body_value); + try { + const too_large_chunked_response = await fetch(base + url, { + method: test_method, + body: fs.createReadStream(temp_file_path), + headers: { + 'transfer-encoding': 'chunked', + }, + }); + + // Cleanup the temp file + fs.unlinkSync(temp_file_path); + + // Assert rejection status code as 413 Too Large Payload + assert_log( + group, + 'Too Large Body 413 HTTP Code Reject (Chunked)', + () => too_large_chunked_response.status === 413 + ); + } catch (error) { + // Cleanup the temp file + fs.unlinkSync(temp_file_path); + } + + // Perform a request with a urlencoded body to test .urlencoded() method + const urlencoded_string = `url1=${param1}&url2=${param2}`; + const urlencoded_response = await fetch(base + url, { + method: test_method, + body: urlencoded_string, + }); + const urlencoded_body = await urlencoded_response.json(); + + // Perform HTTP Request To Endpoint + let req_start_time = Date.now(); + let response = await fetch(base + url, options); + let body = await response.json(); + + // Verify middleware functionalitiy and property binding + assert_log(group, 'Middleware Execution & Timing Test', () => Date.now() - req_start_time > middleware_delay); + + assert_log( + group, + 'Middleware Property Binding Test', + () => last_endpoint_mproperty === middleware_property && last_endpoint_mproperty2 === middleware_property + ); + + assert_log(group, 'Route Specific Middleware Avoidance Test', () => last_endpoint_mproperty3 == undefined); + + await fetch(base + route_specific_endpoint, { + headers: { + 'x-middleware-test-3': 'true', + }, + }); + + assert_log( + group, + 'Route Specific Middleware Binding & Property Test', + () => last_endpoint_mproperty3 === middleware_property + ); + + // Test request uncaught rejections + await test_request_uncaught_rejections(); + + // Test double iteration violation for middlewares + await test_middleware_double_iteration(); + + // Test layered middleware iterations + await test_middleware_layered_iterations(); + + // Test simulated middleware iteration error + await test_middleware_iteration_error(); + + // Test uncaught async middleware error + await test_middleware_uncaught_async_error(); + + // Test dynamic middleware iteration + await test_middleware_dynamic_iteration(); + + // Test middleware execution order + await test_middleware_execution_order(); + + // Verify .app.locals + assert_log(group, candidate + '.app.locals', () => body.locals.some_reference.some_data === true); + + // Verify .method + assert_log(group, candidate + '.method', () => test_method === body.method); + + // Verify .url + assert_log(group, candidate + '.url', () => body.url === url); + + // Verify .path + assert_log(group, candidate + '.path', () => path === body.path); + + test_request_router_paths_test(); + + // Verify .query + assert_log(group, candidate + '.query', () => query.substring(1) === body.path_query); + + // Verify .ip + assert_log(group, candidate + '.ip', () => body.ip === '127.0.0.1'); + + // Verify .proxy_ip + assert_log(group, candidate + '.proxy_ip', () => body.proxy_ip === ''); + + // Verify .headers + assert_log(group, candidate + '.headers["x-test-value", "cookie", "content-length"]', () => { + let headers = body.headers; + let value_test = headers['x-test-value'] === header_test_value; + let cookie_test = headers.cookie === options.headers.cookie; + let content_length_test = +headers['content-length'] === fetch_body.length; + return value_test && cookie_test && content_length_test; + }); + + // Verify .query_parameters + assert_log(group, candidate + '.query_parameters', () => { + let query1_test = body.query_parameters.query1 === query1; + let query2_test = body.query_parameters.query2 === query2; + return query1_test && query2_test; + }); + + // Verify .path_parameters + assert_log(group, candidate + '.path_parameters', () => { + let param1_test = body.path_parameters.param1 === param1; + let param2_test = body.path_parameters.param2 === param2; + return param1_test && param2_test; + }); + + // Verify .cookies + assert_log(group, candidate + '.cookies', () => body.cookies[header_test_cookie.name] === header_test_cookie.value); + + // Verify chunked transfer request stream + await test_request_chunked_stream(); + + // Verify .stream readable request stream piping + await test_request_stream_pipe(); + + // Verify .sign() and .unsign() + assert_log(group, `${candidate}.sign() and ${candidate}.unsign()`, () => body.signature_check === true); + + // Verify .text() + assert_log(group, candidate + '.text()', () => body.body.text === options.body); + + // Verify .json() + assert_log(group, candidate + '.json()', () => JSON.stringify(body.body.json) === options.body); + + // Verify .json() with chunked transfer + await test_request_chunked_json(); + + // Verify .json() with small body payload echo test + await test_request_body_echo_test(); + + // Verify .urlencoded() + assert_log(group, candidate + '.urlencoded()', () => { + const { url1, url2 } = urlencoded_body.body.urlencoded; + return url1 === param1 && url2 === param2; + }); + + // Test .multipart() uploader with both a sync/async handler + await test_request_multipart(false); + await test_request_multipart(true); + + log(group, `Finished Testing ${candidate} In ${Date.now() - start_time}ms\n`); +} + +module.exports = { + test_request_object: test_request_object, +}; diff --git a/packages/hyper-express/tests/components/http/Response.js b/packages/hyper-express/tests/components/http/Response.js new file mode 100644 index 0000000..6889b36 --- /dev/null +++ b/packages/hyper-express/tests/components/http/Response.js @@ -0,0 +1,198 @@ +const { log, assert_log, random_string } = require('../../scripts/operators.js'); +const { HyperExpress, fetch, server } = require('../../configuration.js'); +const { test_livefile_object } = require('../../components/features/LiveFile.js'); +const { test_response_custom_status } = require('./scenarios/response_custom_status.js'); +const { test_response_send_no_body } = require('./scenarios/response_send_no_body.js'); +const { test_response_headers_behavior } = require('./scenarios/response_headers_behavior.js'); +const { test_response_stream_method } = require('./scenarios/response_stream.js'); +const { test_response_chunked_write } = require('./scenarios/response_chunked_write.js'); +const { test_response_piped_write } = require('./scenarios/response_piped.js'); +const { test_response_events } = require('./scenarios/response_hooks.js'); +const { test_response_sync_writes } = require('./scenarios/response_stream_sync_writes.js'); +const { test_response_custom_content_length } = require('./scenarios/response_custom_content_length.js'); +const { test_response_sse } = require('./scenarios/response_sse.js'); +const { test_response_set_header } = require('./scenarios/response_set_header.js'); +const router = new HyperExpress.Router(); +const endpoint = '/tests/response/operators'; +const endpoint_url = server.base + endpoint; + +function write_prepare_event(request, response) { + if (typeof request.url == 'string' && !response.completed) { + response.header('hook-called', 'prepare'); + events_emitted.push('prepare'); + } +} + +// Create Backend HTTP Route +const events_emitted = []; +router.post(endpoint, async (request, response) => { + let body = await request.json(); + + // Validate response.app.locals + if (response.app.locals.some_reference.some_data !== true) throw new Error('Invalid Response App Locals Detected!'); + + // Test hooks + response.on('abort', () => events_emitted.push('abort')); + response.on('prepare', write_prepare_event); + response.on('finish', () => events_emitted.push('finish')); + response.on('close', () => events_emitted.push('close')); + + // Perform Requested Operations For Testing + if (Array.isArray(body)) + body.forEach((operation) => { + let method = operation[0]; + let parameters = operation[1]; + + // Utilize the Response.statusCode compatibility setter for status code modifications + if (method == 'status') { + response.statusCode = parameters; + } else if (Array.isArray(parameters)) { + // Support up to 4 multi parameters + response[method](parameters[0], parameters[1], parameters[2], parameters[3]); + } else { + response[method](parameters); + } + }); + + if (!response.aborted) return response.send(); +}); + +// Bind router to webserver +const { TEST_SERVER } = require('../Server.js'); +const { test_response_send_status } = require('./scenarios/response_send_status.js'); +TEST_SERVER.use(router); + +async function test_response_object() { + let start_time = Date.now(); + let group = 'RESPONSE'; + let candidate = 'HyperExpress.Response'; + log(group, 'Testing HyperExpress.Response Object...'); + + // Test HyperExpress.Response Operators + let test_status_code = 404; + let test_mime_type = 'html'; + let header_test_name = random_string(10); + let header_test_value = random_string(10); + let cookie_test_name = random_string(10); + let cookie_test_value = random_string(10); + let test_html_placeholder = random_string(20); + let test_cookie = { + name: random_string(10) + '_sess', + value: random_string(10), + }; + + let response1 = await fetch(endpoint_url, { + method: 'POST', + headers: { + 'content-type': 'application/json', + cookie: `${test_cookie.name}=${test_cookie.value}`, + }, + body: JSON.stringify([ + ['status', test_status_code], + ['type', test_mime_type], + ['header', [header_test_name, header_test_value]], + ['cookie', [cookie_test_name, cookie_test_value]], + ['cookie', [test_cookie.name, null]], + ['send', test_html_placeholder], + ]), + }); + let body1 = await response1.text(); + + // Verify .status() + assert_log(group, candidate + '.status()', () => test_status_code === response1.status); + + // Verify .type() + assert_log( + group, + candidate + '.type()', + () => response1.headers.get('content-type') === 'text/html; charset=utf-8' + ); + + // Verify .header() + assert_log(group, candidate + '.header()', () => response1.headers.get(header_test_name) === header_test_value); + + // Verify .cookie() + assert_log(group, candidate + '.cookie() AND .cookie(name, null) to delete', () => { + let cookies = {}; + response1.headers + .get('set-cookie') + .split(', ') + .forEach((chunk) => { + if (chunk.indexOf('=') > -1) { + chunk = chunk.split('='); + let name = chunk[0]; + let value = chunk[1].split(';')[0]; + let properties = chunk.join('=').split('; ')[1]; + cookies[name] = { + value: value, + properties: properties, + }; + } + }); + + let test_cookie_test = cookies[cookie_test_name]?.value === cookie_test_value; + let delete_cookie_value_test = cookies[test_cookie.name]?.value === ''; + let delete_cookie_props_test = cookies[test_cookie.name]?.properties === 'Max-Age=0'; + return test_cookie_test && delete_cookie_value_test && delete_cookie_props_test; + }); + + // Verify the custom HTTP status code and message support + await test_response_custom_status(); + + // Verify the custom HTTP status code and message support + await test_response_send_status(); + + // Verify the behavior of the .header() and .cookie() methods + await test_response_headers_behavior(); + + // Verify .on() aka. Response events + await test_response_events(); + + // Verify .send() + assert_log(group, candidate + '.send()', () => body1 === test_html_placeholder); + + // Verify .send() with custom content-length header specified body + await test_response_custom_content_length(); + + // Verify .send() with no body and custom content-length + await test_response_send_no_body(); + + // Test Response.sse (Server-Sent Events) support + await test_response_sse(); + + // Test Response.stream() + await test_response_stream_method(); + + // Test Response.write() for sync writes + await test_response_sync_writes(); + + // Test Response.write() for chunked writing + await test_response_chunked_write(); + + // Test Response.write() for piped writes + await test_response_piped_write(); + + // Test Response.LiveFile object + await test_livefile_object(); + + // Test Response.set() header + await test_response_set_header(); + + // Verify .on() aka. Response events + assert_log( + group, + candidate + '.on()', + () => + events_emitted.length == 3 && + events_emitted[0] === 'prepare' && + events_emitted[1] === 'finish' && + events_emitted[2] === 'close' && + response1.headers.get('hook-called') === 'prepare' + ); + + log(group, `Finished Testing ${candidate} In ${Date.now() - start_time}ms\n`); +} + +module.exports = { + test_response_object: test_response_object, +}; diff --git a/packages/hyper-express/tests/components/http/scenarios/middleware_double_iteration.js b/packages/hyper-express/tests/components/http/scenarios/middleware_double_iteration.js new file mode 100644 index 0000000..2867224 --- /dev/null +++ b/packages/hyper-express/tests/components/http/scenarios/middleware_double_iteration.js @@ -0,0 +1,55 @@ +const { assert_log } = require('../../../scripts/operators.js'); +const { HyperExpress, fetch, server } = require('../../../configuration.js'); +const router = new HyperExpress.Router(); +const endpoint = '/tests/request'; +const scenario_endpoint = '/middleware-double-iteration'; +const endpoint_url = server.base + endpoint + scenario_endpoint; + +// This middleware should only run on this endpoint +const double_iteration_middleware = async (request, response, next) => { + // Bind an artificial error handler so we don't treat this as uncaught error + request.expected_error = () => response.status(501).send('DOUBLE_ITERATION_VIOLATION'); + + // Since this is an async callback, calling next and the async callback resolving will trigger a double iteration violation + next(); +}; + +const delay_middleware = (request, response, next) => setTimeout(next, 10); + +// Create Backend HTTP Route +router.get( + scenario_endpoint, + double_iteration_middleware, + [delay_middleware], // This weird parameter pattern is to test Express.js compatibility pattern for providing multiple middlewares through parameters/arrays + { + max_body_length: 1024 * 1024 * 10, + middlewares: [delay_middleware], + }, + async (request, response) => { + return response.send('Good'); + } +); + +// Bind router to webserver +const { TEST_SERVER } = require('../../Server.js'); +TEST_SERVER.use(endpoint, router); + +async function test_middleware_double_iteration() { + const group = 'REQUEST'; + const candidate = 'HyperExpress.Request'; + + // Perform fetch request + const response = await fetch(endpoint_url); + const body = await response.text(); + + // Test to see error handler was properly called on expected middleware error + assert_log( + group, + `${candidate} Middleware Double Iteration Violation`, + () => response.status === 501 && body === 'DOUBLE_ITERATION_VIOLATION' + ); +} + +module.exports = { + test_middleware_double_iteration, +}; diff --git a/packages/hyper-express/tests/components/http/scenarios/middleware_dynamic_iteration.js b/packages/hyper-express/tests/components/http/scenarios/middleware_dynamic_iteration.js new file mode 100644 index 0000000..35ab38a --- /dev/null +++ b/packages/hyper-express/tests/components/http/scenarios/middleware_dynamic_iteration.js @@ -0,0 +1,108 @@ +const { assert_log } = require('../../../scripts/operators.js'); +const { HyperExpress, fetch, server } = require('../../../configuration.js'); +const router = new HyperExpress.Router(); +const endpoint = '/tests/request'; +const scenario_endpoint = '/middleware-dynamic-iteration'; +const endpoint_url = server.base + endpoint + scenario_endpoint; +const { TEST_SERVER } = require('../../Server.js'); + +// Bind a global middleware which is a wildcard for a path that has no existing routes +// This middleware will apply to the not found handler +const global_wildcard_middleware = (request, response, next) => { + // Check if dynamic middleware is enabled + if (request.headers['x-dynamic-middleware'] === 'true') { + return response.send('GLOBAL_WILDCARD'); + } + + // Call next middleware + next(); +}; + +TEST_SERVER.use('/global-wildcard/', global_wildcard_middleware); + +// Bind a global middleware which is a wildcard for a path that is on an existing route path +const route_specific_dynamic_middleware = (request, response, next) => { + if (request.headers['x-dynamic-middleware'] === 'true') { + response.send('ROUTE_SPECIFIC_WILDCARD'); + } + + // Call next middleware + next(); +}; +router.use('/middleware-dynamic-iteration/middleware', route_specific_dynamic_middleware); + +// Bind a middleware which will try target an incomplete part of the path and should not be executed +const incomplete_path_middleware = (request, response, next) => { + // This should never be executed + console.log('INCOMPLETE_PATH_MIDDLEWARE'); + return response.send('INCOMPLETE_PATH_MIDDLEWARE'); +}; +router.use('/middleware-dy', incomplete_path_middleware); // Notice how "/middleware-dy" should not match "/middleware-dynamic-iteration/..." + +// Create Backend HTTP Route +router.get(scenario_endpoint + '/*', async (request, response) => { + response.send('ROUTE_HANDLER'); +}); + +// Bind router to webserver +TEST_SERVER.use(endpoint, router); + +async function test_middleware_dynamic_iteration() { + const group = 'REQUEST'; + const candidate = 'HyperExpress.Request'; + + // Make a fetch request to a random path that will not be found + const not_found_response = await fetch(server.base + '/not-found/' + Math.random(), { + headers: { + 'x-dynamic-middleware': 'true', + }, + }); + + // Assert that we received a 404 response + assert_log(group, `${candidate} Unhandled Middleware Iteration`, () => not_found_response.status === 404); + + // Make a fetch request to a global not found path on the global wildcard pattern + const global_response = await fetch(server.base + '/global-wildcard/' + Math.random(), { + headers: { + 'x-dynamic-middleware': 'true', + }, + }); + const global_text = await global_response.text(); + + // Assert that the global wildcard middleware was executed + assert_log(group, `${candidate} Global Dynamic Middleware Iteration`, () => global_text === 'GLOBAL_WILDCARD'); + + // Make a fetch request to a path that has a route with a wildcard middleware + const route_specific_response = await fetch(endpoint_url + '/middleware/' + Math.random(), { + headers: { + 'x-dynamic-middleware': 'true', + }, + }); + const route_specific_text = await route_specific_response.text(); + + // Assert that the route specific wildcard middleware was executed + assert_log( + group, + `${candidate} Route-Specific Dynamic Middleware Iteration`, + () => route_specific_text === 'ROUTE_SPECIFIC_WILDCARD' + ); + + // Make a fetch request to a path that has an exact route match + const route_handler_response = await fetch(endpoint_url + '/test/random/' + Math.random(), { + headers: { + 'x-dynamic-middleware': 'true', + }, + }); + const route_handler_text = await route_handler_response.text(); + + // Assert that the route handler was executed + assert_log( + group, + `${candidate} Route-Specific Dynamic Middleware Pattern Matching Check`, + () => route_handler_text === 'ROUTE_HANDLER' + ); +} + +module.exports = { + test_middleware_dynamic_iteration, +}; diff --git a/packages/hyper-express/tests/components/http/scenarios/middleware_execution_order.js b/packages/hyper-express/tests/components/http/scenarios/middleware_execution_order.js new file mode 100644 index 0000000..3bfc387 --- /dev/null +++ b/packages/hyper-express/tests/components/http/scenarios/middleware_execution_order.js @@ -0,0 +1,103 @@ +const { assert_log } = require('../../../scripts/operators.js'); +const { HyperExpress, fetch, server } = require('../../../configuration.js'); +const router = new HyperExpress.Router(); +const endpoint = '/tests/request'; +const scenario_endpoint = '/middleware-execution-order'; +const endpoint_url = server.base + endpoint + scenario_endpoint; +const { TEST_SERVER } = require('../../Server.js'); + +// Create a middleware to bind a middleware executions array to the request object +router.use(scenario_endpoint, (request, response, next) => { + // Initialize an array to contain middleware_executions + request.middleware_executions = []; + next(); +}); + +// Create a single depth middleware +router.use(scenario_endpoint + '/one', (request, response, next) => { + request.middleware_executions.push('one'); + next(); +}); + +// Create a two depth middleware that depends on the previous middleware +router.use(scenario_endpoint + '/one/two', (request, response, next) => { + request.middleware_executions.push('one/two'); + next(); +}); + +// Create a unique single depth middleware +router.use(scenario_endpoint + '/three', (request, response, next) => { + request.middleware_executions.push('three'); + next(); +}); + +// Create a catch-all middleware to ensure execution order +router.use(scenario_endpoint, (request, response, next) => { + request.middleware_executions.push('catch-all'); + next(); +}); + +// Bind routes for each middleware to test route assignment +router.get(scenario_endpoint + '/one', (request, response) => { + request.middleware_executions.push('one/route'); + response.json(request.middleware_executions); +}); + +router.get( + scenario_endpoint + '/one/two/*', + { + max_body_length: 100 * 1e6, + }, + (request, response) => { + request.middleware_executions.push('one/two/route'); + response.json(request.middleware_executions); + } +); + +// Bind router to webserver +TEST_SERVER.use(endpoint, router); + +async function test_middleware_execution_order() { + const group = 'REQUEST'; + const candidate = 'HyperExpress.Request'; + + // Make a fetch request to just the scenario endpoint which should only trigger catch-all + const catch_all_response = await fetch(endpoint_url); + const catch_all_response_json = await catch_all_response.json(); + assert_log( + group, + `${candidate} Catch-All Middleware Execution Order`, + () => ['catch-all', 'not-found'].join(',') === catch_all_response_json.join(',') + ); + + // Make a fetch request to the single depth middleware + const single_depth_response = await fetch(endpoint_url + '/one'); + const single_depth_response_json = await single_depth_response.json(); + assert_log( + group, + `${candidate} Single Path Depth Middleware Execution Order`, + () => ['one', 'catch-all', 'one/route'].join(',') === single_depth_response_json.join(',') + ); + + // Make a fetch request to the two depth middleware that depends on the previous middleware + const two_depth_response = await fetch(endpoint_url + '/one/two/' + Math.random()); + const two_depth_response_json = await two_depth_response.json(); + assert_log( + group, + `${candidate} Double Path Depth-Dependent Middleware Execution Order`, + () => ['one', 'one/two', 'catch-all', 'one/two/route'].join(',') === two_depth_response_json.join(',') + ); + + // Make a fetch request to the unique single depth middleware + const unique_single_depth_response = await fetch(endpoint_url + '/three/' + Math.random()); + const unique_single_depth_response_json = await unique_single_depth_response.json(); + assert_log( + group, + `${candidate} Single Path Depth Unique Middleware Execution Order`, + () => ['three', 'catch-all', 'not-found'].join(',') === unique_single_depth_response_json.join(',') + ); +} + +module.exports = { + test_middleware_execution_order, +}; diff --git a/packages/hyper-express/tests/components/http/scenarios/middleware_iteration_error.js b/packages/hyper-express/tests/components/http/scenarios/middleware_iteration_error.js new file mode 100644 index 0000000..a9d860b --- /dev/null +++ b/packages/hyper-express/tests/components/http/scenarios/middleware_iteration_error.js @@ -0,0 +1,43 @@ +const { assert_log } = require('../../../scripts/operators.js'); +const { HyperExpress, fetch, server } = require('../../../configuration.js'); +const router = new HyperExpress.Router(); +const endpoint = '/tests/request'; +const scenario_endpoint = '/middleware-error'; +const endpoint_url = server.base + endpoint + scenario_endpoint; + +const middleware = (request, response, next) => { + // Bind an artificial error handler so we don't treat this as uncaught error + request.expected_error = () => response.status(501).send('MIDDLEWARE_ERROR'); + + // Assume some problem occured, so we pass an error to next + next(new Error('EXPECTED_ERROR')); +}; + +// Create Backend HTTP Route +router.get(scenario_endpoint, middleware, async (request, response) => { + return response.send('Good'); +}); + +// Bind router to webserver +const { TEST_SERVER } = require('../../Server.js'); +TEST_SERVER.use(endpoint, router); + +async function test_middleware_iteration_error() { + const group = 'REQUEST'; + const candidate = 'HyperExpress.Request'; + + // Perform fetch request + const response = await fetch(endpoint_url); + const body = await response.text(); + + // Test to see error handler was properly called on expected middleware error + assert_log( + group, + `${candidate} Middleware Thrown Iteration Error Handler`, + () => response.status === 501 && body === 'MIDDLEWARE_ERROR' + ); +} + +module.exports = { + test_middleware_iteration_error, +}; diff --git a/packages/hyper-express/tests/components/http/scenarios/middleware_layered_iteration.js b/packages/hyper-express/tests/components/http/scenarios/middleware_layered_iteration.js new file mode 100644 index 0000000..15dc3be --- /dev/null +++ b/packages/hyper-express/tests/components/http/scenarios/middleware_layered_iteration.js @@ -0,0 +1,73 @@ +const { assert_log } = require('../../../scripts/operators.js'); +const { HyperExpress, fetch, server } = require('../../../configuration.js'); +const crypto = require('crypto'); +const router = new HyperExpress.Router(); +const endpoint = '/tests/request'; +const scenario_endpoint = '/middleware-layered-iteration'; +const endpoint_url = server.base + endpoint + scenario_endpoint; + +// Create Backend HTTP Route +const options = { + max_body_length: 1024 * 1024 * 25, +}; + +// Shallow copy of options before route creation +const options_copy = { + ...options, +}; + +router.post( + scenario_endpoint, + options, + async (req, res, next) => { + req.body = await req.json(); + }, + (req, res, next) => { + res.locals.data = req.body; + next(); + }, + (req, res) => { + res.status(200).json(res.locals.data); + } +); + +// Bind router to webserver +const { TEST_SERVER } = require('../../Server.js'); +TEST_SERVER.use(endpoint, router); + +async function test_middleware_layered_iterations(iterations = 5) { + const group = 'REQUEST'; + const candidate = 'HyperExpress.Request'; + for (let iteration = 0; iteration < iterations; iteration++) { + // Generate a random payload + const payload = {}; + for (let i = 0; i < 10; i++) { + payload[crypto.randomUUID()] = crypto.randomUUID(); + } + + // Perform fetch request + const response = await fetch(endpoint_url, { + method: 'POST', + body: JSON.stringify(payload), + }); + const body = await response.json(); + + // Test to see error handler was properly called on expected middleware error + assert_log( + group, + `${candidate} Middleware Layered Iterations Test #${iteration + 1}`, + () => JSON.stringify(payload) === JSON.stringify(body) + ); + } + + // Test to see that the provided options object was not modified + assert_log( + group, + `${candidate} Middleware Provided Object Immutability Test`, + () => JSON.stringify(options) === JSON.stringify(options_copy) + ); +} + +module.exports = { + test_middleware_layered_iterations, +}; diff --git a/packages/hyper-express/tests/components/http/scenarios/middleware_uncaught_async_error.js b/packages/hyper-express/tests/components/http/scenarios/middleware_uncaught_async_error.js new file mode 100644 index 0000000..abd1475 --- /dev/null +++ b/packages/hyper-express/tests/components/http/scenarios/middleware_uncaught_async_error.js @@ -0,0 +1,43 @@ +const { assert_log } = require('../../../scripts/operators.js'); +const { HyperExpress, fetch, server } = require('../../../configuration.js'); +const router = new HyperExpress.Router(); +const endpoint = '/tests/request'; +const scenario_endpoint = '/middleware-uncaught-async-error'; +const endpoint_url = server.base + endpoint + scenario_endpoint; + +const middleware = async (request, response, next) => { + // Bind an artificial error handler so we don't treat this as uncaught error + request.expected_error = () => response.status(501).send('MIDDLEWARE_ERROR'); + + // Assume some problem occured, so we pass an error to next + throw new Error('EXPECTED_ERROR'); +}; + +// Create Backend HTTP Route +router.get(scenario_endpoint, middleware, async (request, response) => { + return response.send('Good'); +}); + +// Bind router to webserver +const { TEST_SERVER } = require('../../Server.js'); +TEST_SERVER.use(endpoint, router); + +async function test_middleware_uncaught_async_error() { + const group = 'REQUEST'; + const candidate = 'HyperExpress.Request'; + + // Perform fetch request + const response = await fetch(endpoint_url); + const body = await response.text(); + + // Test to see error handler was properly called on expected middleware error + assert_log( + group, + `${candidate} Middleware Thrown Iteration Error Handler`, + () => response.status === 501 && body === 'MIDDLEWARE_ERROR' + ); +} + +module.exports = { + test_middleware_uncaught_async_error, +}; diff --git a/packages/hyper-express/tests/components/http/scenarios/request_body_echo_test.js b/packages/hyper-express/tests/components/http/scenarios/request_body_echo_test.js new file mode 100644 index 0000000..66842f4 --- /dev/null +++ b/packages/hyper-express/tests/components/http/scenarios/request_body_echo_test.js @@ -0,0 +1,59 @@ +const crypto = require('crypto'); +const { assert_log } = require('../../../scripts/operators.js'); +const { HyperExpress, fetch, server } = require('../../../configuration.js'); +const router = new HyperExpress.Router(); +const endpoint = '/tests/request'; +const scenario_endpoint = '/json-body-echo'; +const endpoint_url = server.base + endpoint + scenario_endpoint; + +// Create Backend HTTP Route +router.post( + scenario_endpoint, + async (req) => { + req.body = await req.json(); + return; + }, + (req, res, next) => { + res.locals.data = req.body; + next(); + }, + (_, res) => { + res.status(200).json(res.locals.data); + } +); + +// Bind router to webserver +const { TEST_SERVER } = require('../../Server.js'); +TEST_SERVER.use(endpoint, router); + +async function test_request_body_echo_test(iterations = 5) { + const group = 'REQUEST'; + const candidate = 'HyperExpress.Request.json()'; + + for (let i = 0; i < iterations; i++) { + // Generate a small random payload + const payload = { + foo: crypto.randomBytes(5).toString('hex'), + }; + + // Make the fetch request + const response = await fetch(endpoint_url, { + method: 'POST', + body: JSON.stringify(payload), + }); + + // Retrieve the JSON response body + const body = await response.json(); + + // Assert that the payload and response body are the same + assert_log( + group, + `${candidate} JSON Small Body Echo Test #${i + 1}`, + () => JSON.stringify(payload) === JSON.stringify(body) + ); + } +} + +module.exports = { + test_request_body_echo_test, +}; diff --git a/packages/hyper-express/tests/components/http/scenarios/request_chunked_json.js b/packages/hyper-express/tests/components/http/scenarios/request_chunked_json.js new file mode 100644 index 0000000..fc48dbf --- /dev/null +++ b/packages/hyper-express/tests/components/http/scenarios/request_chunked_json.js @@ -0,0 +1,43 @@ +const path = require('path'); +const fs = require('fs'); +const { assert_log } = require('../../../scripts/operators.js'); +const { HyperExpress, fetch, server } = require('../../../configuration.js'); +const router = new HyperExpress.Router(); +const endpoint = '/tests/request'; +const scenario_endpoint = '/chunked-json'; +const endpoint_url = server.base + endpoint + scenario_endpoint; +const test_file_path = path.resolve(path.join(__dirname, '../../../content/test-body.json')); + +// Create Backend HTTP Route +router.post(scenario_endpoint, async (request, response) => { + const body = await request.json(); + return response.json(body); +}); + +// Bind router to webserver +const { TEST_SERVER } = require('../../Server.js'); +TEST_SERVER.use(endpoint, router); + +async function test_request_chunked_json() { + const group = 'REQUEST'; + const candidate = 'HyperExpress.Request.json()'; + + // Send a buffer of the file in the request body so we have a content-length on server side + const expected_json = JSON.stringify(JSON.parse(fs.readFileSync(test_file_path).toString('utf8'))); + const json_stream_response = await fetch(endpoint_url, { + method: 'POST', + headers: { + 'transfer-encoding': 'chunked', + 'x-file-name': 'request_upload_body.json', + }, + body: fs.createReadStream(test_file_path), + }); + + // Validate the hash uploaded on the server side with the expected hash from client side + const uploaded_json = await json_stream_response.text(); + assert_log(group, `${candidate} Chunked Transfer JSON Upload Test`, () => expected_json === uploaded_json); +} + +module.exports = { + test_request_chunked_json, +}; diff --git a/packages/hyper-express/tests/components/http/scenarios/request_chunked_stream.js b/packages/hyper-express/tests/components/http/scenarios/request_chunked_stream.js new file mode 100644 index 0000000..3424536 --- /dev/null +++ b/packages/hyper-express/tests/components/http/scenarios/request_chunked_stream.js @@ -0,0 +1,72 @@ +const path = require('path'); +const crypto = require('crypto'); +const fs = require('fs'); +const { assert_log } = require('../../../scripts/operators.js'); +const { HyperExpress, fetch, server } = require('../../../configuration.js'); +const router = new HyperExpress.Router(); +const endpoint = '/tests/request'; +const scenario_endpoint = '/chunked-stream'; +const endpoint_url = server.base + endpoint + scenario_endpoint; +const test_file_path = path.resolve(path.join(__dirname, '../../../content/large-image.jpg')); +const test_file_stats = fs.statSync(test_file_path); + +function get_file_write_path(file_name) { + return path.resolve(path.join(__dirname, '../../../content/written/' + file_name)); +} + +// Create Backend HTTP Route +router.post(scenario_endpoint, async (request, response) => { + // Create a writable stream to specified file name path + const file_name = request.headers['x-file-name']; + const path = get_file_write_path(file_name); + const writable = fs.createWriteStream(path); + + // Pipe the readable body stream to the writable and wait for it to finish + request.pipe(writable); + await new Promise((resolve) => writable.once('finish', resolve)); + + // Read the written file's buffer and calculate its md5 hash + const written_buffer = fs.readFileSync(path); + const written_hash = crypto.createHash('md5').update(written_buffer).digest('hex'); + + // Cleanup the written file for future testing + fs.rmSync(path); + + // Return the written hash to be validated on client side + return response.json({ + hash: written_hash, + }); +}); + +// Bind router to webserver +const { TEST_SERVER } = require('../../Server.js'); +TEST_SERVER.use(endpoint, router); + +async function test_request_chunked_stream() { + const group = 'REQUEST'; + const candidate = 'HyperExpress.Request.stream'; + + // Send a buffer of the file in the request body so we have a content-length on server side + const expected_buffer = fs.readFileSync(test_file_path); + const expected_hash = crypto.createHash('md5').update(expected_buffer).digest('hex'); + const buffer_upload_response = await fetch(endpoint_url, { + method: 'POST', + headers: { + 'transfer-encoding': 'chunked', + 'x-file-name': 'request_upload_buffer.jpg', + }, + body: fs.createReadStream(test_file_path), + }); + + // Validate the hash uploaded on the server side with the expected hash from client side + const buffer_upload_body = await buffer_upload_response.json(); + assert_log( + group, + `${candidate} Chunked Transfer Piped Upload With Content Length - ${expected_hash} === ${buffer_upload_body.hash} - ${test_file_stats.size} bytes`, + () => expected_hash === buffer_upload_body.hash + ); +} + +module.exports = { + test_request_chunked_stream, +}; diff --git a/packages/hyper-express/tests/components/http/scenarios/request_multipart.js b/packages/hyper-express/tests/components/http/scenarios/request_multipart.js new file mode 100644 index 0000000..4d9f16b --- /dev/null +++ b/packages/hyper-express/tests/components/http/scenarios/request_multipart.js @@ -0,0 +1,214 @@ +const path = require('path'); +const FormData = require('form-data'); +const crypto = require('crypto'); +const fs = require('fs'); +const { assert_log } = require('../../../scripts/operators.js'); +const { HyperExpress, fetch, server } = require('../../../configuration.js'); +const router = new HyperExpress.Router(); +const endpoint = '/tests/request'; +const scenario_endpoint = '/multipart-form'; +const endpoint_url = server.base + endpoint + scenario_endpoint; + +function md5_from_stream(stream) { + return new Promise((resolve, reject) => { + const hash = crypto.createHash('md5'); + stream.on('data', (chunk) => hash.update(chunk)); + stream.on('end', () => resolve(hash.digest('hex'))); + }); +} + +function md5_from_buffer(buffer) { + return crypto.createHash('md5').update(buffer).digest('hex'); +} + +// Create Backend HTTP Route +router.post(scenario_endpoint, async (request, response) => { + const fields = []; + const ignore_fields = request.headers['ignore-fields'].split(','); + const use_async_handler = request.headers['x-use-async-handler'] === 'true'; + const async_handler = async (field) => { + // Throw a simulated error if the field name is in the ignore list + const simulated_error = request.headers['x-simulate-error'] === 'true'; + if (simulated_error) throw new Error('SIMULATED_ERROR'); + + // Do not process fields which should be ignored + if (ignore_fields.includes(field)) return; + + // Increment the cursor and store locally + const object = { + name: field.name, + value: field.value, + }; + + // Perform integrity verification if this field is a file + if (field.file) { + object.file_name = field.file.name; + object.hash = await md5_from_stream(field.file.stream); + } + + // Store the object into the server fields array for client side + fields.push(object); + }; + + let cursor = -1; + let in_flight = 0; + const sync_handler = (field) => { + // Increment and remember current iteration's cursor + cursor++; + const position = cursor; + const object = { + name: field.name, + value: field.value, + }; + + if (field.file) { + // Asynchronously calculate the md5 hash of the incoming file + in_flight++; + object.file_name = field.file.name; + md5_from_stream(field.file.stream).then((hash) => { + // Decrement the in flight counter and store hash into the server field object + in_flight--; + object.hash = hash; + fields[position] = object; + + // Send response if no more operations in flight + if (in_flight < 1) response.json(fields); + }); + } else { + // Store the server fields into fields object + // Send response if no operations are in flight + fields[position] = object; + } + }; + + // Handle the incoming fields as multipart with the appropriate handler type + try { + await request.multipart(use_async_handler ? async_handler : sync_handler); + } catch (error) { + // Pipe errors back to the client + return response.json({ error: error.message }); + } + + // Only respond here if we are using the async handler or we have no inflight operations + if (use_async_handler || in_flight < 0) return response.json(fields); +}); + +// Bind router to webserver +const { TEST_SERVER } = require('../../Server.js'); +TEST_SERVER.use(endpoint, router); + +function get_asset_buffer(file_name) { + return fs.readFileSync(path.resolve(path.join(__dirname, '../../../content/' + file_name))); +} + +async function test_request_multipart(use_async_handler = false) { + const group = 'REQUEST'; + const candidate = 'HyperExpress.Request.multipart()'; + + const ignore_fields = ['file3']; + const fields = [ + { + name: 'field1', + value: 'field1', + }, + { + name: 'file1', + value: get_asset_buffer('example.txt'), + file_name: 'example.txt', + }, + { + name: 'file2', + value: get_asset_buffer('large-image.jpg'), + }, + { + name: 'file3', + value: get_asset_buffer('test.html'), + file_name: 'something.html', + }, + { + name: 'field2', + value: Math.random().toString(), + }, + ].map((field) => { + if (field.value instanceof Buffer) field.hash = md5_from_buffer(field.value); + return field; + }); + + // Perform a multipart form request that uploads files and fields + const form = new FormData(); + fields.forEach(({ name, value, file_name }) => form.append(name, value, file_name)); + + // Perform multipart uploading with a synchronous handler + const response = await fetch(endpoint_url, { + method: 'POST', + body: form, + headers: { + 'ignore-fields': ignore_fields.join(','), + 'x-use-async-handler': use_async_handler.toString(), + }, + }); + const server_fields = await response.json(); + + // Assert comparison of each field in order to match with client-side from server-side + for (let i = 0; i < fields.length; i++) { + const client_field = fields[i]; + const server_field = server_fields[i]; + + // Only perform assertion if we are not ignoring this field + if (!ignore_fields.includes(client_field.name)) + assert_log( + group, + `${candidate} - Multipart Form Field/File Upload Test (${ + use_async_handler ? 'Asynchronous' : 'Synchronous' + } Handler) - ${client_field.name} - ${client_field.value.length} bytes`, + () => { + // Assert that the field names match + if (client_field.name !== server_field.name) return false; + + // Asser that the field values match if this is a non file type field + if (typeof client_field.value == 'string' && client_field.value !== server_field.value) + return false; + + // Assert that the file names match if it was supplied + if (client_field.file_name && client_field.file_name !== server_field.file_name) return false; + + // Assert the file hashes match if this is a file type field + if (client_field.value instanceof Buffer && client_field.hash !== server_field.hash) return false; + + return true; + } + ); + } + + // Perform simulated error test for only async handler + if (use_async_handler) { + // Create a new form with a random value + const test_form = new FormData(); + test_form.append('field1', 'field1'); + + // Perform multipart uploading with a simulated error + const response = await fetch(endpoint_url, { + method: 'POST', + body: test_form, + headers: { + 'ignore-fields': ignore_fields.join(','), + 'x-use-async-handler': use_async_handler.toString(), + 'x-simulate-error': 'true', + }, + }); + + // Assert that the error was thrown + const { error } = await response.json(); + assert_log( + group, + `${candidate} - Multipart Form Field/File Upload Test (${ + use_async_handler ? 'Asynchronous' : 'Synchronous' + } Handler) - Handler Simulated Error Test`, + () => error === 'SIMULATED_ERROR' + ); + } +} + +module.exports = { + test_request_multipart, +}; diff --git a/packages/hyper-express/tests/components/http/scenarios/request_router_paths_test.js b/packages/hyper-express/tests/components/http/scenarios/request_router_paths_test.js new file mode 100644 index 0000000..6ed7986 --- /dev/null +++ b/packages/hyper-express/tests/components/http/scenarios/request_router_paths_test.js @@ -0,0 +1,44 @@ +const crypto = require('crypto'); +const { assert_log } = require('../../../scripts/operators.js'); +const { HyperExpress, fetch, server } = require('../../../configuration.js'); +const router = new HyperExpress.Router(); +const endpoint = '/tests/request'; +const scenario_endpoint = '/cached-paths'; +const endpoint_url = server.base + endpoint + scenario_endpoint; + +// Create Backend HTTP Route to echo the path of the request +router.get(scenario_endpoint, (req, res) => res.send(req.path)); +router.get(scenario_endpoint + '/:random', (req, res) => res.send(req.path)); + +// Bind router to webserver +const { TEST_SERVER } = require('../../Server.js'); +TEST_SERVER.use(endpoint, router); + +async function test_request_router_paths_test() { + const group = 'REQUEST'; + const candidate = 'HyperExpress.Request.path'; + + // Test the candidates to ensure that the path is being cached properly + const _candidates = []; + const candidates = [ + endpoint_url, + `${endpoint_url}/${crypto.randomUUID()}`, + `${endpoint_url}/${crypto.randomUUID()}`, + ]; + for (const candidate of candidates) { + const response = await fetch(candidate); + const _candidate = await response.text(); + _candidates.push(_candidate); + } + + // Assert that the candidates match + assert_log( + group, + `${candidate} Cached Router Paths Test`, + () => _candidates.join(',') === candidates.map((url) => url.replace(server.base, '')).join(',') + ); +} + +module.exports = { + test_request_router_paths_test, +}; diff --git a/packages/hyper-express/tests/components/http/scenarios/request_stream.js b/packages/hyper-express/tests/components/http/scenarios/request_stream.js new file mode 100644 index 0000000..f1609cf --- /dev/null +++ b/packages/hyper-express/tests/components/http/scenarios/request_stream.js @@ -0,0 +1,71 @@ +const path = require('path'); +const crypto = require('crypto'); +const fs = require('fs'); +const { assert_log } = require('../../../scripts/operators.js'); +const { HyperExpress, fetch, server } = require('../../../configuration.js'); +const router = new HyperExpress.Router(); +const endpoint = '/tests/request'; +const scenario_endpoint = '/stream-pipe'; +const endpoint_url = server.base + endpoint + scenario_endpoint; +const test_file_path = path.resolve(path.join(__dirname, '../../../content/large-image.jpg')); +const test_file_stats = fs.statSync(test_file_path); + +function get_file_write_path(file_name) { + return path.resolve(path.join(__dirname, '../../../content/written/' + file_name)); +} + +// Create Backend HTTP Route +router.post(scenario_endpoint, async (request, response) => { + // Create a writable stream to specified file name path + const file_name = request.headers['x-file-name']; + const path = get_file_write_path(file_name); + const writable = fs.createWriteStream(path); + + // Pipe the readable body stream to the writable and wait for it to finish + request.pipe(writable); + await new Promise((resolve) => writable.once('finish', resolve)); + + // Read the written file's buffer and calculate its md5 hash + const written_buffer = fs.readFileSync(path); + const written_hash = crypto.createHash('md5').update(written_buffer).digest('hex'); + + // Cleanup the written file for future testing + fs.rmSync(path); + + // Return the written hash to be validated on client side + return response.json({ + hash: written_hash, + }); +}); + +// Bind router to webserver +const { TEST_SERVER } = require('../../Server.js'); +TEST_SERVER.use(endpoint, router); + +async function test_request_stream_pipe() { + const group = 'REQUEST'; + const candidate = 'HyperExpress.Request.stream'; + + // Send a buffer of the file in the request body so we have a content-length on server side + const expected_buffer = fs.readFileSync(test_file_path); + const expected_hash = crypto.createHash('md5').update(expected_buffer).digest('hex'); + const buffer_upload_response = await fetch(endpoint_url, { + method: 'POST', + headers: { + 'x-file-name': 'request_upload_buffer.jpg', + }, + body: expected_buffer, + }); + + // Validate the hash uploaded on the server side with the expected hash from client side + const buffer_upload_body = await buffer_upload_response.json(); + assert_log( + group, + `${candidate} Piped Upload With Content Length - ${expected_hash} === ${buffer_upload_body.hash} - ${test_file_stats.size} bytes`, + () => expected_hash === buffer_upload_body.hash + ); +} + +module.exports = { + test_request_stream_pipe, +}; diff --git a/packages/hyper-express/tests/components/http/scenarios/request_uncaught_rejections.js b/packages/hyper-express/tests/components/http/scenarios/request_uncaught_rejections.js new file mode 100644 index 0000000..62d1213 --- /dev/null +++ b/packages/hyper-express/tests/components/http/scenarios/request_uncaught_rejections.js @@ -0,0 +1,85 @@ +const { assert_log } = require('../../../scripts/operators.js'); +const { HyperExpress, fetch, server } = require('../../../configuration.js'); +const router = new HyperExpress.Router(); +const endpoint = '/tests/request'; +const scenario_endpoint = '/uncaught-rejection'; +const endpoint_url = server.base + endpoint + scenario_endpoint; + +// Create Backend HTTP Route +router.post(scenario_endpoint, async (request, response) => { + // Retrieve the desired scenario from the request body + const { scenario } = await request.json(); + + // Bind an expected error handler + request.expected_error = (error) => + response.json({ + code: error.message, + }); + + // Trigger a specific error scenario + switch (scenario) { + case 1: + // Manually throw a shallow error + throw new Error('MANUAL_SHALLOW_ERROR'); + case 2: + // Manually throw a deep error + await new Promise((_, reject) => reject(new Error('MANUAL_DEEP_ERROR'))); + case 3: + // Manually thrown non-Error object + throw 'MANUAL_SHALLOW_NON_ERROR'; + case 4: + // Manually thrown non-Error object + await (async () => { + throw 'MANUAL_DEEP_NON_ERROR'; + })(); + default: + return response.json({ + code: 'SUCCESS', + }); + } +}); + +// Bind router to webserver +const { TEST_SERVER } = require('../../Server.js'); +TEST_SERVER.use(endpoint, router); + +async function test_request_uncaught_rejections() { + const group = 'REQUEST'; + const candidate = 'HyperExpress.Request'; + const promises = [ + [1, 'MANUAL_SHALLOW_ERROR'], + [2, 'MANUAL_DEEP_ERROR'], + [3, 'ERR_CAUGHT_NON_ERROR_TYPE: MANUAL_SHALLOW_NON_ERROR'], + [4, 'ERR_CAUGHT_NON_ERROR_TYPE: MANUAL_DEEP_NON_ERROR'], + ].map( + ([scenario, expected_code]) => + new Promise(async (resolve) => { + // Make the fetch request + const response = await fetch(endpoint_url, { + method: 'POST', + body: JSON.stringify({ + scenario, + }), + }); + + // Retrieve the received code from the server + const { code } = await response.json(); + + // Validate the hash uploaded on the server side with the expected hash from client side + assert_log( + group, + `${candidate} Uncaught Rejections Test Scenario ${scenario} => ${code}`, + () => code === expected_code + ); + + // Release this promise + resolve(); + }) + ); + + await Promise.all(promises); +} + +module.exports = { + test_request_uncaught_rejections, +}; diff --git a/packages/hyper-express/tests/components/http/scenarios/response_chunked_write.js b/packages/hyper-express/tests/components/http/scenarios/response_chunked_write.js new file mode 100644 index 0000000..8c6cc87 --- /dev/null +++ b/packages/hyper-express/tests/components/http/scenarios/response_chunked_write.js @@ -0,0 +1,77 @@ +const path = require('path'); +const crypto = require('crypto'); +const fs = require('fs'); +const { Writable } = require('stream'); +const { assert_log } = require('../../../scripts/operators.js'); +const { HyperExpress, fetch, server } = require('../../../configuration.js'); +const router = new HyperExpress.Router(); +const endpoint = '/tests/response'; +const scenario_endpoint = '/write'; +const endpoint_url = server.base + endpoint + scenario_endpoint; +const test_file_path = path.resolve(path.join(__dirname, '../../../content/large-image.jpg')); +const test_file_stats = fs.statSync(test_file_path); + +function safe_write_chunk(response, chunk, callback) { + return response.write(chunk, 'utf8', callback); +} + +// Create Backend HTTP Route +router.get(scenario_endpoint, async (request, response) => { + // Set some headers to ensure we have proper headers being received + response.header('x-is-written', 'true'); + + // Create a readable stream for test file and stream it + const readable = fs.createReadStream(test_file_path); + + // Create a Writable which we will pipe the readable into + const writable = new Writable({ + write: (chunk, encoding, callback) => { + // Safe write a chunk until it has FULLY been served + safe_write_chunk(response, chunk, callback); + }, + }); + + // Bind event handlers for ending the request once Writable has ended or closed + writable.on('close', () => response.send()); + + // Pipe the readable into the writable we created + readable.pipe(writable); +}); + +// Bind router to webserver +const { TEST_SERVER } = require('../../Server.js'); +TEST_SERVER.use(endpoint, router); + +async function test_response_chunked_write() { + const group = 'RESPONSE'; + const candidate = 'HyperExpress.Response.write()'; + + // Read test file's buffer into memory + const expected_buffer = fs.readFileSync(test_file_path); + const expected_hash = crypto.createHash('md5').update(expected_buffer).digest('hex'); + + // Perform chunked encoding based fetch request to download streamed buffer for test file from server + const chunked_response = await fetch(endpoint_url); + + // Ensure custom headers are received first + assert_log( + group, + `${candidate} Custom Chunked Transfer Write Headers Test`, + () => chunked_response.headers.get('x-is-written') === 'true' + ); + + // Download buffer from request to compare + let received_buffer = await chunked_response.buffer(); + let received_hash = crypto.createHash('md5').update(received_buffer).digest('hex'); + + // Test to see error handler was properly called on expected middleware error + assert_log( + group, + `${candidate} Custom Chunked Transfer Write Buffer/Hash Comparison Test - ${expected_hash} - ${test_file_stats.size} bytes`, + () => expected_buffer.equals(received_buffer) && expected_hash === received_hash + ); +} + +module.exports = { + test_response_chunked_write, +}; diff --git a/packages/hyper-express/tests/components/http/scenarios/response_custom_content_length.js b/packages/hyper-express/tests/components/http/scenarios/response_custom_content_length.js new file mode 100644 index 0000000..00bea22 --- /dev/null +++ b/packages/hyper-express/tests/components/http/scenarios/response_custom_content_length.js @@ -0,0 +1,35 @@ +const crypto = require('crypto'); +const { assert_log } = require('../../../scripts/operators.js'); +const { HyperExpress, fetch, server } = require('../../../configuration.js'); +const router = new HyperExpress.Router(); +const endpoint = '/tests/response'; +const scenario_endpoint = '/custom-content-length'; +const endpoint_url = server.base + endpoint + scenario_endpoint; + +// Generate a random string payload +const payload = crypto.randomBytes(800).toString('hex'); + +// Create Backend HTTP Route +router.get(scenario_endpoint, (_, response) => { + response.header('content-length', payload.length.toString()).send(payload); +}); + +// Bind router to webserver +const { TEST_SERVER } = require('../../Server.js'); +TEST_SERVER.use(endpoint, router); + +async function test_response_custom_content_length() { + const group = 'RESPONSE'; + const candidate = 'HyperExpress.Response.send()'; + + // Send a normal request to trigger the appropriate hooks + const response = await fetch(endpoint_url); + const received = await response.text(); + + // Assert that the received headers all match the expected headers + assert_log(group, `${candidate} Custom Content-Length With Body Test`, () => received === payload); +} + +module.exports = { + test_response_custom_content_length, +}; diff --git a/packages/hyper-express/tests/components/http/scenarios/response_custom_status.js b/packages/hyper-express/tests/components/http/scenarios/response_custom_status.js new file mode 100644 index 0000000..7010688 --- /dev/null +++ b/packages/hyper-express/tests/components/http/scenarios/response_custom_status.js @@ -0,0 +1,55 @@ +const crypto = require('crypto'); +const { assert_log } = require('../../../scripts/operators.js'); +const { HyperExpress, fetch, server } = require('../../../configuration.js'); +const router = new HyperExpress.Router(); +const endpoint = '/tests/response'; +const scenario_endpoint = '/custom-status'; +const endpoint_url = server.base + endpoint + scenario_endpoint; + +// Create Backend HTTP Route +router.post(scenario_endpoint, async (request, response) => { + const { status, message } = await request.json(); + response.statusCode = status; + response.statusMessage = message; + response.send(); +}); + +// Bind router to webserver +const { TEST_SERVER } = require('../../Server.js'); +TEST_SERVER.use(endpoint, router); + +async function test_response_custom_status() { + const group = 'RESPONSE'; + const candidate = 'HyperExpress.Response.statusCode'; + + [ + { + status: 200, + message: 'Some Message', + }, + { + status: 609, + message: 'User Moved to Another Server', + }, + ].map(async ({ status, message }) => { + // Make a request to the server with a custom status code and message + const response = await fetch(endpoint_url, { + method: 'POST', + body: JSON.stringify({ + status, + message, + }), + }); + + // Validate the status code and message on the response + assert_log( + group, + `${candidate} Custom Status Code & Response Test - "HTTP ${status} ${message}"`, + () => response.status === status && response.statusText === message + ); + }); +} + +module.exports = { + test_response_custom_status, +}; diff --git a/packages/hyper-express/tests/components/http/scenarios/response_headers_behavior.js b/packages/hyper-express/tests/components/http/scenarios/response_headers_behavior.js new file mode 100644 index 0000000..5fdd57c --- /dev/null +++ b/packages/hyper-express/tests/components/http/scenarios/response_headers_behavior.js @@ -0,0 +1,122 @@ +const { assert_log } = require('../../../scripts/operators.js'); +const { HyperExpress, fetch, server } = require('../../../configuration.js'); +const router = new HyperExpress.Router(); +const endpoint = '/tests/response'; +const scenario_endpoint = '/headers-behavior'; +const endpoint_url = server.base + endpoint + scenario_endpoint; + +const RAW_HEADERS = [ + { + name: 'test', + value: 'first', // This will be overwritten by the second header + }, + { + name: 'test', + value: 'second', // This will be overwritten by the third header + }, + { + name: 'test', + value: 'third', // This will be the served header for the the "test" header + }, +]; + +const RAW_COOKIES = [ + { + name: 'test-cookie', + value: 'test-value', // This will be overwritten by the second cookie + }, + { + name: 'test-cookie', + value: 'test-value-2', // This will be served to the client + }, + { + name: 'test-cookie-3', + value: 'test-value-3', // This will be served to the client + }, +]; + +// Create Backend HTTP Route +router.get(scenario_endpoint, (request, response) => { + // Serve the headers + RAW_HEADERS.forEach((header) => response.header(header.name, header.value)); + + // Serve the cookies + RAW_COOKIES.forEach((cookie) => response.cookie(cookie.name, cookie.value, 1000 * 60 * 60 * 24 * 7)); + + // Send response + response.send(); +}); + +// Bind router to webserver +const { TEST_SERVER } = require('../../Server.js'); +TEST_SERVER.use(endpoint, router); + +async function test_response_headers_behavior() { + const group = 'RESPONSE'; + const candidate = 'HyperExpress.Response.header()'; + + // Parse the last written header as the expected value + const EXPECTED_HEADERS = {}; + RAW_HEADERS.forEach((header) => { + if (EXPECTED_HEADERS[header.name]) { + if (Array.isArray(EXPECTED_HEADERS[header.name])) { + EXPECTED_HEADERS[header.name].push(header.value); + } else { + EXPECTED_HEADERS[header.name] = [EXPECTED_HEADERS[header.name], header.value]; + } + } else { + EXPECTED_HEADERS[header.name] = header.value; + } + }); + + // Join all multi headers with comma whitespaces + for (const name in EXPECTED_HEADERS) { + if (Array.isArray(EXPECTED_HEADERS[name])) { + EXPECTED_HEADERS[name] = EXPECTED_HEADERS[name].join(', '); + } + } + + // Parse the last written cookie as the expected value + const EXPECTED_COOKIES = {}; + RAW_COOKIES.forEach((cookie) => (EXPECTED_COOKIES[cookie.name] = cookie.value)); + + // Send a fetch request to retrieve headers + const response = await fetch(endpoint_url); + const received_headers = response.headers.raw(); + + // Assert that the headers were served correctly + assert_log(group, `${candidate} - Single/Multiple Header Values Behavior Test`, () => { + let valid = true; + Object.keys(EXPECTED_HEADERS).forEach((name) => { + let expected = EXPECTED_HEADERS[name]; + let received = received_headers[name]; + + // Assert that the received header is an array + valid = Array.isArray(expected) + ? JSON.stringify(expected) === JSON.stringify(received) + : expected === received[0]; + }); + return valid; + }); + + // Assert that the cookies were served correctly + assert_log(group, `${candidate} - Single/Multiple Cookie Values Behavior Test`, () => { + const received_cookies = {}; + received_headers['set-cookie'].forEach((cookie) => { + const [name, value] = cookie.split('; ')[0].split('='); + received_cookies[name] = value; + }); + + let valid = true; + Object.keys(EXPECTED_COOKIES).forEach((name) => { + const expected_value = EXPECTED_COOKIES[name]; + const received_value = received_cookies[name]; + valid = expected_value === received_value; + }); + return valid; + }); +} + +module.exports = { + test_response_headers_behavior, +}; diff --git a/packages/hyper-express/tests/components/http/scenarios/response_hooks.js b/packages/hyper-express/tests/components/http/scenarios/response_hooks.js new file mode 100644 index 0000000..821ef24 --- /dev/null +++ b/packages/hyper-express/tests/components/http/scenarios/response_hooks.js @@ -0,0 +1,68 @@ +const { assert_log, async_wait } = require('../../../scripts/operators.js'); +const { HyperExpress, fetch, server, AbortController } = require('../../../configuration.js'); +const router = new HyperExpress.Router(); +const endpoint = '/tests/response'; +const scenario_endpoint = '/hooks'; +const endpoint_url = server.base + endpoint + scenario_endpoint; +const response_delay = 100; + +const hook_emissions = {}; +function increment_event(type) { + hook_emissions[type] = hook_emissions[type] ? hook_emissions[type] + 1 : 1; +} + +// Create Backend HTTP Route +router.get(scenario_endpoint, (request, response) => { + // Bind all of the hooks to the response + ['abort', 'prepare', 'finish', 'close'].forEach((type) => response.on(type, () => increment_event(type))); + + // Send response after some delay to allow for client to prematurely abort + setTimeout(() => (!response.completed ? response.send() : null), response_delay); +}); + +// Bind router to webserver +const { TEST_SERVER } = require('../../Server.js'); +TEST_SERVER.use(endpoint, router); + +async function test_response_events() { + const group = 'RESPONSE'; + const candidate = 'HyperExpress.Response.on()'; + + // Send a normal request to trigger the appropriate hooks + await fetch(endpoint_url); + + // Assert that only the appropriate hooks were called + assert_log( + group, + `${candidate} - Normal Request Events Test`, + () => hook_emissions['prepare'] === 1 && hook_emissions['finish'] === 1 && hook_emissions['close'] === 1 + ); + + // Send and prematurely abort a request to trigger the appropriate hooks + const controller = new AbortController(); + setTimeout(() => controller.abort(), response_delay / 3); + try { + await fetch(endpoint_url, { + signal: controller.signal, + }); + } catch (error) { + // Supress the error as we expect an abort + // Wait a little bit for the hook emissions to be updated + await async_wait(response_delay / 3); + } + + // Assert that only the appropriate hooks were called + assert_log( + group, + `${candidate} - Premature Aborted Request Events Test`, + () => + hook_emissions['prepare'] === 1 && + hook_emissions['finish'] === 1 && + hook_emissions['close'] === 2 && + hook_emissions['abort'] === 1 + ); +} + +module.exports = { + test_response_events, +}; diff --git a/packages/hyper-express/tests/components/http/scenarios/response_piped.js b/packages/hyper-express/tests/components/http/scenarios/response_piped.js new file mode 100644 index 0000000..ab37dec --- /dev/null +++ b/packages/hyper-express/tests/components/http/scenarios/response_piped.js @@ -0,0 +1,61 @@ +const path = require('path'); +const crypto = require('crypto'); +const fs = require('fs'); +const { assert_log } = require('../../../scripts/operators.js'); +const { HyperExpress, fetch, server } = require('../../../configuration.js'); +const router = new HyperExpress.Router(); +const endpoint = '/tests/response'; +const scenario_endpoint = '/pipe'; +const endpoint_url = server.base + endpoint + scenario_endpoint; +const test_file_path = path.resolve(path.join(__dirname, '../../../content/large-image.jpg')); +const test_file_stats = fs.statSync(test_file_path); + +// Create Backend HTTP Route +router.get(scenario_endpoint, async (request, response) => { + // Set some headers to ensure we have proper headers being received + response.header('x-is-written', 'true'); + + // Create a readable stream for test file and stream it + const readable = fs.createReadStream(test_file_path); + + // Pipe the readable stream into the response + readable.pipe(response); +}); + +// Bind router to webserver +const { TEST_SERVER } = require('../../Server.js'); +TEST_SERVER.use(endpoint, router); + +async function test_response_piped_write() { + const group = 'RESPONSE'; + const candidate = 'HyperExpress.Response.write()'; + + // Read test file's buffer into memory + const expected_buffer = fs.readFileSync(test_file_path); + const expected_hash = crypto.createHash('md5').update(expected_buffer).digest('hex'); + + // Perform chunked encoding based fetch request to download streamed buffer for test file from server + const chunked_response = await fetch(endpoint_url); + + // Ensure custom headers are received first + assert_log( + group, + `${candidate} Piped Stream Write Headers Test`, + () => chunked_response.headers.get('x-is-written') === 'true' + ); + + // Download buffer from request to compare + let received_buffer = await chunked_response.buffer(); + let received_hash = crypto.createHash('md5').update(received_buffer).digest('hex'); + + // Test to see error handler was properly called on expected middleware error + assert_log( + group, + `${candidate} Piped Stream Write Buffer/Hash Comparison Test - ${expected_hash} - ${test_file_stats.size} bytes`, + () => expected_buffer.equals(received_buffer) && expected_hash === received_hash + ); +} + +module.exports = { + test_response_piped_write, +}; diff --git a/packages/hyper-express/tests/components/http/scenarios/response_send_no_body.js b/packages/hyper-express/tests/components/http/scenarios/response_send_no_body.js new file mode 100644 index 0000000..429fd31 --- /dev/null +++ b/packages/hyper-express/tests/components/http/scenarios/response_send_no_body.js @@ -0,0 +1,49 @@ +const { assert_log } = require('../../../scripts/operators.js'); +const { HyperExpress, fetch, server } = require('../../../configuration.js'); +const router = new HyperExpress.Router(); +const endpoint = '/tests/response'; +const scenario_endpoint = '/send-no-body'; +const endpoint_url = server.base + endpoint + scenario_endpoint; + +// Create Backend HTTP Route +const response_headers = [ + ['Content-Type', 'application/json'], + ['Content-Length', Math.floor(Math.random() * 1e5).toString()], + ['Last-Modified', new Date().toUTCString()], + ['ETag', 'W/"' + Math.floor(Math.random() * 1e5).toString() + '"'], +]; + +router.head(scenario_endpoint, (_, response) => { + // Write the response headers + response_headers.forEach(([key, value]) => response.header(key, value)); + + // Should send without body under the hood with the custom content-length + return response.vary('Accept-Encoding').send(); +}); + +// Bind router to webserver +const { TEST_SERVER } = require('../../Server.js'); +TEST_SERVER.use(endpoint, router); + +async function test_response_send_no_body() { + const group = 'RESPONSE'; + const candidate = 'HyperExpress.Response.send()'; + + // Send a normal request to trigger the appropriate hooks + const response = await fetch(endpoint_url, { + method: 'HEAD', + }); + + // Assert that the received headers all match the expected headers + assert_log(group, `${candidate} Custom Content-Length Without Body Test`, () => { + let verdict = true; + response_headers.forEach(([key, value]) => { + if (response.headers.get(key) !== value) verdict = false; + }); + return verdict; + }); +} + +module.exports = { + test_response_send_no_body, +}; diff --git a/packages/hyper-express/tests/components/http/scenarios/response_send_status.js b/packages/hyper-express/tests/components/http/scenarios/response_send_status.js new file mode 100644 index 0000000..68d2949 --- /dev/null +++ b/packages/hyper-express/tests/components/http/scenarios/response_send_status.js @@ -0,0 +1,49 @@ +const { assert_log } = require('../../../scripts/operators.js'); +const { HyperExpress, fetch, server } = require('../../../configuration.js'); +const router = new HyperExpress.Router(); +const endpoint = '/tests/response'; +const scenario_endpoint = '/send-status'; +const endpoint_url = server.base + endpoint + scenario_endpoint; + +// Create Backend HTTP Route +router.post(scenario_endpoint, async (request, response) => { + const { status } = await request.json(); + response.sendStatus(status); +}); + +// Bind router to webserver +const { TEST_SERVER } = require('../../Server.js'); +TEST_SERVER.use(endpoint, router); + +async function test_response_send_status() { + const group = 'RESPONSE'; + const candidate = 'HyperExpress.Response.statusCode'; + + [ + { + status: 200, + }, + { + status: 609, + }, + ].map(async ({ status }) => { + // Make a request to the server with a status code + const response = await fetch(endpoint_url, { + method: 'POST', + body: JSON.stringify({ + status, + }), + }); + + // Validate the status code on the response + assert_log( + group, + `${candidate} Custom Status Code & Response Test - "HTTP ${status}"`, + () => response.status === status + ); + }); +} + +module.exports = { + test_response_send_status, +}; diff --git a/packages/hyper-express/tests/components/http/scenarios/response_set_header.js b/packages/hyper-express/tests/components/http/scenarios/response_set_header.js new file mode 100644 index 0000000..1d7aef2 --- /dev/null +++ b/packages/hyper-express/tests/components/http/scenarios/response_set_header.js @@ -0,0 +1,35 @@ +const { assert_log } = require('../../../scripts/operators.js'); +const { fetch, server } = require('../../../configuration.js'); +const { TEST_SERVER } = require('../../Server.js'); + +const endpoint = '/tests/response/set'; +const endpoint_url = server.base + endpoint; + +// Create Backend HTTP Route +TEST_SERVER.get(endpoint, async (request, response) => { + response.set({ 'test-header-1': 'test-value-1' }); + response.set('test-header-2', 'test-value-2'); + return response.end(); +}); + +async function test_response_set_header() { + let group = 'RESPONSE'; + let candidate = 'HyperExpress.Response.set()'; + + // Perform fetch request + const response = await fetch(endpoint_url); + const headers = response.headers.raw(); + + assert_log( + group, + candidate + ' Set Header Test', + () => { + return headers['test-header-1'] == 'test-value-1' + && headers['test-header-2'] == 'test-value-2'; + } + ); +} + +module.exports = { + test_response_set_header, +}; diff --git a/packages/hyper-express/tests/components/http/scenarios/response_sse.js b/packages/hyper-express/tests/components/http/scenarios/response_sse.js new file mode 100644 index 0000000..b783d39 --- /dev/null +++ b/packages/hyper-express/tests/components/http/scenarios/response_sse.js @@ -0,0 +1,136 @@ +const { assert_log, async_wait } = require('../../../scripts/operators.js'); +const { HyperExpress, server, EventSource } = require('../../../configuration.js'); +const router = new HyperExpress.Router(); +const endpoint = '/tests/response'; +const scenario_endpoint = '/sse'; +const endpoint_url = server.base + endpoint + scenario_endpoint; + +const test_data = [ + { + data: 'asdasdasd', + }, + { + data: 'xasdxasdxasd', + }, + { + event: 'x3123x123x', + data: 'xasdasdasdxasd', + }, + { + event: '3x123x123x', + data: '123123x123x12', + }, + { + id: '3x12x123x123x', + event: 'x3123x123', + data: 'x123x123x123x123', + }, + { + data: 'x3123x123x1231', + }, +]; + +// Create Backend HTTP Route to serve test data +let sse_closed = false; +router.get(scenario_endpoint, async (request, response) => { + // Ensure SSE is available for this request + if (response.sse) { + // Open the SSE connection to ensure the client is properly connected + response.sse.open(); + + // Serve the appropriate test data after a short delay + await async_wait(5); + test_data.forEach(({ id, event, data }) => { + // Send with the appropriate parameters based on the test data + let output; + if (id && event && data) { + output = response.sse.send(id, event, data); + } else if (event && data) { + output = response.sse.send(event, data); + } else { + output = response.sse.send(data); + } + + if (!output) console.log(`Failed to send SSE message: ${id}, ${event}, ${data}`); + }); + + // Listen for the client to close the connection + response.once('abort', () => (sse_closed = true)); + response.once('close', () => (sse_closed = true)); + } +}); + +// Bind router to webserver +const { TEST_SERVER } = require('../../Server.js'); +TEST_SERVER.use(endpoint, router); + +async function test_response_sse() { + const group = 'RESPONSE'; + const candidate = 'HyperExpress.Response.sse'; + + // Open a new SSE connection to the server + const sse = new EventSource(endpoint_url); + + // Record all of the incoming events to assert against test data + const recorded_data = []; + const recorded_ids = []; + const record_event = (event, customEvent) => { + // Determine various properties about this event + const is_custom_id = Number.isNaN(+event.lastEventId); + const is_recorded_id = recorded_ids.includes(event.lastEventId); + const data = event.data; + + // Build the event based on recorded properties + const payload = {}; + if (is_custom_id && !is_recorded_id) payload.id = event.lastEventId; + if (customEvent) payload.event = customEvent; + if (data) payload.data = data; + recorded_data.push(payload); + + // Remember the event ID for future reference as the last event ID does not reset + if (is_custom_id && !is_recorded_id) recorded_ids.push(event.lastEventId); + }; + + // Bind custom event handlers from test data array + test_data.forEach(({ event }) => (event ? sse.addEventListener(event, (ev) => record_event(ev, event)) : null)); + + // Bind a catch-all message handler + sse.onmessage = record_event; + + // Wait for the connection to initially open and disconnect + let interval; + await new Promise((resolve, reject) => { + sse.onerror = reject; + + // Wait for all test data to be received + interval = setInterval(() => { + if (recorded_data.length >= test_data.length) resolve(); + }, 100); + }); + clearInterval(interval); + + // Close the connection + sse.close(); + + // Let the server propogate the boolean value + await async_wait(5); + + // Assert that all test data was received successfully + assert_log( + group, + `${candidate} - Server-Sent Events Communiciation Test`, + () => + sse_closed && + test_data.find( + (test) => + recorded_data.find( + (recorded) => + test.id === recorded.id && test.event === recorded.event && test.data === recorded.data + ) === undefined + ) === undefined + ); +} + +module.exports = { + test_response_sse, +}; diff --git a/packages/hyper-express/tests/components/http/scenarios/response_stream.js b/packages/hyper-express/tests/components/http/scenarios/response_stream.js new file mode 100644 index 0000000..824cb75 --- /dev/null +++ b/packages/hyper-express/tests/components/http/scenarios/response_stream.js @@ -0,0 +1,101 @@ +const path = require('path'); +const crypto = require('crypto'); +const fs = require('fs'); +const { assert_log } = require('../../../scripts/operators.js'); +const { HyperExpress, fetch, server } = require('../../../configuration.js'); +const router = new HyperExpress.Router(); +const endpoint = '/tests/response'; +const scenario_endpoint = '/stream'; +const endpoint_url = server.base + endpoint + scenario_endpoint; +const test_file_path = path.resolve(path.join(__dirname, '../../../content/large-image.jpg')); +const test_file_stats = fs.statSync(test_file_path); + +// Create Backend HTTP Route +router.get(scenario_endpoint, async (request, response) => { + // Set some headers to ensure we have proper headers being received + response.header('x-is-streamed', 'true'); + + // Create a readable stream for test file and stream it + const readable = fs.createReadStream(test_file_path); + + // Deliver with chunked encoding if specified by header or fall back to normal handled delivery + const use_chunked_encoding = request.headers['x-chunked-encoding'] === 'true'; + response.stream(readable, use_chunked_encoding ? undefined : test_file_stats.size); +}); + +// Bind router to webserver +const { TEST_SERVER } = require('../../Server.js'); +TEST_SERVER.use(endpoint, router); + +async function test_response_stream_method() { + const group = 'RESPONSE'; + const candidate = 'HyperExpress.Response.stream()'; + + // Read test file's buffer into memory + const expected_buffer = fs.readFileSync(test_file_path); + const expected_hash = crypto.createHash('md5').update(expected_buffer).digest('hex'); + + // Perform chunked encoding based fetch request to download streamed buffer for test file from server + const chunked_response = await fetch(endpoint_url, { + headers: { + 'x-chunked-encoding': 'true', + }, + }); + + // Ensure custom headers are received first + assert_log( + group, + `${candidate} Chunked Transfer Streamed Headers Test`, + () => chunked_response.headers.get('x-is-streamed') === 'true' + ); + + // Download buffer from request to compare + let received_buffer = await chunked_response.buffer(); + let received_hash = crypto.createHash('md5').update(received_buffer).digest('hex'); + + // Test to see error handler was properly called on expected middleware error + assert_log( + group, + `${candidate} Chunked Transfer Streamed Buffer/Hash Comparison Test - ${expected_hash} - ${test_file_stats.size} bytes`, + () => { + const matches = expected_buffer.equals(received_buffer) && expected_hash === received_hash; + if (!matches) { + console.log({ + expected_buffer, + received_buffer, + expected_hash, + received_hash, + }); + } + + return matches; + } + ); + + // Perform handled response based fetch request to download streamed buffer for test file from server + const handled_response = await fetch(endpoint_url); + + // Ensure custom headers are received and a valid content-length is also received + assert_log( + group, + `${candidate} Handled Response Streamed Headers & Content-Length Test`, + () => + handled_response.headers.get('x-is-streamed') === 'true' && + +handled_response.headers.get('content-length') === expected_buffer.byteLength + ); + + // Download buffer from request to compare + received_buffer = await handled_response.buffer(); + received_hash = crypto.createHash('md5').update(received_buffer).digest('hex'); + + // Test to see error handler was properly called on expected middleware error + assert_log( + group, + `${candidate} Handled Response Streamed Buffer/Hash Comparison Test - ${expected_hash} - ${test_file_stats.size} bytes`, + () => expected_buffer.equals(received_buffer) && expected_hash === received_hash + ); +} + +module.exports = { + test_response_stream_method, +}; diff --git a/packages/hyper-express/tests/components/http/scenarios/response_stream_sync_writes.js b/packages/hyper-express/tests/components/http/scenarios/response_stream_sync_writes.js new file mode 100644 index 0000000..92daf4e --- /dev/null +++ b/packages/hyper-express/tests/components/http/scenarios/response_stream_sync_writes.js @@ -0,0 +1,42 @@ +const { assert_log } = require('../../../scripts/operators.js'); +const { HyperExpress, fetch, server } = require('../../../configuration.js'); +const router = new HyperExpress.Router(); +const endpoint = '/tests/response'; +const scenario_endpoint = '/sync-writes'; +const endpoint_url = server.base + endpoint + scenario_endpoint; + +const expected_parts = ['1', '2', '3', 'done']; + +// Create Backend HTTP Route +router.get(scenario_endpoint, (request, response) => { + // Write the first 3 parts with response.write() + response.write(expected_parts[0]); + response.write(expected_parts[1]); + response.write(expected_parts[2]); + + // Send the last part with response.send() + response.send(expected_parts[3]); +}); + +// Bind router to webserver +const { TEST_SERVER } = require('../../Server.js'); +TEST_SERVER.use(endpoint, router); + +async function test_response_sync_writes() { + const group = 'RESPONSE'; + const candidate = 'HyperExpress.Response.write()'; + + // Make a fetch request to the endpoint + const response = await fetch(endpoint_url); + + // Get the received body from the response + const expected_body = expected_parts.join(''); + const received_body = await response.text(); + + // Ensure that the received body is the same as the expected body + assert_log(group, `${candidate} Sync Writes Test`, () => expected_body === received_body); +} + +module.exports = { + test_response_sync_writes, +}; diff --git a/packages/hyper-express/tests/components/router/Router.js b/packages/hyper-express/tests/components/router/Router.js new file mode 100644 index 0000000..3197e1b --- /dev/null +++ b/packages/hyper-express/tests/components/router/Router.js @@ -0,0 +1,115 @@ +const { log, assert_log } = require('../../scripts/operators.js'); +const { HyperExpress, fetch, server } = require('../../configuration.js'); +const { test_router_chainable_route } = require('./scenarios/chainable_routes.js'); +const { TEST_SERVER } = require('../Server.js'); +const endpoint_base = '/tests/router/echo-'; + +// Inject middleweare signature values into the requests through global, local and route specific middlewares +const middleware_signature = [Math.random(), Math.random(), Math.random()]; +TEST_SERVER.use((request, response, next) => { + // Initialize with first signature value + request.middleware_signature = [middleware_signature[0]]; + next(); +}); + +// Define all HTTP test method definitions +const route_definitions = { + get: { + method: 'GET', + call: 'get', + }, + post: { + method: 'POST', + call: 'post', + }, + del: { + method: 'DELETE', + call: 'delete', + }, + options: { + method: 'OPTIONS', + call: 'options', + }, + patch: { + method: 'PATCH', + call: 'patch', + }, + put: { + method: 'PUT', + call: 'put', + }, + trace: { + method: 'TRACE', + call: 'trace', + }, +}; + +// Create dynamic routes for testing across all methods +const router = new HyperExpress.Router(); +Object.keys(route_definitions).forEach((type) => { + const { method, call } = route_definitions[type]; + router[call]( + endpoint_base + type, + async (request, response) => { + // Push the third signature value to the request + request.middleware_signature.push(middleware_signature[2]); + }, + (request, response) => { + // Echo the methods, call and signature values to the client + response.json({ + method: request.method, + signature: request.middleware_signature, + }); + } + ); +}); +TEST_SERVER.use(router); + +// Bind a second global middleware +TEST_SERVER.use((request, response, next) => { + // Push the second signature value to the request + request.middleware_signature.push(middleware_signature[1]); + next(); +}); + +async function test_router_object() { + // Prepare Test Candidates + log('ROUTER', 'Testing HyperExpress.Router Object...'); + const group = 'ROUTER'; + const candidate = 'HyperExpress.Router'; + const start_time = Date.now(); + + // Test all route definitions to ensure consistency + await Promise.all( + Object.keys(route_definitions).map(async (type) => { + // Retrieve the expected method and call values + const { method, call } = route_definitions[type]; + + // Make the fetch request + const response = await fetch(server.base + endpoint_base + type, { + method, + }); + + // Retrieve the response body + const body = await response.json(); + + // Assert the response body + assert_log(group, `${candidate}.${call}() - HTTP ${method} Test`, () => { + const call_check = typeof router[call] == 'function'; + const method_check = method === body.method; + const signature_check = JSON.stringify(body.signature) === JSON.stringify(middleware_signature); + const route_check = TEST_SERVER.routes[type][endpoint_base + type] !== undefined; + return call_check && method_check && signature_check && route_check; + }); + }) + ); + + // Test the chainable route scenario + await test_router_chainable_route(); + + log(group, `Finished Testing ${candidate} In ${Date.now() - start_time}ms\n`); +} + +module.exports = { + test_router_object, +}; diff --git a/packages/hyper-express/tests/components/router/scenarios/chainable_routes.js b/packages/hyper-express/tests/components/router/scenarios/chainable_routes.js new file mode 100644 index 0000000..76d76eb --- /dev/null +++ b/packages/hyper-express/tests/components/router/scenarios/chainable_routes.js @@ -0,0 +1,59 @@ +const { assert_log } = require('../../../scripts/operators.js'); +const { HyperExpress, fetch, server } = require('../../../configuration.js'); +const endpoint = '/tests/router'; +const scenario_endpoint = '/chainable-routes'; +const endpoint_url = server.base + endpoint + scenario_endpoint; + +const routes = [ + { + method: 'GET', + payload: Math.random().toString(), + }, + { + method: 'POST', + payload: Math.random().toString(), + }, + { + method: 'PUT', + payload: Math.random().toString(), + }, + { + method: 'DELETE', + payload: Math.random().toString(), + }, +]; + +const router = new HyperExpress.Router(); + +let chainable = router.route(scenario_endpoint); +for (const route of routes) { + // This will test the chainability of the router + // Simulates Router.route().get().post().put().delete() + chainable = chainable[route.method.toLowerCase()]((_, response) => { + response.send(route.payload); + }); +} + +// Bind router to webserver +const { TEST_SERVER } = require('../../Server.js'); +TEST_SERVER.use(endpoint, router); + +async function test_router_chainable_route() { + const group = 'REQUEST'; + const candidate = 'HyperExpress.Router.route()'; + + // Perform fetch requests for each method + for (const route of routes) { + const response = await fetch(endpoint_url, { + method: route.method, + }); + + // Assert that the payload matches payload sent + const _payload = await response.text(); + assert_log(group, `${candidate} Chained HTTP ${route.method} Route`, () => _payload === route.payload); + } +} + +module.exports = { + test_router_chainable_route, +}; diff --git a/packages/hyper-express/tests/components/ws/Websocket.js b/packages/hyper-express/tests/components/ws/Websocket.js new file mode 100644 index 0000000..078c853 --- /dev/null +++ b/packages/hyper-express/tests/components/ws/Websocket.js @@ -0,0 +1,134 @@ +const { log, random_string, assert_log } = require('../../scripts/operators.js'); +const { HyperExpress, Websocket, server } = require('../../configuration.js'); +const { test_websocket_stream } = require('./scenarios/stream.js'); +const { test_websocket_writable } = require('./scenarios/writable.js'); + +const Router = new HyperExpress.Router(); +const TestPath = '/websocket-component'; +const TestCode = 1000; +const TestKey = random_string(30); + +// Create websocket route for handling protected upgrade +let remote_ws; +let remote_closed = false; +Router.ws('/echo', (ws) => { + // Store websocket object for checking throught tests + remote_ws = ws; + + // Bind message handler + ws.on('message', (message) => { + // Echo messages until we receive 'CLOSE' message + if (message === 'CLOSE') { + ws.close(TestCode); + } else { + ws.send(message); + } + }); + + // This will test that close event fires properly + ws.on('close', () => (remote_closed = true)); +}); + +// Create upgrade route for testing user assigned upgrade handler +Router.upgrade('/echo', (request, response) => { + // Reject upgrade request if valid key is not provided + const key = request.query_parameters['key']; + const delay = +request.query_parameters['delay'] || 0; + if (key !== TestKey) return response.status(403).send(); + + // Upgrade request with delay to simulate async upgrade handler or not + if (delay) { + setTimeout( + () => + response.upgrade({ + key, + }), + delay + ); + } else { + response.upgrade({ + key, + }); + } +}); + +// Bind router to test server instance +const { TEST_SERVER } = require('../../components/Server.js'); +TEST_SERVER.use(TestPath, Router); + +async function test_websocket_component() { + const group = 'WEBSOCKET'; + const candidate = 'HyperExpress.Websocket'; + const endpoint_base = `${server.base.replace('http', 'ws')}${TestPath}`; + log(group, 'Testing ' + candidate); + + // Test with No delay and random delay between 30-60ms for upgrade handler + await Promise.all( + [0, Math.floor(Math.random() * 30) + 30].map((delay) => { + // Test protected websocket route upgrade handling (NO KEY) + let count = 5; + const delay_message = `With ${delay}ms Delay`; + const ws_echo = new Websocket(`${endpoint_base}/echo?key=${TestKey}&delay=${delay}`); + return new Promise((resolve, reject) => { + let expecting; + ws_echo.on('open', () => { + // Assert that remote upgrade context was accessible from polyfill component + assert_log( + group, + `${candidate} ${delay_message} Upgrade Context Integrity`, + () => remote_ws.context.key === TestKey + ); + + // Start of echo chain with an expected random string + expecting = random_string(10); + ws_echo.send(expecting); + }); + + ws_echo.on('message', (message) => { + // Perform assertion to compare expected value with received value + message = message.toString(); + assert_log( + group, + `${candidate} ${delay_message} Echo Test > [${expecting} === ${message}]`, + () => expecting === message + ); + + // Perform echo tests until count is 0 + count--; + if (count > 0) { + expecting = random_string(10); + ws_echo.send(expecting); + } else { + // Tell remote to close connection + ws_echo.send('CLOSE'); + } + }); + + // Create a reject timeout to throw on hangups + let timeout = setTimeout(reject, 1000); + ws_echo.on('close', (code) => { + // Assert that close code matches the test code + assert_log(group, `${candidate} ${delay_message} Connection Close Code`, () => code === TestCode); + + clearTimeout(timeout); + resolve(); + }); + }); + }) + ); + + // Assert that remote server also closed connection/update polyfill state appropriately + assert_log(group, `${candidate} Remote Polyfill Close`, () => remote_ws.closed === true && remote_closed === true); + + // Test websocket .stream() method + await test_websocket_stream(); + + // Test websocket .writable property + await test_websocket_writable(); + + log(group, `Finished Testing ${candidate}\n`); +} + +module.exports = { + test_websocket_component, +}; diff --git a/packages/hyper-express/tests/components/ws/WebsocketRoute.js b/packages/hyper-express/tests/components/ws/WebsocketRoute.js new file mode 100644 index 0000000..3372944 --- /dev/null +++ b/packages/hyper-express/tests/components/ws/WebsocketRoute.js @@ -0,0 +1,122 @@ +const { log, random_string, assert_log } = require('../../scripts/operators.js'); +const { HyperExpress, Websocket, server } = require('../../configuration.js'); + +const Router = new HyperExpress.Router(); +const TestPath = '/websocket-route'; +const TestPayload = random_string(30); +const TestKey = random_string(30); +const TestOptions = { + idle_timeout: 500, + message_type: 'String', + compression: HyperExpress.compressors.DISABLED, + max_backpressure: 512 * 1024, + max_payload_length: 16 * 1024, +}; + +// Create websocket route for testing default upgrade handler +Router.ws('/unprotected', TestOptions, (ws) => { + // Send test payload and close if successful + if (ws.send(TestPayload)) ws.close(); +}); + +// Create upgrade route for testing user assigned upgrade handler +Router.upgrade('/protected', (request, response) => { + // Reject upgrade request if valid key is not provided + const key = request.query_parameters['key']; + if (key !== TestKey) return response.status(403).send(); + + // Upgrade request normally + response.upgrade({ + key, + }); +}); + +// Create websocket route for handling protected upgrade +Router.ws('/protected', (ws) => { + // Send test payload and close if successful + if (ws.send(TestPayload)) ws.close(); +}); + +// Bind router to test server instance +const { TEST_SERVER } = require('../../components/Server.js'); +TEST_SERVER.use(TestPath, Router); + +async function test_websocket_route() { + const group = 'WEBSOCKET'; + const candidate = 'HyperExpress.WebsocketRoute'; + const endpoint_base = `${server.base.replace('http', 'ws')}${TestPath}`; + log(group, 'Testing ' + candidate); + + // Test unprotected websocket route upgrade handling + const ws_unprotected = new Websocket(`${endpoint_base}/unprotected`); + await new Promise((resolve, reject) => { + // Store last message to test payload integrity + let last_message; + ws_unprotected.on('message', (message) => { + last_message = message.toString(); + }); + + // Create a reject timeout to throw on hangups + let timeout = setTimeout(reject, 1000); + ws_unprotected.on('close', () => { + // Perform assertion to test for valid last message + assert_log(group, `${candidate} Default/Unprotected Upgrade Handler`, () => last_message === TestPayload); + + // Cancel reject timeout and move on after assertion succeeds + clearTimeout(timeout); + resolve(); + }); + }); + + // Test protected websocket route upgrade handling (NO KEY) + const ws_protected_nokey = new Websocket(`${endpoint_base}/protected`); + await new Promise((resolve, reject) => { + // Store last error so we can compare the expected error type + let last_error; + ws_protected_nokey.on('error', (error) => { + last_error = error; + }); + + // Create a reject timeout to throw on hangups + let timeout = setTimeout(reject, 1000); + ws_protected_nokey.on('close', () => { + // Perform assertion to test for valid last message + assert_log( + group, + `${candidate} Protected Upgrade Handler Rejection With No Key`, + () => last_error && last_error.message.indexOf('403') > -1 + ); + + // Cancel reject timeout and move on after assertion succeeds + clearTimeout(timeout); + resolve(); + }); + }); + + // Test protected websocket route upgrade handling (WITH KEY) + const ws_protected_key = new Websocket(`${endpoint_base}/protected?key=${TestKey}`); + await new Promise((resolve, reject) => { + // Store last message to test payload integrity + let last_message; + ws_protected_key.on('message', (message) => { + last_message = message.toString(); + }); + + // Create a reject timeout to throw on hangups + let timeout = setTimeout(reject, 1000); + ws_protected_key.on('close', () => { + // Perform assertion to test for valid last message + assert_log(group, `${candidate} Protected Upgrade Handler With Key`, () => last_message === TestPayload); + + // Cancel reject timeout and move on after assertion succeeds + clearTimeout(timeout); + resolve(); + }); + }); + + log(group, `Finished Testing ${candidate}\n`); +} + +module.exports = { + test_websocket_route, +}; diff --git a/packages/hyper-express/tests/components/ws/scenarios/stream.js b/packages/hyper-express/tests/components/ws/scenarios/stream.js new file mode 100644 index 0000000..3dcd786 --- /dev/null +++ b/packages/hyper-express/tests/components/ws/scenarios/stream.js @@ -0,0 +1,64 @@ +const path = require('path'); +const crypto = require('crypto'); +const fs = require('fs'); +const { assert_log } = require('../../../scripts/operators.js'); +const { HyperExpress, Websocket, server } = require('../../../configuration.js'); + +const Router = new HyperExpress.Router(); +const TestPath = '/websocket-component'; +const TestFilePath = path.resolve(path.join(__dirname, '../../../content/large-image.jpg')); + +// Create an endpoint for serving a file +Router.ws('/stream', async (ws) => { + // Create a readable stream to serve to the receiver + const readable = fs.createReadStream(TestFilePath); + + // Stream the readable stream to the receiver + await ws.stream(readable); + + // Close the connection once we are done streaming + ws.close(); +}); + +// Bind router to test server instance +const { TEST_SERVER } = require('../../../components/Server.js'); +TEST_SERVER.use(TestPath, Router); + +async function test_websocket_stream() { + const group = 'WEBSOCKET'; + const candidate = 'HyperExpress.Websocket.stream()'; + const endpoint_base = `${server.base.replace('http', 'ws')}${TestPath}`; + + // Read test file's buffer into memory + const expected_buffer = fs.readFileSync(TestFilePath); + const expected_hash = crypto.createHash('md5').update(expected_buffer).digest('hex'); + + // Test protected websocket route upgrade handling (NO KEY) + const ws_stream = new Websocket(`${endpoint_base}/stream`); + await new Promise((resolve, reject) => { + let received_buffer; + let received_hash; + + // Assign a message handler to receive from websocket + ws_stream.on('message', (message) => { + // Store the received buffer and its hash + received_buffer = message; + received_hash = crypto.createHash('md5').update(received_buffer).digest('hex'); + }); + + // Assign a close handler to handle assertion + ws_stream.on('close', () => { + // Perform assertion to compare buffers and hashes + assert_log( + group, + `${candidate} - Streamed Binary Buffer Integrity - [${expected_hash}] == [${received_hash}]`, + () => expected_buffer.equals(received_buffer) && expected_hash === received_hash + ); + resolve(); + }); + }); +} + +module.exports = { + test_websocket_stream, +}; diff --git a/packages/hyper-express/tests/components/ws/scenarios/writable.js b/packages/hyper-express/tests/components/ws/scenarios/writable.js new file mode 100644 index 0000000..e8726cc --- /dev/null +++ b/packages/hyper-express/tests/components/ws/scenarios/writable.js @@ -0,0 +1,69 @@ +const path = require('path'); +const crypto = require('crypto'); +const fs = require('fs'); +const { assert_log } = require('../../../scripts/operators.js'); +const { HyperExpress, Websocket, server } = require('../../../configuration.js'); + +const Router = new HyperExpress.Router(); +const TestPath = '/websocket-component'; +const TestFilePath = path.resolve(path.join(__dirname, '../../../content/large-image.jpg')); + +// Create an endpoint for serving a file +Router.ws('/writable', async (ws) => { + // Create a readable stream to serve to the receiver + let readable = fs.createReadStream(TestFilePath); + + // Pipe the readable into the websocket writable + readable.pipe(ws.writable); + + // Bind a handler for once readable is finished + readable.once('close', () => { + // Repeat the same process as above to test multiple pipes to the same websocket connection + readable = fs.createReadStream(TestFilePath); + readable.pipe(ws.writable); + + // Bind the end handler again to close the connection this time + readable.once('close', () => ws.close()); + }); +}); + +// Bind router to test server instance +const { TEST_SERVER } = require('../../../components/Server.js'); +TEST_SERVER.use(TestPath, Router); + +async function test_websocket_writable() { + const group = 'WEBSOCKET'; + const candidate = 'HyperExpress.Websocket.writable'; + const endpoint_base = `${server.base.replace('http', 'ws')}${TestPath}`; + + // Read test file's buffer into memory + const expected_buffer = fs.readFileSync(TestFilePath); + const expected_hash = crypto.createHash('md5').update(expected_buffer).digest('hex'); + + // Test protected websocket route upgrade handling (NO KEY) + const ws_writable = new Websocket(`${endpoint_base}/writable`); + await new Promise((resolve, reject) => { + // Assign a message handler to receive from websocket + let counter = 1; + ws_writable.on('message', (message) => { + // Derive the retrieved buffer and its hash + const received_buffer = message; + const received_hash = crypto.createHash('md5').update(received_buffer).digest('hex'); + + // Assert the received data against the expected data + assert_log( + group, + `${candidate} - Piped Binary Buffer Integrity #${counter} - [${expected_hash}] == [${received_hash}]`, + () => expected_buffer.equals(received_buffer) && expected_hash === received_hash + ); + counter++; + }); + + // Assign a close handler to handle assertion + ws_writable.on('close', () => resolve()); + }); +} + +module.exports = { + test_websocket_writable, +}; diff --git a/packages/hyper-express/tests/configuration.js b/packages/hyper-express/tests/configuration.js new file mode 100644 index 0000000..0468580 --- /dev/null +++ b/packages/hyper-express/tests/configuration.js @@ -0,0 +1,26 @@ +const http = require('http'); +const fetch = require('node-fetch'); +const Websocket = require('ws'); +const EventSource = require('eventsource'); +const HyperExpress = require('../index.js'); +const AbortController = require('abort-controller'); + +const patchedFetch = (url, options = {}) => { + // Use a different http agent for each request to prevent connection pooling + options.agent = new http.Agent({ keepAlive: false }); + return fetch(url, options); +}; + +module.exports = { + fetch: patchedFetch, + Websocket, + EventSource, + HyperExpress, + AbortController, + server: { + host: '127.0.0.1', + port: '8080', // Ports should always be numbers but we are maintaining compatibility with strings + secure_port: 8443, + base: 'http://127.0.0.1:8080', + }, +}; diff --git a/packages/hyper-express/tests/content/example.txt b/packages/hyper-express/tests/content/example.txt new file mode 100644 index 0000000..81290d6 --- /dev/null +++ b/packages/hyper-express/tests/content/example.txt @@ -0,0 +1 @@ +This is some example text \ No newline at end of file diff --git a/packages/hyper-express/tests/content/large-image.jpg b/packages/hyper-express/tests/content/large-image.jpg new file mode 100644 index 0000000..86b2b4e Binary files /dev/null and b/packages/hyper-express/tests/content/large-image.jpg differ diff --git a/packages/hyper-express/tests/content/test-body.json b/packages/hyper-express/tests/content/test-body.json new file mode 100644 index 0000000..012c244 --- /dev/null +++ b/packages/hyper-express/tests/content/test-body.json @@ -0,0 +1,6 @@ +{ + "field1": "value1", + "field2": "value2", + "field3": "value3", + "field4": "12381923819283192831923" +} diff --git a/packages/hyper-express/tests/content/test.html b/packages/hyper-express/tests/content/test.html new file mode 100644 index 0000000..b0cc931 --- /dev/null +++ b/packages/hyper-express/tests/content/test.html @@ -0,0 +1,8 @@ + + + Test HTML + + + Test HTML + + diff --git a/packages/hyper-express/tests/content/written/.required b/packages/hyper-express/tests/content/written/.required new file mode 100644 index 0000000..e69de29 diff --git a/packages/hyper-express/tests/index.js b/packages/hyper-express/tests/index.js new file mode 100644 index 0000000..f9a24e8 --- /dev/null +++ b/packages/hyper-express/tests/index.js @@ -0,0 +1,93 @@ +const HyperExpress = require('../index.js'); +const { log, assert_log } = require('./scripts/operators.js'); +const { test_hostmanager_object } = require('./components/features/HostManager.js'); +const { test_router_object } = require('./components/router/Router.js'); +const { test_request_object } = require('./components/http/Request.js'); +const { test_response_object } = require('./components/http/Response.js'); +const { test_websocket_route } = require('./components/ws/WebsocketRoute.js'); +const { test_session_middleware } = require('./middlewares/hyper-express-session/index.js'); +const { test_websocket_component } = require('./components/ws/Websocket.js'); +// const { test_body_parser_middleware } = require('./middlewares/hyper-express-body-parser/index.js'); + +const { server } = require('./configuration.js'); +const { TEST_SERVER, not_found_handler, test_server_shutdown } = require('./components/Server.js'); +(async () => { + try { + // While this is effectively doing the same thing as the not_found_handler, we do not want HyperExpress to also bind its own not found handler which would throw a duplicate route error + TEST_SERVER.all('*', not_found_handler); + + // Initiate Test API Webserver + const group = 'Server'; + const start_time = Date.now(); + await TEST_SERVER.listen(server.port, server.host); + log('TESTING', `Successfully Started HyperExpress HTTP Server @ ${server.host}:${server.port}`); + + // Assert that the server port matches the configuration port + assert_log(group, 'Server Listening Port Test', () => +server.port === TEST_SERVER.port); + + // Assert that a server instance with a bad SSL configuration throws an error + await assert_log(group, 'Good SSL Configuration Initialization Test', async () => { + let result = false; + try { + const TEST_GOOD_SERVER = new HyperExpress.Server({ + key_file_name: './tests/ssl/dummy-key.pem', + cert_file_name: './tests/ssl/dummy-cert.pem', + }); + + // Also tests the callback functionality of the listen method + await new Promise((resolve) => { + TEST_GOOD_SERVER.listen(server.secure_port, server.host, resolve); + }); + TEST_GOOD_SERVER.close(); + result = true; + } catch (error) { + console.error(error); + } + return result; + }); + + // Assert that a server instance with a bad SSL configuration throws an error + assert_log(group, 'Bad SSL Configuration Error Test', () => { + let result = true; + try { + const TEST_BAD_SERVER = new HyperExpress.Server({ + key_file_name: './error.key', + cert_file_name: './error.cert', + }); + result = false; + } catch (error) { + return true; + } + return result; + }); + + // Test Server.HostManager Object + test_hostmanager_object(); + + // Test Router Object + await test_router_object(); + + // Test Request Object + await test_request_object(); + + // Test Response Object + await test_response_object(); + + // Test WebsocketRoute Object + await test_websocket_route(); + + // Test Websocket Polyfill Object + await test_websocket_component(); + + // Test SessionEngine Middleware + await test_session_middleware(); + + // Test the server shutdown process + await test_server_shutdown(); + + log('TESTING', `Successfully Tested All Specified Tests For HyperExpress In ${Date.now() - start_time}ms!`); + process.exit(); + } catch (error) { + console.log(error); + } +})(); diff --git a/packages/hyper-express/tests/local.js b/packages/hyper-express/tests/local.js new file mode 100644 index 0000000..5c0838b --- /dev/null +++ b/packages/hyper-express/tests/local.js @@ -0,0 +1,78 @@ +const fs = require('fs'); +const crypto = require('crypto'); +const { log } = require('./scripts/operators.js'); +const { server, fetch } = require('./configuration.js'); +const { TEST_SERVER } = require('./components/Server.js'); + +(async () => { + try { + // Define information about the test file + const test_file_path = './content/large-files/song.mp3'; + const test_file_checksum = crypto.createHash('md5').update(fs.readFileSync(test_file_path)).digest('hex'); + const test_file_stats = fs.statSync(test_file_path); + + // Create a simple GET route to stream a large file with chunked encoding + TEST_SERVER.get('/stream', async (request, response) => { + // Write appropriate headers + response.header('md5-checksum', test_file_checksum).type('mp3'); + + // Stream the file to the client + const readable = fs.createReadStream(test_file_path); + + // Stream the file to the client with a random streaming method + const random = Math.floor(Math.random() * 3); + switch (random) { + case 0: + // Use Chunked Transfer + readable.once('close', () => response.send()); + readable.pipe(response); + break; + case 1: + // Use Chunked-Transfer With Built In Streaming + response.stream(readable); + break; + case 2: + // Use Streaming With Content-Length + response.stream(readable, test_file_stats.size); + break; + } + }); + + // Initiate Test API Webserver + await TEST_SERVER.listen(server.port, server.host); + log( + 'TESTING', + `Successfully Started HyperExpress HTTP Server For Local Testing @ ${server.host}:${server.port}` + ); + + // Perform a stress test of the endpoint + let completed = 0; + let start_ts = Date.now(); + const test_endpoint = async () => { + // Make a request to the endpoint + const response = await fetch(`${server.base}/stream`); + + // Retrieve both the expected and received checksums + const expected_checksum = response.headers.get('md5-checksum'); + const received_checksum = crypto + .createHash('md5') + .update(await response.buffer()) + .digest('hex'); + + // Assert that the checksums match + if (expected_checksum !== received_checksum) + throw new Error( + `Checksums Do Not Match! Expected: ${expected_checksum} Received: ${received_checksum}` + ); + completed++; + }; + + setInterval(test_endpoint, 0); + setInterval( + () => console.log(`Requests/Second: ${(completed / ((Date.now() - start_ts) / 1000)).toFixed(2)}`), + 1000 + ); + } catch (error) { + console.log(error); + } +})(); diff --git a/packages/hyper-express/tests/middlewares/hyper-express-body-parser/configuration.json b/packages/hyper-express/tests/middlewares/hyper-express-body-parser/configuration.json new file mode 100644 index 0000000..9222950 --- /dev/null +++ b/packages/hyper-express/tests/middlewares/hyper-express-body-parser/configuration.json @@ -0,0 +1,3 @@ +{ + "path": "/middlewares/hyper-express-body-parser" +} diff --git a/packages/hyper-express/tests/middlewares/hyper-express-body-parser/index.js b/packages/hyper-express/tests/middlewares/hyper-express-body-parser/index.js new file mode 100644 index 0000000..89e626c --- /dev/null +++ b/packages/hyper-express/tests/middlewares/hyper-express-body-parser/index.js @@ -0,0 +1,22 @@ +const { test_parser_limit } = require('./scenarios/parser_limit.js'); +const { test_parser_validation } = require('./scenarios/parser_validation.js'); +const { test_parser_compression } = require('./scenarios/parser_compression.js'); +const { test_parser_types } = require('./scenarios/parser_types.js'); + +async function test_body_parser_middleware() { + // Test the BodyParser.options.limit property for limiting the size of the body + await test_parser_limit(); + + // Test the BodyParser.options.type and BodyParser.options.verify options functionaltiy + await test_parser_validation(); + + // Test the BodyParser compression functionality + await test_parser_compression(); + + // Test the BodyParser body types functionality + await test_parser_types(); +} + +module.exports = { + test_body_parser_middleware, +}; diff --git a/packages/hyper-express/tests/middlewares/hyper-express-body-parser/scenarios/parser_compression.js b/packages/hyper-express/tests/middlewares/hyper-express-body-parser/scenarios/parser_compression.js new file mode 100644 index 0000000..010076a --- /dev/null +++ b/packages/hyper-express/tests/middlewares/hyper-express-body-parser/scenarios/parser_compression.js @@ -0,0 +1,184 @@ +const fs = require('fs'); +const zlib = require('zlib'); +const BodyParser = require('../../../../middlewares/hyper-express-body-parser/index.js'); +const { log, assert_log, md5_from_buffer } = require('../../../scripts/operators.js'); +const { fetch, server } = require('../../../configuration.js'); +const { TEST_SERVER } = require('../../../components/Server.js'); +const { path } = require('../configuration.json'); +const endpoint = `${path}/scenarios/parser-compression`; +const endpoint_url = server.base + endpoint; +const test_file_path = './content/large-image.jpg'; + +// Bind a raw parser to the endpoint which does not uncompress the body +TEST_SERVER.use( + endpoint, + BodyParser.raw({ + limit: '5mb', + inflate: false, + type: 'application/octet-stream-strict', + }) +); + +// Bind a raw parser to the endpoint which does uncompress the body +TEST_SERVER.use( + endpoint, + BodyParser.raw({ + limit: '5mb', + type: 'application/octet-stream', + }) +); + +// Create Backend HTTP Route +TEST_SERVER.post(endpoint, (request, response) => { + // Echo the body back to the client + return response.send(request.body); +}); + +let file_length_cache = {}; +function get_test_file_length(encoding = 'identity') { + // Return value from cache if it exists + if (file_length_cache[encoding]) { + return file_length_cache[encoding]; + } + + // Otherwise, calculate the length of the file + const buffer = fs.readFileSync(test_file_path); + switch (encoding) { + case 'identity': + file_length_cache[encoding] = buffer.length; + break; + case 'gzip': + file_length_cache[encoding] = zlib.gzipSync(buffer).length; + break; + case 'deflate': + file_length_cache[encoding] = zlib.deflateSync(buffer).length; + break; + default: + throw new Error('Unsupported encoding: ' + encoding); + } + + return file_length_cache[encoding]; +} + +function get_test_file_stream(encoding = 'identity') { + const readable = fs.createReadStream(test_file_path); + switch (encoding) { + case 'gzip': + const gzip = zlib.createGzip(); + return readable.pipe(gzip); + case 'deflate': + const deflate = zlib.createDeflate(); + return readable.pipe(deflate); + default: + return readable; + } +} + +async function test_parser_compression() { + // User Specified ID Brute Vulnerability Test + let group = 'MIDDLEWARE'; + let candidate = 'Middleware.BodyParser'; + log(group, 'Testing ' + candidate + ' - Parser Compression Test'); + + // Determine the expected md5 hash of the test file + const expected_md5 = md5_from_buffer(await fs.promises.readFile(test_file_path)); + + // Perform fetch requests with the strict type but different compression types + const [strict_response_1, strict_response_2] = await Promise.all([ + fetch(endpoint_url, { + method: 'POST', + body: get_test_file_stream(), + headers: { + 'content-type': 'application/octet-stream-strict', + 'content-length': get_test_file_length(), + }, + }), + fetch(endpoint_url, { + method: 'POST', + body: get_test_file_stream('gzip'), + headers: { + 'content-type': 'application/octet-stream-strict', + 'content-encoding': 'gzip', + 'content-length': get_test_file_length('gzip'), + }, + }), + ]); + + // Retrieve the response bodies of the strict responses + const strict_body_1 = await strict_response_1.buffer(); + + // Assert that the strict response 1 was successful with matching bodies + assert_log( + group, + candidate + ' - Strict Parser Echo With Normal Body Test', + () => strict_response_1.status == 200 && md5_from_buffer(strict_body_1) == expected_md5 + ); + + // Assert that the strict response 2 was unsuccessful with a 415 status code + assert_log( + group, + candidate + ' - Strict Parser Reject With Compressed Body Test', + () => strict_response_2.status == 415 + ); + + // Test the integrity of buffer with different compressions with server + const [normal_response_1, normal_response_2, normal_response_3] = await Promise.all([ + fetch(endpoint_url, { + method: 'POST', + body: get_test_file_stream(), + headers: { + 'content-type': 'application/octet-stream', + 'content-length': get_test_file_length(), + }, + }), + fetch(endpoint_url, { + method: 'POST', + body: get_test_file_stream('deflate'), + headers: { + 'content-type': 'application/octet-stream', + 'content-encoding': 'deflate', + 'content-length': get_test_file_length('deflate'), + }, + }), + fetch(endpoint_url, { + method: 'POST', + body: get_test_file_stream('gzip'), + headers: { + 'content-type': 'application/octet-stream', + 'content-encoding': 'gzip', + 'content-length': get_test_file_length('gzip'), + }, + }), + ]); + + // Assert that all of the normal responses returned a 200 status code + assert_log( + group, + candidate + ' - Normal Parser Identity/Deflated/Gzipped HTTP Response Test', + () => normal_response_1.status == 200 && normal_response_2.status == 200 && normal_response_3.status == 200 + ); + + // Retrieve the response bodies of the normal responses + const [normal_body_1, normal_body_2, normal_body_3] = await Promise.all([ + normal_response_1.buffer(), + normal_response_2.buffer(), + normal_response_3.buffer(), + ]); + + // Assert that all normal bodies match the expected body + assert_log( + group, + candidate + ' - Normal Parser Identity/Deflated/Gzipped Body Integrity Test', + () => + md5_from_buffer(normal_body_1) == expected_md5 && + md5_from_buffer(normal_body_2) == expected_md5 && + md5_from_buffer(normal_body_3) == expected_md5 + ); + + // Wait for all the promises to resolve + log(group, 'Finished ' + candidate + ' - Parser Compression Test\n'); +} + +module.exports = { + test_parser_compression, +}; diff --git a/packages/hyper-express/tests/middlewares/hyper-express-body-parser/scenarios/parser_limit.js b/packages/hyper-express/tests/middlewares/hyper-express-body-parser/scenarios/parser_limit.js new file mode 100644 index 0000000..a151a6c --- /dev/null +++ b/packages/hyper-express/tests/middlewares/hyper-express-body-parser/scenarios/parser_limit.js @@ -0,0 +1,71 @@ +const crypto = require('crypto'); +const BodyParser = require('../../../../middlewares/hyper-express-body-parser/index.js'); +const { log, assert_log } = require('../../../scripts/operators.js'); +const { fetch, server } = require('../../../configuration.js'); +const { TEST_SERVER } = require('../../../components/Server.js'); +const { path } = require('../configuration.json'); +const endpoint = `${path}/scenarios/parser-limit`; +const endpoint_url = server.base + endpoint; + +// Bind a raw parser to the endpoint +const TEST_LIMIT_BYTES = Math.floor(Math.random() * 100) + 100; +TEST_SERVER.use( + endpoint, + BodyParser.raw({ + limit: TEST_LIMIT_BYTES, + }) +); + +// Create Backend HTTP Route +TEST_SERVER.post(endpoint, (request, response) => { + return response.send(); +}); + +async function test_parser_limit() { + // User Specified ID Brute Vulnerability Test + let group = 'MIDDLEWARE'; + let candidate = 'Middleware.BodyParser'; + log(group, 'Testing ' + candidate + ' - Parser Body Size Limit Test'); + + // Perform fetch requests with various body sizes + const promises = [ + Math.floor(Math.random() * TEST_LIMIT_BYTES), // Smaller than max size + TEST_LIMIT_BYTES, // Max size + Math.floor(Math.random() * TEST_LIMIT_BYTES) + TEST_LIMIT_BYTES, // Larger than max size + TEST_LIMIT_BYTES * Math.floor(Math.random() * 5), // Random Factor Larger than max size + Math.floor(TEST_LIMIT_BYTES * 0.1), // Smaller than max size + ].map( + (size_bytes) => + new Promise(async (resolve) => { + // Generate a random buffer of bytes size + const buffer = crypto.randomBytes(size_bytes); + + // Make the fetch request + const response = await fetch(endpoint_url, { + method: 'POST', + body: buffer, + headers: { + 'content-type': 'application/octet-stream', + }, + }); + + // Assert that the response status code is 413 for the large body + assert_log( + group, + candidate + + ` - Body Size Limit Test With ${size_bytes} / ${TEST_LIMIT_BYTES} Bytes Limit -> HTTP ${response.status}`, + () => response.status == (size_bytes > TEST_LIMIT_BYTES ? 413 : 200) + ); + + resolve(); + }) + ); + + // Wait for all the promises to resolve + await Promise.all(promises); + log(group, 'Finished ' + candidate + ' - Parser Body Size Limit Test\n'); +} + +module.exports = { + test_parser_limit, +}; diff --git a/packages/hyper-express/tests/middlewares/hyper-express-body-parser/scenarios/parser_types.js b/packages/hyper-express/tests/middlewares/hyper-express-body-parser/scenarios/parser_types.js new file mode 100644 index 0000000..95ddc85 --- /dev/null +++ b/packages/hyper-express/tests/middlewares/hyper-express-body-parser/scenarios/parser_types.js @@ -0,0 +1,94 @@ +const crypto = require('crypto'); +const BodyParser = require('../../../../middlewares/hyper-express-body-parser/index.js'); +const { log, assert_log } = require('../../../scripts/operators.js'); +const { fetch, server } = require('../../../configuration.js'); +const { TEST_SERVER } = require('../../../components/Server.js'); +const { path } = require('../configuration.json'); +const endpoint = `${path}/scenarios/parser-types`; +const endpoint_url = server.base + endpoint; + +// Bind all parser types to the endpoint +TEST_SERVER.use(endpoint, BodyParser.raw(), BodyParser.text(), BodyParser.json()); + +// Create Backend HTTP Route +TEST_SERVER.post(endpoint, (request, response) => { + const content_type = request.headers['content-type']; + switch (content_type) { + case 'application/json': + return response.json(request.body); + default: + return response.send(request.body); + } +}); + +async function test_parser_types() { + // User Specified ID Brute Vulnerability Test + let group = 'MIDDLEWARE'; + let candidate = 'Middleware.BodyParser'; + log(group, 'Testing ' + candidate + ' - Parser Body Types Test'); + + // Test the empty bodies + + // Perform fetch requests with various body types + const promises = [ + [crypto.randomBytes(1000), 'application/octet-stream'], + [crypto.randomBytes(1000).toString('hex'), 'text/plain'], + [ + JSON.stringify({ + name: 'json test', + payload: crypto.randomBytes(1000).toString('hex'), + }), + 'application/json', + ], + ].map( + ([request_body, content_type]) => + new Promise(async (resolve) => { + // Make the fetch request + const response = await fetch(endpoint_url, { + method: 'POST', + headers: { + 'content-type': content_type, + }, + body: request_body, + }); + + // Parse the incoming body as the appropriate type + let response_body; + switch (content_type) { + case 'application/octet-stream': + response_body = await response.buffer(); + break; + case 'text/plain': + response_body = await response.text(); + break; + case 'application/json': + response_body = await response.text(); + break; + } + + // Assert that the response status code is 413 for the large body + assert_log(group, candidate + ` - Body Type Test With '${content_type}'`, () => { + switch (content_type) { + case 'application/octet-stream': + return Buffer.compare(request_body, response_body) === 0; + case 'text/plain': + return request_body === response_body; + case 'application/json': + return JSON.stringify(request_body) === JSON.stringify(response_body); + default: + return false; + } + }); + + resolve(); + }) + ); + + // Wait for all the promises to resolve + await Promise.all(promises); + log(group, 'Finished ' + candidate + ' - Parser Body Types Test\n'); +} + +module.exports = { + test_parser_types, +}; diff --git a/packages/hyper-express/tests/middlewares/hyper-express-body-parser/scenarios/parser_validation.js b/packages/hyper-express/tests/middlewares/hyper-express-body-parser/scenarios/parser_validation.js new file mode 100644 index 0000000..6c87490 --- /dev/null +++ b/packages/hyper-express/tests/middlewares/hyper-express-body-parser/scenarios/parser_validation.js @@ -0,0 +1,75 @@ +const crypto = require('crypto'); +const BodyParser = require('../../../../middlewares/hyper-express-body-parser/index.js'); +const { log, assert_log } = require('../../../scripts/operators.js'); +const { fetch, server } = require('../../../configuration.js'); +const { TEST_SERVER } = require('../../../components/Server.js'); +const { path } = require('../configuration.json'); +const endpoint = `${path}/scenarios/parser-validation`; +const endpoint_url = server.base + endpoint; + +const TEST_PAYLOAD_SIZE = Math.floor(Math.random() * 250) + 250; + +// Bind a raw parser that will only parse if the content type matches +TEST_SERVER.use( + endpoint, + BodyParser.raw({ + type: 'application/octet-stream', + verify: (req, res, buffer) => { + return buffer.length > TEST_PAYLOAD_SIZE * 0.5; + }, // Reject bodies that are less than half size of the payload + }) +); + +// Create Backend HTTP Route +TEST_SERVER.post(endpoint, (request, response) => { + // Send a 200 if we have some body content else send a 204 + response.status(request.body.length > 0 ? 200 : 204).send(); +}); + +async function test_parser_validation() { + // User Specified ID Brute Vulnerability Test + let group = 'MIDDLEWARE'; + let candidate = 'Middleware.BodyParser'; + log(group, 'Testing ' + candidate + ' - Parser Body Validation Test'); + + // Perform fetch requests with various body sizes + const promises = [ + ['application/json', TEST_PAYLOAD_SIZE, 204], // ~100% of the payload size but incorrect content type + ['application/octet-stream', Math.floor(TEST_PAYLOAD_SIZE * 0.25), 403], // ~25% of the payload size + ['application/octet-stream', Math.floor(TEST_PAYLOAD_SIZE * 0.75), 200], // ~75% of the payload size + ['application/octet-stream', TEST_PAYLOAD_SIZE, 200], // ~75% of the payload size + ].map( + ([content_type, size_bytes, status_code]) => + new Promise(async (resolve) => { + // Generate a random buffer of bytes size + const buffer = crypto.randomBytes(size_bytes); + + // Make the fetch request + const response = await fetch(endpoint_url, { + method: 'POST', + body: buffer, + headers: { + 'content-type': content_type, + }, + }); + + // Assert that the response status code is 413 for the large body + assert_log( + group, + candidate + + ` - Content Type & Verify Function Test With "${content_type}" @ ${size_bytes} Bytes Payload`, + () => response.status === status_code + ); + + resolve(); + }) + ); + + // Wait for all the promises to resolve + await Promise.all(promises); + log(group, 'Finished ' + candidate + ' - Parser Body Validation Test\n'); +} + +module.exports = { + test_parser_validation, +}; diff --git a/packages/hyper-express/tests/middlewares/hyper-express-session/configuration.json b/packages/hyper-express/tests/middlewares/hyper-express-session/configuration.json new file mode 100644 index 0000000..f62148d --- /dev/null +++ b/packages/hyper-express/tests/middlewares/hyper-express-session/configuration.json @@ -0,0 +1,4 @@ +{ + "path": "/middlewares/hyper-express-session", + "log_store_events": false +} diff --git a/packages/hyper-express/tests/middlewares/hyper-express-session/index.js b/packages/hyper-express/tests/middlewares/hyper-express-session/index.js new file mode 100644 index 0000000..8c5eca1 --- /dev/null +++ b/packages/hyper-express/tests/middlewares/hyper-express-session/index.js @@ -0,0 +1,23 @@ +// Bind test session engine to configuration path on test server +const { TEST_SERVER } = require('../../components/Server.js'); +const { TEST_ENGINE } = require('./test_engine.js'); +const { path } = require('./configuration.json'); +TEST_SERVER.use(path, TEST_ENGINE); + +const { test_properties_scenario } = require('./scenarios/properties.js'); +const { test_brute_scenario } = require('./scenarios/brute.js'); +const { test_duration_scenario } = require('./scenarios/duration.js'); +const { test_roll_scenario } = require('./scenarios/roll.js'); +const { test_visits_scenario } = require('./scenarios/visits.js'); + +async function test_session_middleware() { + await test_properties_scenario(); + await test_brute_scenario(); + await test_roll_scenario(); + await test_visits_scenario(); + await test_duration_scenario(); +} + +module.exports = { + test_session_middleware, +}; diff --git a/packages/hyper-express/tests/middlewares/hyper-express-session/scenarios/brute.js b/packages/hyper-express/tests/middlewares/hyper-express-session/scenarios/brute.js new file mode 100644 index 0000000..e2e21d7 --- /dev/null +++ b/packages/hyper-express/tests/middlewares/hyper-express-session/scenarios/brute.js @@ -0,0 +1,49 @@ +const { log, assert_log, random_string, async_for_each } = require('../../../scripts/operators.js'); +const { fetch, server } = require('../../../configuration.js'); +const { TEST_SERVER } = require('../../../components/Server.js'); +const { TEST_STORE } = require('../test_engine.js'); +const { path } = require('../configuration.json'); +const endpoint = `${path}/scenarios/brute`; +const endpoint_url = server.base + endpoint; + +// Create Backend HTTP Route +TEST_SERVER.post(endpoint, async (request, response) => { + await request.session.start(); + return response.json({ + session_id: request.session.id, + store: TEST_STORE.data, + }); +}); + +async function test_brute_scenario() { + // User Specified ID Brute Vulnerability Test + let group = 'MIDDLEWARE'; + let candidate = 'Middleware.SessionEngine.Session'; + let last_session_id = ''; + log(group, `Testing ${candidate} - Self-Specified/Brute Session ID Test`); + TEST_STORE.empty(); + await async_for_each([0, 1, 2, 3, 4], async (value, next) => { + let response = await fetch(endpoint_url, { + method: 'POST', + headers: { + cookie: 'test_sess=' + random_string(30), // Random Session IDs + }, + }); + + let body = await response.json(); + assert_log( + group, + candidate + ' Brute-Force ID Vulnerability Prevention @ ' + value, + () => Object.keys(body.store).length === 0 && last_session_id !== body.session_id + ); + + last_session_id = body.session_id; + next(); + }); + + log(group, `Finished Testing ${candidate} - Self-Specified/Brute Session ID Test\n`); +} + +module.exports = { + test_brute_scenario, +}; diff --git a/packages/hyper-express/tests/middlewares/hyper-express-session/scenarios/duration.js b/packages/hyper-express/tests/middlewares/hyper-express-session/scenarios/duration.js new file mode 100644 index 0000000..f364c2a --- /dev/null +++ b/packages/hyper-express/tests/middlewares/hyper-express-session/scenarios/duration.js @@ -0,0 +1,75 @@ +const { log, assert_log, async_for_each } = require('../../../scripts/operators.js'); +const { fetch, server } = require('../../../configuration.js'); +const { TEST_SERVER } = require('../../../components/Server.js'); +const { TEST_STORE, TEST_ENGINE } = require('../test_engine.js'); +const { path } = require('../configuration.json'); +const endpoint = `${path}/scenarios/duration`; +const endpoint_url = server.base + endpoint; + +// Create Backend HTTP Route +TEST_SERVER.post(endpoint, async (request, response) => { + await TEST_ENGINE.cleanup(); // Purposely trigger cleanup before every request to simulate ideal session cleanup + await request.session.start(); + let body = await request.text(); + let duration = parseInt(body); + + if (duration > 0) request.session.set_duration(duration); + + return response.json({ + session_id: request.session.id, + store: TEST_STORE.data, + }); +}); + +async function test_duration_scenario() { + let group = 'MIDDLEWARE'; + let candidate = 'Middleware.SessionEngine.Session'; + let cookies = []; + + log(group, 'Testing ' + candidate + ' - Custom Duration/Cleanup Test'); + TEST_STORE.empty(); + await async_for_each([1, 2, 3], async (value, next) => { + let response = await fetch(endpoint_url, { + method: 'POST', + headers: { + cookie: cookies.join('; '), + }, + body: value < 3 ? '250' : '', // Set Custom Duration On First 2 Requests + }); + let headers = response.headers.raw(); + let body = await response.json(); + + // Send session cookie with future requests + if (Array.isArray(headers['set-cookie'])) { + cookies = []; + headers['set-cookie'].forEach((chunk) => { + chunk = chunk.split('; ')[0].split('='); + let name = chunk[0]; + let value = chunk[1]; + let header = `${name}=${value}`; + cookies.push(header); + }); + } + + assert_log(group, candidate + ' Custom Duration/Cleanup @ Iteration ' + value, () => { + if (value == 1 || value == 3) return Object.keys(body.store).length == 0; + + let store_test = Object.keys(body.store).length == 1; + let sess_obj_test = body.store?.[body.session_id]?.data !== undefined; + + return store_test && sess_obj_test; + }); + + // Wait 1.5 Seconds for session to expire with custom duration before 3rd request + let delay = value == 2 ? 300 : 0; + if (delay > 0) log(group, `Waiting ${delay}ms to simulate custom duration expiry...`); + + setTimeout((n) => n(), delay, next); + }); + + log(group, 'Finished Testing ' + candidate + ' - Custom Duration/Cleanup Test\n'); +} + +module.exports = { + test_duration_scenario, +}; diff --git a/packages/hyper-express/tests/middlewares/hyper-express-session/scenarios/properties.js b/packages/hyper-express/tests/middlewares/hyper-express-session/scenarios/properties.js new file mode 100644 index 0000000..0c90e9e --- /dev/null +++ b/packages/hyper-express/tests/middlewares/hyper-express-session/scenarios/properties.js @@ -0,0 +1,71 @@ +const { log, assert_log, random_string, async_for_each } = require('../../../scripts/operators.js'); +const { fetch, server } = require('../../../configuration.js'); +const { TEST_SERVER } = require('../../../components/Server.js'); +const { TEST_STORE } = require('../test_engine.js'); +const { path } = require('../configuration.json'); +const endpoint = `${path}/scenarios/properties`; +const endpoint_url = server.base + endpoint; + +// Create Backend HTTP Route +TEST_SERVER.get(endpoint, async (request, response) => { + // Start the session + await request.session.start(); + + // Set some value into the session object + // The await is unneccessary but it is used to simulate a long running operation + await request.session.set({ + myid: 'some_id', + visits: 0, + }); + + return response.json({ + id: request.session.id, + signed_id: request.session.signed_id, + ready: request.session.ready, + stored: request.session.stored, + }); +}); + +async function test_properties_scenario() { + // Test session persistence with visits test - VISITS ITERATOR TEST + let group = 'MIDDLEWARE'; + let candidate = 'Middleware.SessionEngine.Session'; + + // Make first fetch request + const response1 = await fetch(endpoint_url); + const data1 = await response1.json(); + + // Make second fetch request + const response2 = await fetch(endpoint_url, { + headers: { + cookie: response1.headers.get('set-cookie').split('; ')[0], + }, + }); + const data2 = await response2.json(); + + // Assert that the Session.id is a string and exactly same in both requests + assert_log( + group, + `${candidate}.id`, + () => typeof data1.id == 'string' && data1.id.length > 0 && data1.id == data2.id + ); + + // Assert that the Session.signed_id is a string and exactly same in both requests + assert_log( + group, + `${candidate}.signed_id`, + () => typeof data1.signed_id == 'string' && data1.signed_id.length > 0 && data1.signed_id == data2.signed_id + ); + + // Assert that the session was Session.ready in both requests + assert_log(group, `${candidate}.ready`, () => data1.ready && data2.ready); + + // Assert that the session was Session.stored only in second request + assert_log(group, `${candidate}.stored`, () => !data1.stored && data2.stored); + + log(group, 'Finished Testing ' + candidate + ' - Properties Test\n'); +} + +module.exports = { + test_properties_scenario, +}; diff --git a/packages/hyper-express/tests/middlewares/hyper-express-session/scenarios/roll.js b/packages/hyper-express/tests/middlewares/hyper-express-session/scenarios/roll.js new file mode 100644 index 0000000..e7c7c8a --- /dev/null +++ b/packages/hyper-express/tests/middlewares/hyper-express-session/scenarios/roll.js @@ -0,0 +1,74 @@ +const { log, assert_log, random_string, async_for_each } = require('../../../scripts/operators.js'); +const { fetch, server } = require('../../../configuration.js'); +const { TEST_SERVER } = require('../../../components/Server.js'); +const { TEST_STORE } = require('../test_engine.js'); +const { path } = require('../configuration.json'); +const endpoint = `${path}/scenarios/roll`; +const endpoint_url = server.base + endpoint; + +// Create Backend HTTP Route +TEST_SERVER.post(endpoint, async (request, response) => { + await request.session.start(); + if (request.session.get('some_data') == undefined) { + request.session.set('some_data', random_string(10)); + } else { + // Performs a delete and migrate to a new roll id + await request.session.roll(); + } + + return response.json({ + session_id: request.session.id, + session_data: request.session.get(), + store: TEST_STORE.data, + }); +}); + +async function test_roll_scenario() { + let group = 'MIDDLEWARE'; + let candidate = 'Middleware.SessionEngine.Session'; + let cookies = []; + let last_rolled_id = ''; + log(group, 'Testing ' + candidate + ' - Roll Test'); + + TEST_STORE.empty(); + await async_for_each([0, 0, 1, 0], async (value, next) => { + let response = await fetch(endpoint_url, { + method: 'POST', + headers: { + cookie: cookies.join('; '), + }, + }); + let headers = response.headers.raw(); + let body = await response.json(); + + // Send session cookie with future requests + let current_session_id; + if (Array.isArray(headers['set-cookie'])) { + cookies = []; // Reset cookies for new session id + headers['set-cookie'].forEach((chunk) => { + chunk = chunk.split('; ')[0].split('='); + let name = chunk[0]; + let value = chunk[1]; + let header = `${name}=${value}`; + if (name === 'test_sess') current_session_id = value; + cookies.push(header); + }); + } + + assert_log(group, candidate + ' Session Roll @ Iterative Scenario ' + value, () => { + // Store will always be empty due to lazy persistance and .roll() destroying session during request + let store_test = Object.keys(body.store).length === Math.floor(value); + let id_test = value < 1 ? current_session_id !== last_rolled_id : true; + last_rolled_id = current_session_id; + return store_test && id_test; + }); + + next(); + }); + + log(group, 'Finished Testing ' + candidate + ' - Roll Test\n'); +} + +module.exports = { + test_roll_scenario, +}; diff --git a/packages/hyper-express/tests/middlewares/hyper-express-session/scenarios/visits.js b/packages/hyper-express/tests/middlewares/hyper-express-session/scenarios/visits.js new file mode 100644 index 0000000..157d66a --- /dev/null +++ b/packages/hyper-express/tests/middlewares/hyper-express-session/scenarios/visits.js @@ -0,0 +1,106 @@ +const { log, assert_log, async_for_each } = require('../../../scripts/operators.js'); +const { fetch, server } = require('../../../configuration.js'); +const { TEST_SERVER } = require('../../../components/Server.js'); +const { TEST_STORE } = require('../test_engine.js'); +const { path } = require('../configuration.json'); +const endpoint = `${path}/scenarios/visits`; +const endpoint_url = server.base + endpoint; + +// Create Backend HTTP Route +TEST_SERVER.post(endpoint, async (request, response) => { + await request.session.start(); + let visits = request.session.get('visits'); + + if (visits == undefined) { + visits = 1; + } else if (visits < 5) { + visits++; + } else { + visits = undefined; + } + + if (visits) { + request.session.set('visits', visits); + } else { + await request.session.destroy(); + } + + return response.json({ + session_id: request.session.id, + session: request.session.get(), + store: TEST_STORE.data, + }); +}); + +async function test_visits_scenario() { + // Test session persistence with visits test - VISITS ITERATOR TEST + let group = 'MIDDLEWARE'; + let candidate = 'Middleware.SessionEngine.Session'; + let cookies = []; + let session_expiry = 0; + + TEST_STORE.empty(); + log(group, 'Testing ' + candidate + ' - Visits Test'); + await async_for_each([1, 2, 3, 4, 5, 0, 1, 2, 3, 4], async (value, next) => { + let response = await fetch(endpoint_url, { + method: 'POST', + headers: { + cookie: cookies.join('; '), + }, + }); + let headers = response.headers.raw(); + let body = await response.json(); + + // Send session cookie with future requests + if (Array.isArray(headers['set-cookie'])) { + cookies = []; + headers['set-cookie'].forEach((chunk) => { + let chunks = chunk.split('; ')[0].split('='); + let name = chunks[0]; + let value = chunks[1]; + let header = `${name}=${value}`; + + // Ensure the cookie is not a "delete" operation with a max-age of 0 + if (chunk.toLowerCase().includes('max-age=0')) return; + + // Push the cookie to the list of cookies to send with future requests + cookies.push(header); + }); + } + + // Perform Visits Check + if (value == 0) { + assert_log(group, `${candidate} VISITS_TEST @ ${value}`, () => { + let visits_test = Object.keys(body.session).length == 0; + let store_test = body.store[body.session_id] == undefined && Object.keys(body.store).length == 0; + return visits_test && store_test; + }); + } else if (value == 1) { + assert_log(group, `${candidate} VISITS_TEST @ ${value}`, () => { + let visits_test = body.session.visits === value; + let store_test = body.store[body.session_id] == undefined; + return visits_test && store_test; + }); + } else { + assert_log(group, `${candidate} OBJ_TOUCH_TEST & OBJ_VISITS_TEST @ ${value}`, () => { + let session_object = body.store?.[body.session_id]; + let visits_test = body.session.visits === value; + let store_test = session_object?.data?.visits === value; + + let touch_test = value < 3; + if (!touch_test && session_object.expiry >= session_expiry) touch_test = true; + + session_expiry = session_object.expiry; + return visits_test && store_test && touch_test; + }); + } + + next(); + }); + + log(group, 'Finished Testing ' + candidate + ' - Visits Test\n'); +} + +module.exports = { + test_visits_scenario, +}; diff --git a/packages/hyper-express/tests/middlewares/hyper-express-session/test_engine.js b/packages/hyper-express/tests/middlewares/hyper-express-session/test_engine.js new file mode 100644 index 0000000..9f660eb --- /dev/null +++ b/packages/hyper-express/tests/middlewares/hyper-express-session/test_engine.js @@ -0,0 +1,64 @@ +const SessionEngine = require('../../../middlewares/hyper-express-session/index.js'); +const MemoryStore = require('../../scripts/MemoryStore.js'); +const { random_string } = require('../../scripts/operators.js'); + +// Create Test Engine For Usage In Tests +const TEST_ENGINE = new SessionEngine({ + duration: 1000 * 60 * 45, + cookie: { + name: 'test_sess', + httpOnly: false, + secure: false, + sameSite: 'none', + secret: random_string(20), + }, +}); + +const { log } = require('../../scripts/operators.js'); +const { log_store_events } = require('./configuration.json'); +function store_log(message) { + if (log_store_events === true) log('SESSION_STORE', message); +} + +// Use a simulated SQL-like memory store +const TEST_STORE = new MemoryStore(); + +// Handle READ events +TEST_ENGINE.use('read', (session) => { + store_log('READ -> ' + session.id); + return TEST_STORE.select(session.id); +}); + +// Handle WRITE events +TEST_ENGINE.use('write', (session) => { + if (session.stored) { + store_log('UPDATE -> ' + session.id + ' -> ' + session.expires_at); + TEST_STORE.update(session.id, session.get(), session.expires_at); + } else { + store_log('INSERT -> ' + session.id + ' -> ' + session.expires_at); + TEST_STORE.insert(session.id, session.get(), session.expires_at); + } +}); + +// Handle TOUCH events +TEST_ENGINE.use('touch', (session) => { + store_log('TOUCH -> ' + session.id + ' -> ' + session.expires_at); + TEST_STORE.touch(session.id, session.expires_at); +}); + +// Handle DESTROY events +TEST_ENGINE.use('destroy', (session) => { + store_log('DESTROY -> ' + session.id); + TEST_STORE.delete(session.id); +}); + +// Handle CLEANUP events +TEST_ENGINE.use('cleanup', () => { + store_log('CLEANUP -> ALL SESSIONS'); + TEST_STORE.cleanup(); +}); + +module.exports = { + TEST_ENGINE, + TEST_STORE, +}; diff --git a/packages/hyper-express/tests/package-lock.json b/packages/hyper-express/tests/package-lock.json new file mode 100644 index 0000000..fc618ad --- /dev/null +++ b/packages/hyper-express/tests/package-lock.json @@ -0,0 +1,3943 @@ +{ + "name": "hyper-express-tests", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "hyper-express-tests", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "abort-controller": "^3.0.0", + "eventsource": "^2.0.0", + "form-data": "^4.0.0", + "i": "^0.3.7", + "node-fetch": "^2.6.6", + "npm": "^8.5.5", + "time-cost": "^1.0.0", + "ws": "^8.2.3", + "zlib": "^1.0.5" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/eventsource": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.0.tgz", + "integrity": "sha512-U9TI2qLWwedwiDLCbSUoSAPHGK2P7nT6/f25wBzMy9tWOKgFoNY4n+GYCPCYg3sGKrIoCmpChJoO3KKymcLo8A==", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/i": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/i/-/i-0.3.7.tgz", + "integrity": "sha512-FYz4wlXgkQwIPqhzC5TdNMLSE5+GS1IIDJZY/1ZiEPCT2S3COUVZeT5OW4BmW4r5LHLQuOosSwsvnroG9GR59Q==", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/mime-db": { + "version": "1.51.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", + "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.34", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz", + "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==", + "dependencies": { + "mime-db": "1.51.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/npm": { + "version": "8.5.5", + "resolved": "https://registry.npmjs.org/npm/-/npm-8.5.5.tgz", + "integrity": "sha512-a1vl26nokCNlD+my/iNYmOUPx/hpYR4ZyZk8gb7/A2XXtrPZf2gTSJOnVjS77jQS+BSfIVQpipZwXWCL0+5wzg==", + "bundleDependencies": [ + "@isaacs/string-locale-compare", + "@npmcli/arborist", + "@npmcli/ci-detect", + "@npmcli/config", + "@npmcli/map-workspaces", + "@npmcli/package-json", + "@npmcli/run-script", + "abbrev", + "ansicolors", + "ansistyles", + "archy", + "cacache", + "chalk", + "chownr", + "cli-columns", + "cli-table3", + "columnify", + "fastest-levenshtein", + "glob", + "graceful-fs", + "hosted-git-info", + "ini", + "init-package-json", + "is-cidr", + "json-parse-even-better-errors", + "libnpmaccess", + "libnpmdiff", + "libnpmexec", + "libnpmfund", + "libnpmhook", + "libnpmorg", + "libnpmpack", + "libnpmpublish", + "libnpmsearch", + "libnpmteam", + "libnpmversion", + "make-fetch-happen", + "minipass", + "minipass-pipeline", + "mkdirp", + "mkdirp-infer-owner", + "ms", + "node-gyp", + "nopt", + "npm-audit-report", + "npm-install-checks", + "npm-package-arg", + "npm-pick-manifest", + "npm-profile", + "npm-registry-fetch", + "npm-user-validate", + "npmlog", + "opener", + "pacote", + "parse-conflict-json", + "proc-log", + "qrcode-terminal", + "read", + "read-package-json", + "read-package-json-fast", + "readdir-scoped-modules", + "rimraf", + "semver", + "ssri", + "tar", + "text-table", + "tiny-relative-date", + "treeverse", + "validate-npm-package-name", + "which", + "write-file-atomic" + ], + "dependencies": { + "@isaacs/string-locale-compare": "^1.1.0", + "@npmcli/arborist": "^5.0.3", + "@npmcli/ci-detect": "^2.0.0", + "@npmcli/config": "^4.0.1", + "@npmcli/map-workspaces": "^2.0.2", + "@npmcli/package-json": "^1.0.1", + "@npmcli/run-script": "^3.0.1", + "abbrev": "~1.1.1", + "ansicolors": "~0.3.2", + "ansistyles": "~0.1.3", + "archy": "~1.0.0", + "cacache": "^16.0.2", + "chalk": "^4.1.2", + "chownr": "^2.0.0", + "cli-columns": "^4.0.0", + "cli-table3": "^0.6.1", + "columnify": "^1.6.0", + "fastest-levenshtein": "^1.0.12", + "glob": "^7.2.0", + "graceful-fs": "^4.2.9", + "hosted-git-info": "^5.0.0", + "ini": "^2.0.0", + "init-package-json": "^3.0.1", + "is-cidr": "^4.0.2", + "json-parse-even-better-errors": "^2.3.1", + "libnpmaccess": "^6.0.2", + "libnpmdiff": "^4.0.2", + "libnpmexec": "^4.0.2", + "libnpmfund": "^3.0.1", + "libnpmhook": "^8.0.2", + "libnpmorg": "^4.0.2", + "libnpmpack": "^4.0.2", + "libnpmpublish": "^6.0.2", + "libnpmsearch": "^5.0.2", + "libnpmteam": "^4.0.2", + "libnpmversion": "^3.0.1", + "make-fetch-happen": "^10.0.6", + "minipass": "^3.1.6", + "minipass-pipeline": "^1.2.4", + "mkdirp": "^1.0.4", + "mkdirp-infer-owner": "^2.0.0", + "ms": "^2.1.2", + "node-gyp": "^9.0.0", + "nopt": "^5.0.0", + "npm-audit-report": "^2.1.5", + "npm-install-checks": "^4.0.0", + "npm-package-arg": "^9.0.1", + "npm-pick-manifest": "^7.0.0", + "npm-profile": "^6.0.2", + "npm-registry-fetch": "^13.0.1", + "npm-user-validate": "^1.0.1", + "npmlog": "^6.0.1", + "opener": "^1.5.2", + "pacote": "^13.0.5", + "parse-conflict-json": "^2.0.1", + "proc-log": "^2.0.0", + "qrcode-terminal": "^0.12.0", + "read": "~1.0.7", + "read-package-json": "^5.0.0", + "read-package-json-fast": "^2.0.3", + "readdir-scoped-modules": "^1.1.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "ssri": "^8.0.1", + "tar": "^6.1.11", + "text-table": "~0.2.0", + "tiny-relative-date": "^1.3.0", + "treeverse": "^1.0.4", + "validate-npm-package-name": "~3.0.0", + "which": "^2.0.2", + "write-file-atomic": "^4.0.1" + }, + "bin": { + "npm": "bin/npm-cli.js", + "npx": "bin/npx-cli.js" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16" + } + }, + "node_modules/npm/node_modules/@gar/promisify": { + "version": "1.1.3", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/@isaacs/string-locale-compare": { + "version": "1.1.0", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/@npmcli/arborist": { + "version": "5.0.3", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@isaacs/string-locale-compare": "^1.1.0", + "@npmcli/installed-package-contents": "^1.0.7", + "@npmcli/map-workspaces": "^2.0.0", + "@npmcli/metavuln-calculator": "^3.0.1", + "@npmcli/move-file": "^1.1.0", + "@npmcli/name-from-folder": "^1.0.1", + "@npmcli/node-gyp": "^1.0.3", + "@npmcli/package-json": "^1.0.1", + "@npmcli/run-script": "^3.0.0", + "bin-links": "^3.0.0", + "cacache": "^16.0.0", + "common-ancestor-path": "^1.0.1", + "json-parse-even-better-errors": "^2.3.1", + "json-stringify-nice": "^1.1.4", + "mkdirp": "^1.0.4", + "mkdirp-infer-owner": "^2.0.0", + "nopt": "^5.0.0", + "npm-install-checks": "^4.0.0", + "npm-package-arg": "^9.0.0", + "npm-pick-manifest": "^7.0.0", + "npm-registry-fetch": "^13.0.0", + "npmlog": "^6.0.1", + "pacote": "^13.0.5", + "parse-conflict-json": "^2.0.1", + "proc-log": "^2.0.0", + "promise-all-reject-late": "^1.0.0", + "promise-call-limit": "^1.0.1", + "read-package-json-fast": "^2.0.2", + "readdir-scoped-modules": "^1.1.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "ssri": "^8.0.1", + "treeverse": "^1.0.4", + "walk-up-path": "^1.0.0" + }, + "bin": { + "arborist": "bin/index.js" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16" + } + }, + "node_modules/npm/node_modules/@npmcli/ci-detect": { + "version": "2.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16" + } + }, + "node_modules/npm/node_modules/@npmcli/config": { + "version": "4.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/map-workspaces": "^2.0.1", + "ini": "^2.0.0", + "mkdirp-infer-owner": "^2.0.0", + "nopt": "^5.0.0", + "proc-log": "^2.0.0", + "read-package-json-fast": "^2.0.3", + "semver": "^7.3.5", + "walk-up-path": "^1.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16" + } + }, + "node_modules/npm/node_modules/@npmcli/disparity-colors": { + "version": "1.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "ansi-styles": "^4.3.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/@npmcli/fs": { + "version": "1.1.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@gar/promisify": "^1.0.1", + "semver": "^7.3.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16" + } + }, + "node_modules/npm/node_modules/@npmcli/git": { + "version": "3.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/promise-spawn": "^1.3.2", + "lru-cache": "^7.3.1", + "mkdirp": "^1.0.4", + "npm-pick-manifest": "^7.0.0", + "proc-log": "^2.0.0", + "promise-inflight": "^1.0.1", + "promise-retry": "^2.0.1", + "semver": "^7.3.5", + "which": "^2.0.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16" + } + }, + "node_modules/npm/node_modules/@npmcli/installed-package-contents": { + "version": "1.0.7", + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-bundled": "^1.1.1", + "npm-normalize-package-bin": "^1.0.1" + }, + "bin": { + "installed-package-contents": "index.js" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/npm/node_modules/@npmcli/map-workspaces": { + "version": "2.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/name-from-folder": "^1.0.1", + "glob": "^7.2.0", + "minimatch": "^5.0.1", + "read-package-json-fast": "^2.0.3" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16" + } + }, + "node_modules/npm/node_modules/@npmcli/map-workspaces/node_modules/brace-expansion": { + "version": "2.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/map-workspaces/node_modules/minimatch": { + "version": "5.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/@npmcli/metavuln-calculator": { + "version": "3.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "cacache": "^16.0.0", + "json-parse-even-better-errors": "^2.3.1", + "pacote": "^13.0.3", + "semver": "^7.3.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16" + } + }, + "node_modules/npm/node_modules/@npmcli/move-file": { + "version": "1.1.2", + "inBundle": true, + "license": "MIT", + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/@npmcli/name-from-folder": { + "version": "1.0.1", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/@npmcli/node-gyp": { + "version": "1.0.3", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/@npmcli/package-json": { + "version": "1.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "json-parse-even-better-errors": "^2.3.1" + } + }, + "node_modules/npm/node_modules/@npmcli/promise-spawn": { + "version": "1.3.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "infer-owner": "^1.0.4" + } + }, + "node_modules/npm/node_modules/@npmcli/run-script": { + "version": "3.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/node-gyp": "^1.0.3", + "@npmcli/promise-spawn": "^1.3.2", + "node-gyp": "^9.0.0", + "read-package-json-fast": "^2.0.3" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16" + } + }, + "node_modules/npm/node_modules/@tootallnate/once": { + "version": "2.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/npm/node_modules/abbrev": { + "version": "1.1.1", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/agent-base": { + "version": "6.0.2", + "inBundle": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/npm/node_modules/agentkeepalive": { + "version": "4.2.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "depd": "^1.1.2", + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/npm/node_modules/aggregate-error": { + "version": "3.1.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/ansi-regex": { + "version": "5.0.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/ansi-styles": { + "version": "4.3.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/npm/node_modules/ansicolors": { + "version": "0.3.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/ansistyles": { + "version": "0.1.3", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/aproba": { + "version": "2.0.0", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/archy": { + "version": "1.0.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/are-we-there-yet": { + "version": "3.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16" + } + }, + "node_modules/npm/node_modules/asap": { + "version": "2.0.6", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/balanced-match": { + "version": "1.0.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/bin-links": { + "version": "3.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "cmd-shim": "^4.0.1", + "mkdirp-infer-owner": "^2.0.0", + "npm-normalize-package-bin": "^1.0.0", + "read-cmd-shim": "^2.0.0", + "rimraf": "^3.0.0", + "write-file-atomic": "^4.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16" + } + }, + "node_modules/npm/node_modules/binary-extensions": { + "version": "2.2.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/brace-expansion": { + "version": "1.1.11", + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/npm/node_modules/builtins": { + "version": "1.0.3", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/cacache": { + "version": "16.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^1.0.0", + "@npmcli/move-file": "^1.1.2", + "chownr": "^2.0.0", + "fs-minipass": "^2.1.0", + "glob": "^7.2.0", + "infer-owner": "^1.0.4", + "lru-cache": "^7.5.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "mkdirp": "^1.0.4", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.1", + "tar": "^6.1.11", + "unique-filename": "^1.1.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16" + } + }, + "node_modules/npm/node_modules/chalk": { + "version": "4.1.2", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/npm/node_modules/chownr": { + "version": "2.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/cidr-regex": { + "version": "3.1.1", + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "ip-regex": "^4.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/clean-stack": { + "version": "2.2.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/npm/node_modules/cli-columns": { + "version": "4.0.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/npm/node_modules/cli-table3": { + "version": "0.6.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "colors": "1.4.0" + } + }, + "node_modules/npm/node_modules/clone": { + "version": "1.0.4", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/npm/node_modules/cmd-shim": { + "version": "4.1.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "mkdirp-infer-owner": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/color-convert": { + "version": "2.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/npm/node_modules/color-name": { + "version": "1.1.4", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/color-support": { + "version": "1.1.3", + "inBundle": true, + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/npm/node_modules/colors": { + "version": "1.4.0", + "inBundle": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/npm/node_modules/columnify": { + "version": "1.6.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "strip-ansi": "^6.0.1", + "wcwidth": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/npm/node_modules/common-ancestor-path": { + "version": "1.0.1", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/concat-map": { + "version": "0.0.1", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/console-control-strings": { + "version": "1.1.0", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/debug": { + "version": "4.3.3", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/npm/node_modules/debug/node_modules/ms": { + "version": "2.1.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/debuglog": { + "version": "1.0.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/npm/node_modules/defaults": { + "version": "1.0.3", + "inBundle": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + } + }, + "node_modules/npm/node_modules/delegates": { + "version": "1.0.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/depd": { + "version": "1.1.2", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/npm/node_modules/dezalgo": { + "version": "1.0.3", + "inBundle": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "node_modules/npm/node_modules/diff": { + "version": "5.0.0", + "inBundle": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/npm/node_modules/emoji-regex": { + "version": "8.0.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/encoding": { + "version": "0.1.13", + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/npm/node_modules/env-paths": { + "version": "2.2.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/npm/node_modules/err-code": { + "version": "2.0.3", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/fastest-levenshtein": { + "version": "1.0.12", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/fs-minipass": { + "version": "2.1.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/fs.realpath": { + "version": "1.0.0", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/function-bind": { + "version": "1.1.1", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/gauge": { + "version": "4.0.3", + "inBundle": true, + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16" + } + }, + "node_modules/npm/node_modules/glob": { + "version": "7.2.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/graceful-fs": { + "version": "4.2.9", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/has": { + "version": "1.0.3", + "inBundle": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/npm/node_modules/has-flag": { + "version": "4.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/has-unicode": { + "version": "2.0.1", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/hosted-git-info": { + "version": "5.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^7.5.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16" + } + }, + "node_modules/npm/node_modules/http-cache-semantics": { + "version": "4.1.0", + "inBundle": true, + "license": "BSD-2-Clause" + }, + "node_modules/npm/node_modules/http-proxy-agent": { + "version": "5.0.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/npm/node_modules/https-proxy-agent": { + "version": "5.0.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/npm/node_modules/humanize-ms": { + "version": "1.2.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/npm/node_modules/iconv-lite": { + "version": "0.6.3", + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm/node_modules/ignore-walk": { + "version": "4.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/imurmurhash": { + "version": "0.1.4", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/npm/node_modules/indent-string": { + "version": "4.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/infer-owner": { + "version": "1.0.4", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/inflight": { + "version": "1.0.6", + "inBundle": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/npm/node_modules/inherits": { + "version": "2.0.4", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/ini": { + "version": "2.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/init-package-json": { + "version": "3.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-package-arg": "^9.0.0", + "promzard": "^0.3.0", + "read": "^1.0.7", + "read-package-json": "^5.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4", + "validate-npm-package-name": "^3.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16" + } + }, + "node_modules/npm/node_modules/ip": { + "version": "1.1.5", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/ip-regex": { + "version": "4.3.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/is-cidr": { + "version": "4.0.2", + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "cidr-regex": "^3.1.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/is-core-module": { + "version": "2.8.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/npm/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/is-lambda": { + "version": "1.0.1", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/isexe": { + "version": "2.0.0", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/json-stringify-nice": { + "version": "1.1.4", + "inBundle": true, + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/jsonparse": { + "version": "1.3.1", + "engines": [ + "node >= 0.2.0" + ], + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/just-diff": { + "version": "5.0.1", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/just-diff-apply": { + "version": "4.0.1", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/libnpmaccess": { + "version": "6.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "aproba": "^2.0.0", + "minipass": "^3.1.1", + "npm-package-arg": "^9.0.1", + "npm-registry-fetch": "^13.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16" + } + }, + "node_modules/npm/node_modules/libnpmdiff": { + "version": "4.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/disparity-colors": "^1.0.1", + "@npmcli/installed-package-contents": "^1.0.7", + "binary-extensions": "^2.2.0", + "diff": "^5.0.0", + "minimatch": "^3.0.4", + "npm-package-arg": "^9.0.1", + "pacote": "^13.0.5", + "tar": "^6.1.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16" + } + }, + "node_modules/npm/node_modules/libnpmexec": { + "version": "4.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/arborist": "^5.0.0", + "@npmcli/ci-detect": "^2.0.0", + "@npmcli/run-script": "^3.0.0", + "chalk": "^4.1.0", + "mkdirp-infer-owner": "^2.0.0", + "npm-package-arg": "^9.0.1", + "npmlog": "^6.0.1", + "pacote": "^13.0.5", + "proc-log": "^2.0.0", + "read": "^1.0.7", + "read-package-json-fast": "^2.0.2", + "walk-up-path": "^1.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16" + } + }, + "node_modules/npm/node_modules/libnpmfund": { + "version": "3.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/arborist": "^5.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16" + } + }, + "node_modules/npm/node_modules/libnpmhook": { + "version": "8.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "aproba": "^2.0.0", + "npm-registry-fetch": "^13.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16" + } + }, + "node_modules/npm/node_modules/libnpmorg": { + "version": "4.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "aproba": "^2.0.0", + "npm-registry-fetch": "^13.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16" + } + }, + "node_modules/npm/node_modules/libnpmpack": { + "version": "4.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/run-script": "^3.0.0", + "npm-package-arg": "^9.0.1", + "pacote": "^13.0.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16" + } + }, + "node_modules/npm/node_modules/libnpmpublish": { + "version": "6.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "normalize-package-data": "^4.0.0", + "npm-package-arg": "^9.0.1", + "npm-registry-fetch": "^13.0.0", + "semver": "^7.1.3", + "ssri": "^8.0.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16" + } + }, + "node_modules/npm/node_modules/libnpmsearch": { + "version": "5.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-registry-fetch": "^13.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16" + } + }, + "node_modules/npm/node_modules/libnpmteam": { + "version": "4.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "aproba": "^2.0.0", + "npm-registry-fetch": "^13.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16" + } + }, + "node_modules/npm/node_modules/libnpmversion": { + "version": "3.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^3.0.0", + "@npmcli/run-script": "^3.0.0", + "json-parse-even-better-errors": "^2.3.1", + "proc-log": "^2.0.0", + "semver": "^7.3.5", + "stringify-package": "^1.0.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16" + } + }, + "node_modules/npm/node_modules/lru-cache": { + "version": "7.5.1", + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/npm/node_modules/make-fetch-happen": { + "version": "10.0.6", + "inBundle": true, + "license": "ISC", + "dependencies": { + "agentkeepalive": "^4.2.1", + "cacache": "^16.0.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.5.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^2.0.3", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^6.1.1", + "ssri": "^8.0.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16" + } + }, + "node_modules/npm/node_modules/minimatch": { + "version": "3.1.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/npm/node_modules/minipass": { + "version": "3.1.6", + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-collect": { + "version": "1.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/minipass-fetch": { + "version": "2.0.3", + "inBundle": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.1.6", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/npm/node_modules/minipass-flush": { + "version": "1.0.5", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/minipass-json-stream": { + "version": "1.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "jsonparse": "^1.3.1", + "minipass": "^3.0.0" + } + }, + "node_modules/npm/node_modules/minipass-pipeline": { + "version": "1.2.4", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-sized": { + "version": "1.0.3", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minizlib": { + "version": "2.1.2", + "inBundle": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/mkdirp": { + "version": "1.0.4", + "inBundle": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/mkdirp-infer-owner": { + "version": "2.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "infer-owner": "^1.0.4", + "mkdirp": "^1.0.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/ms": { + "version": "2.1.3", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/mute-stream": { + "version": "0.0.8", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/negotiator": { + "version": "0.6.3", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/npm/node_modules/node-gyp": { + "version": "9.0.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^10.0.3", + "nopt": "^5.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^12.22 || ^14.13 || >=16" + } + }, + "node_modules/npm/node_modules/nopt": { + "version": "5.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npm/node_modules/normalize-package-data": { + "version": "4.0.0", + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^5.0.0", + "is-core-module": "^2.8.1", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16" + } + }, + "node_modules/npm/node_modules/npm-audit-report": { + "version": "2.1.5", + "inBundle": true, + "license": "ISC", + "dependencies": { + "chalk": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/npm-bundled": { + "version": "1.1.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-normalize-package-bin": "^1.0.1" + } + }, + "node_modules/npm/node_modules/npm-install-checks": { + "version": "4.0.0", + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/npm-normalize-package-bin": { + "version": "1.0.1", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/npm-package-arg": { + "version": "9.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "hosted-git-info": "^5.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^3.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16" + } + }, + "node_modules/npm/node_modules/npm-packlist": { + "version": "4.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "glob": "^7.2.0", + "ignore-walk": "^4.0.1", + "npm-bundled": "^1.1.2", + "npm-normalize-package-bin": "^1.0.1" + }, + "bin": { + "npm-packlist": "bin/index.js" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16" + } + }, + "node_modules/npm/node_modules/npm-pick-manifest": { + "version": "7.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-install-checks": "^4.0.0", + "npm-normalize-package-bin": "^1.0.1", + "npm-package-arg": "^9.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16" + } + }, + "node_modules/npm/node_modules/npm-profile": { + "version": "6.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-registry-fetch": "^13.0.0", + "proc-log": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16" + } + }, + "node_modules/npm/node_modules/npm-registry-fetch": { + "version": "13.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "make-fetch-happen": "^10.0.3", + "minipass": "^3.1.6", + "minipass-fetch": "^2.0.1", + "minipass-json-stream": "^1.0.1", + "minizlib": "^2.1.2", + "npm-package-arg": "^9.0.0", + "proc-log": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16" + } + }, + "node_modules/npm/node_modules/npm-user-validate": { + "version": "1.0.1", + "inBundle": true, + "license": "BSD-2-Clause" + }, + "node_modules/npm/node_modules/npmlog": { + "version": "6.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.0", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16" + } + }, + "node_modules/npm/node_modules/once": { + "version": "1.4.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/npm/node_modules/opener": { + "version": "1.5.2", + "inBundle": true, + "license": "(WTFPL OR MIT)", + "bin": { + "opener": "bin/opener-bin.js" + } + }, + "node_modules/npm/node_modules/p-map": { + "version": "4.0.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm/node_modules/pacote": { + "version": "13.0.5", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^3.0.0", + "@npmcli/installed-package-contents": "^1.0.7", + "@npmcli/promise-spawn": "^1.2.0", + "@npmcli/run-script": "^3.0.1", + "cacache": "^16.0.0", + "chownr": "^2.0.0", + "fs-minipass": "^2.1.0", + "infer-owner": "^1.0.4", + "minipass": "^3.1.6", + "mkdirp": "^1.0.4", + "npm-package-arg": "^9.0.0", + "npm-packlist": "^4.0.0", + "npm-pick-manifest": "^7.0.0", + "npm-registry-fetch": "^13.0.1", + "proc-log": "^2.0.0", + "promise-retry": "^2.0.1", + "read-package-json": "^5.0.0", + "read-package-json-fast": "^2.0.3", + "rimraf": "^3.0.2", + "ssri": "^8.0.1", + "tar": "^6.1.11" + }, + "bin": { + "pacote": "lib/bin.js" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16" + } + }, + "node_modules/npm/node_modules/parse-conflict-json": { + "version": "2.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "json-parse-even-better-errors": "^2.3.1", + "just-diff": "^5.0.1", + "just-diff-apply": "^4.0.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16" + } + }, + "node_modules/npm/node_modules/path-is-absolute": { + "version": "1.0.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm/node_modules/proc-log": { + "version": "2.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16" + } + }, + "node_modules/npm/node_modules/promise-all-reject-late": { + "version": "1.0.1", + "inBundle": true, + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/promise-call-limit": { + "version": "1.0.1", + "inBundle": true, + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/promise-inflight": { + "version": "1.0.1", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/promise-retry": { + "version": "2.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/promzard": { + "version": "0.3.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "read": "1" + } + }, + "node_modules/npm/node_modules/qrcode-terminal": { + "version": "0.12.0", + "inBundle": true, + "bin": { + "qrcode-terminal": "bin/qrcode-terminal.js" + } + }, + "node_modules/npm/node_modules/read": { + "version": "1.0.7", + "inBundle": true, + "license": "ISC", + "dependencies": { + "mute-stream": "~0.0.4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/npm/node_modules/read-cmd-shim": { + "version": "2.0.0", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/read-package-json": { + "version": "5.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "glob": "^7.2.0", + "json-parse-even-better-errors": "^2.3.1", + "normalize-package-data": "^4.0.0", + "npm-normalize-package-bin": "^1.0.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16" + } + }, + "node_modules/npm/node_modules/read-package-json-fast": { + "version": "2.0.3", + "inBundle": true, + "license": "ISC", + "dependencies": { + "json-parse-even-better-errors": "^2.3.0", + "npm-normalize-package-bin": "^1.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/readable-stream": { + "version": "3.6.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/npm/node_modules/readdir-scoped-modules": { + "version": "1.1.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "debuglog": "^1.0.1", + "dezalgo": "^1.0.0", + "graceful-fs": "^4.1.2", + "once": "^1.3.0" + } + }, + "node_modules/npm/node_modules/retry": { + "version": "0.12.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/npm/node_modules/rimraf": { + "version": "3.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/safe-buffer": { + "version": "5.2.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/safer-buffer": { + "version": "2.1.2", + "inBundle": true, + "license": "MIT", + "optional": true + }, + "node_modules/npm/node_modules/semver": { + "version": "7.3.5", + "inBundle": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/semver/node_modules/lru-cache": { + "version": "6.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/set-blocking": { + "version": "2.0.0", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/signal-exit": { + "version": "3.0.7", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/smart-buffer": { + "version": "4.2.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/npm/node_modules/socks": { + "version": "2.6.2", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ip": "^1.1.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.13.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/npm/node_modules/socks-proxy-agent": { + "version": "6.1.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.1", + "socks": "^2.6.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/npm/node_modules/spdx-correct": { + "version": "3.1.1", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/npm/node_modules/spdx-exceptions": { + "version": "2.3.0", + "inBundle": true, + "license": "CC-BY-3.0" + }, + "node_modules/npm/node_modules/spdx-expression-parse": { + "version": "3.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/npm/node_modules/spdx-license-ids": { + "version": "3.0.11", + "inBundle": true, + "license": "CC0-1.0" + }, + "node_modules/npm/node_modules/ssri": { + "version": "8.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/string_decoder": { + "version": "1.3.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/npm/node_modules/string-width": { + "version": "4.2.3", + "inBundle": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/stringify-package": { + "version": "1.0.1", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/strip-ansi": { + "version": "6.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/supports-color": { + "version": "7.2.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/tar": { + "version": "6.1.11", + "inBundle": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^3.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/npm/node_modules/text-table": { + "version": "0.2.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/tiny-relative-date": { + "version": "1.3.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/treeverse": { + "version": "1.0.4", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/unique-filename": { + "version": "1.1.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^2.0.0" + } + }, + "node_modules/npm/node_modules/unique-slug": { + "version": "2.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + } + }, + "node_modules/npm/node_modules/util-deprecate": { + "version": "1.0.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/validate-npm-package-license": { + "version": "3.0.4", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/npm/node_modules/validate-npm-package-name": { + "version": "3.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "builtins": "^1.0.3" + } + }, + "node_modules/npm/node_modules/walk-up-path": { + "version": "1.0.0", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/wcwidth": { + "version": "1.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/npm/node_modules/which": { + "version": "2.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/wide-align": { + "version": "1.1.5", + "inBundle": true, + "license": "ISC", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/npm/node_modules/wrappy": { + "version": "1.0.2", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/write-file-atomic": { + "version": "4.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16" + } + }, + "node_modules/npm/node_modules/yallist": { + "version": "4.0.0", + "inBundle": true, + "license": "ISC" + }, + "node_modules/time-cost": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/time-cost/-/time-cost-1.0.0.tgz", + "integrity": "sha512-GOxm/HrgYxVRIUXq4nP2s46zcaESp2H/vPQMj9zeuvODiXE1Slr/Ey7BnagD3QeKmkdzSYLJ7zaSMfzBeeJKvQ==" + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/ws": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.4.0.tgz", + "integrity": "sha512-IHVsKe2pjajSUIl4KYMQOdlyliovpEPquKkqbwswulszzI7r0SfQrxnXdWAEqOlDCLrVSJzo+O1hAwdog2sKSQ==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/zlib": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/zlib/-/zlib-1.0.5.tgz", + "integrity": "sha1-bnyXL8NxxkWmr7A6sUdp3vEU/MA=", + "hasInstallScript": true, + "engines": { + "node": ">=0.2.0" + } + } + }, + "dependencies": { + "abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "requires": { + "event-target-shim": "^5.0.0" + } + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" + }, + "eventsource": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.0.tgz", + "integrity": "sha512-U9TI2qLWwedwiDLCbSUoSAPHGK2P7nT6/f25wBzMy9tWOKgFoNY4n+GYCPCYg3sGKrIoCmpChJoO3KKymcLo8A==" + }, + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "i": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/i/-/i-0.3.7.tgz", + "integrity": "sha512-FYz4wlXgkQwIPqhzC5TdNMLSE5+GS1IIDJZY/1ZiEPCT2S3COUVZeT5OW4BmW4r5LHLQuOosSwsvnroG9GR59Q==" + }, + "mime-db": { + "version": "1.51.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", + "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==" + }, + "mime-types": { + "version": "2.1.34", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz", + "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==", + "requires": { + "mime-db": "1.51.0" + } + }, + "node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "requires": { + "whatwg-url": "^5.0.0" + } + }, + "npm": { + "version": "8.5.5", + "resolved": "https://registry.npmjs.org/npm/-/npm-8.5.5.tgz", + "integrity": "sha512-a1vl26nokCNlD+my/iNYmOUPx/hpYR4ZyZk8gb7/A2XXtrPZf2gTSJOnVjS77jQS+BSfIVQpipZwXWCL0+5wzg==", + "requires": { + "@isaacs/string-locale-compare": "^1.1.0", + "@npmcli/arborist": "^5.0.3", + "@npmcli/ci-detect": "^2.0.0", + "@npmcli/config": "^4.0.1", + "@npmcli/map-workspaces": "^2.0.2", + "@npmcli/package-json": "^1.0.1", + "@npmcli/run-script": "^3.0.1", + "abbrev": "~1.1.1", + "ansicolors": "~0.3.2", + "ansistyles": "~0.1.3", + "archy": "~1.0.0", + "cacache": "^16.0.2", + "chalk": "^4.1.2", + "chownr": "^2.0.0", + "cli-columns": "^4.0.0", + "cli-table3": "^0.6.1", + "columnify": "^1.6.0", + "fastest-levenshtein": "^1.0.12", + "glob": "^7.2.0", + "graceful-fs": "^4.2.9", + "hosted-git-info": "^5.0.0", + "ini": "^2.0.0", + "init-package-json": "^3.0.1", + "is-cidr": "^4.0.2", + "json-parse-even-better-errors": "^2.3.1", + "libnpmaccess": "^6.0.2", + "libnpmdiff": "^4.0.2", + "libnpmexec": "^4.0.2", + "libnpmfund": "^3.0.1", + "libnpmhook": "^8.0.2", + "libnpmorg": "^4.0.2", + "libnpmpack": "^4.0.2", + "libnpmpublish": "^6.0.2", + "libnpmsearch": "^5.0.2", + "libnpmteam": "^4.0.2", + "libnpmversion": "^3.0.1", + "make-fetch-happen": "^10.0.6", + "minipass": "^3.1.6", + "minipass-pipeline": "^1.2.4", + "mkdirp": "^1.0.4", + "mkdirp-infer-owner": "^2.0.0", + "ms": "^2.1.2", + "node-gyp": "^9.0.0", + "nopt": "^5.0.0", + "npm-audit-report": "^2.1.5", + "npm-install-checks": "^4.0.0", + "npm-package-arg": "^9.0.1", + "npm-pick-manifest": "^7.0.0", + "npm-profile": "^6.0.2", + "npm-registry-fetch": "^13.0.1", + "npm-user-validate": "^1.0.1", + "npmlog": "^6.0.1", + "opener": "^1.5.2", + "pacote": "^13.0.5", + "parse-conflict-json": "^2.0.1", + "proc-log": "^2.0.0", + "qrcode-terminal": "^0.12.0", + "read": "~1.0.7", + "read-package-json": "^5.0.0", + "read-package-json-fast": "^2.0.3", + "readdir-scoped-modules": "^1.1.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "ssri": "^8.0.1", + "tar": "^6.1.11", + "text-table": "~0.2.0", + "tiny-relative-date": "^1.3.0", + "treeverse": "^1.0.4", + "validate-npm-package-name": "~3.0.0", + "which": "^2.0.2", + "write-file-atomic": "^4.0.1" + }, + "dependencies": { + "@gar/promisify": { + "version": "1.1.3", + "bundled": true + }, + "@isaacs/string-locale-compare": { + "version": "1.1.0", + "bundled": true + }, + "@npmcli/arborist": { + "version": "5.0.3", + "bundled": true, + "requires": { + "@isaacs/string-locale-compare": "^1.1.0", + "@npmcli/installed-package-contents": "^1.0.7", + "@npmcli/map-workspaces": "^2.0.0", + "@npmcli/metavuln-calculator": "^3.0.1", + "@npmcli/move-file": "^1.1.0", + "@npmcli/name-from-folder": "^1.0.1", + "@npmcli/node-gyp": "^1.0.3", + "@npmcli/package-json": "^1.0.1", + "@npmcli/run-script": "^3.0.0", + "bin-links": "^3.0.0", + "cacache": "^16.0.0", + "common-ancestor-path": "^1.0.1", + "json-parse-even-better-errors": "^2.3.1", + "json-stringify-nice": "^1.1.4", + "mkdirp": "^1.0.4", + "mkdirp-infer-owner": "^2.0.0", + "nopt": "^5.0.0", + "npm-install-checks": "^4.0.0", + "npm-package-arg": "^9.0.0", + "npm-pick-manifest": "^7.0.0", + "npm-registry-fetch": "^13.0.0", + "npmlog": "^6.0.1", + "pacote": "^13.0.5", + "parse-conflict-json": "^2.0.1", + "proc-log": "^2.0.0", + "promise-all-reject-late": "^1.0.0", + "promise-call-limit": "^1.0.1", + "read-package-json-fast": "^2.0.2", + "readdir-scoped-modules": "^1.1.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "ssri": "^8.0.1", + "treeverse": "^1.0.4", + "walk-up-path": "^1.0.0" + } + }, + "@npmcli/ci-detect": { + "version": "2.0.0", + "bundled": true + }, + "@npmcli/config": { + "version": "4.0.1", + "bundled": true, + "requires": { + "@npmcli/map-workspaces": "^2.0.1", + "ini": "^2.0.0", + "mkdirp-infer-owner": "^2.0.0", + "nopt": "^5.0.0", + "proc-log": "^2.0.0", + "read-package-json-fast": "^2.0.3", + "semver": "^7.3.5", + "walk-up-path": "^1.0.0" + } + }, + "@npmcli/disparity-colors": { + "version": "1.0.1", + "bundled": true, + "requires": { + "ansi-styles": "^4.3.0" + } + }, + "@npmcli/fs": { + "version": "1.1.0", + "bundled": true, + "requires": { + "@gar/promisify": "^1.0.1", + "semver": "^7.3.5" + } + }, + "@npmcli/git": { + "version": "3.0.0", + "bundled": true, + "requires": { + "@npmcli/promise-spawn": "^1.3.2", + "lru-cache": "^7.3.1", + "mkdirp": "^1.0.4", + "npm-pick-manifest": "^7.0.0", + "proc-log": "^2.0.0", + "promise-inflight": "^1.0.1", + "promise-retry": "^2.0.1", + "semver": "^7.3.5", + "which": "^2.0.2" + } + }, + "@npmcli/installed-package-contents": { + "version": "1.0.7", + "bundled": true, + "requires": { + "npm-bundled": "^1.1.1", + "npm-normalize-package-bin": "^1.0.1" + } + }, + "@npmcli/map-workspaces": { + "version": "2.0.2", + "bundled": true, + "requires": { + "@npmcli/name-from-folder": "^1.0.1", + "glob": "^7.2.0", + "minimatch": "^5.0.1", + "read-package-json-fast": "^2.0.3" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "bundled": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "5.0.1", + "bundled": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, + "@npmcli/metavuln-calculator": { + "version": "3.0.1", + "bundled": true, + "requires": { + "cacache": "^16.0.0", + "json-parse-even-better-errors": "^2.3.1", + "pacote": "^13.0.3", + "semver": "^7.3.5" + } + }, + "@npmcli/move-file": { + "version": "1.1.2", + "bundled": true, + "requires": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + } + }, + "@npmcli/name-from-folder": { + "version": "1.0.1", + "bundled": true + }, + "@npmcli/node-gyp": { + "version": "1.0.3", + "bundled": true + }, + "@npmcli/package-json": { + "version": "1.0.1", + "bundled": true, + "requires": { + "json-parse-even-better-errors": "^2.3.1" + } + }, + "@npmcli/promise-spawn": { + "version": "1.3.2", + "bundled": true, + "requires": { + "infer-owner": "^1.0.4" + } + }, + "@npmcli/run-script": { + "version": "3.0.1", + "bundled": true, + "requires": { + "@npmcli/node-gyp": "^1.0.3", + "@npmcli/promise-spawn": "^1.3.2", + "node-gyp": "^9.0.0", + "read-package-json-fast": "^2.0.3" + } + }, + "@tootallnate/once": { + "version": "2.0.0", + "bundled": true + }, + "abbrev": { + "version": "1.1.1", + "bundled": true + }, + "agent-base": { + "version": "6.0.2", + "bundled": true, + "requires": { + "debug": "4" + } + }, + "agentkeepalive": { + "version": "4.2.1", + "bundled": true, + "requires": { + "debug": "^4.1.0", + "depd": "^1.1.2", + "humanize-ms": "^1.2.1" + } + }, + "aggregate-error": { + "version": "3.1.0", + "bundled": true, + "requires": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + } + }, + "ansi-regex": { + "version": "5.0.1", + "bundled": true + }, + "ansi-styles": { + "version": "4.3.0", + "bundled": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "ansicolors": { + "version": "0.3.2", + "bundled": true + }, + "ansistyles": { + "version": "0.1.3", + "bundled": true + }, + "aproba": { + "version": "2.0.0", + "bundled": true + }, + "archy": { + "version": "1.0.0", + "bundled": true + }, + "are-we-there-yet": { + "version": "3.0.0", + "bundled": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + } + }, + "asap": { + "version": "2.0.6", + "bundled": true + }, + "balanced-match": { + "version": "1.0.2", + "bundled": true + }, + "bin-links": { + "version": "3.0.0", + "bundled": true, + "requires": { + "cmd-shim": "^4.0.1", + "mkdirp-infer-owner": "^2.0.0", + "npm-normalize-package-bin": "^1.0.0", + "read-cmd-shim": "^2.0.0", + "rimraf": "^3.0.0", + "write-file-atomic": "^4.0.0" + } + }, + "binary-extensions": { + "version": "2.2.0", + "bundled": true + }, + "brace-expansion": { + "version": "1.1.11", + "bundled": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "builtins": { + "version": "1.0.3", + "bundled": true + }, + "cacache": { + "version": "16.0.2", + "bundled": true, + "requires": { + "@npmcli/fs": "^1.0.0", + "@npmcli/move-file": "^1.1.2", + "chownr": "^2.0.0", + "fs-minipass": "^2.1.0", + "glob": "^7.2.0", + "infer-owner": "^1.0.4", + "lru-cache": "^7.5.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "mkdirp": "^1.0.4", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.1", + "tar": "^6.1.11", + "unique-filename": "^1.1.1" + } + }, + "chalk": { + "version": "4.1.2", + "bundled": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "chownr": { + "version": "2.0.0", + "bundled": true + }, + "cidr-regex": { + "version": "3.1.1", + "bundled": true, + "requires": { + "ip-regex": "^4.1.0" + } + }, + "clean-stack": { + "version": "2.2.0", + "bundled": true + }, + "cli-columns": { + "version": "4.0.0", + "bundled": true, + "requires": { + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + } + }, + "cli-table3": { + "version": "0.6.1", + "bundled": true, + "requires": { + "colors": "1.4.0", + "string-width": "^4.2.0" + } + }, + "clone": { + "version": "1.0.4", + "bundled": true + }, + "cmd-shim": { + "version": "4.1.0", + "bundled": true, + "requires": { + "mkdirp-infer-owner": "^2.0.0" + } + }, + "color-convert": { + "version": "2.0.1", + "bundled": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "bundled": true + }, + "color-support": { + "version": "1.1.3", + "bundled": true + }, + "colors": { + "version": "1.4.0", + "bundled": true, + "optional": true + }, + "columnify": { + "version": "1.6.0", + "bundled": true, + "requires": { + "strip-ansi": "^6.0.1", + "wcwidth": "^1.0.0" + } + }, + "common-ancestor-path": { + "version": "1.0.1", + "bundled": true + }, + "concat-map": { + "version": "0.0.1", + "bundled": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true + }, + "debug": { + "version": "4.3.3", + "bundled": true, + "requires": { + "ms": "2.1.2" + }, + "dependencies": { + "ms": { + "version": "2.1.2", + "bundled": true + } + } + }, + "debuglog": { + "version": "1.0.1", + "bundled": true + }, + "defaults": { + "version": "1.0.3", + "bundled": true, + "requires": { + "clone": "^1.0.2" + } + }, + "delegates": { + "version": "1.0.0", + "bundled": true + }, + "depd": { + "version": "1.1.2", + "bundled": true + }, + "dezalgo": { + "version": "1.0.3", + "bundled": true, + "requires": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "diff": { + "version": "5.0.0", + "bundled": true + }, + "emoji-regex": { + "version": "8.0.0", + "bundled": true + }, + "encoding": { + "version": "0.1.13", + "bundled": true, + "optional": true, + "requires": { + "iconv-lite": "^0.6.2" + } + }, + "env-paths": { + "version": "2.2.1", + "bundled": true + }, + "err-code": { + "version": "2.0.3", + "bundled": true + }, + "fastest-levenshtein": { + "version": "1.0.12", + "bundled": true + }, + "fs-minipass": { + "version": "2.1.0", + "bundled": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true + }, + "function-bind": { + "version": "1.1.1", + "bundled": true + }, + "gauge": { + "version": "4.0.3", + "bundled": true, + "requires": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + } + }, + "glob": { + "version": "7.2.0", + "bundled": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "graceful-fs": { + "version": "4.2.9", + "bundled": true + }, + "has": { + "version": "1.0.3", + "bundled": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "4.0.0", + "bundled": true + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true + }, + "hosted-git-info": { + "version": "5.0.0", + "bundled": true, + "requires": { + "lru-cache": "^7.5.1" + } + }, + "http-cache-semantics": { + "version": "4.1.0", + "bundled": true + }, + "http-proxy-agent": { + "version": "5.0.0", + "bundled": true, + "requires": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + } + }, + "https-proxy-agent": { + "version": "5.0.0", + "bundled": true, + "requires": { + "agent-base": "6", + "debug": "4" + } + }, + "humanize-ms": { + "version": "1.2.1", + "bundled": true, + "requires": { + "ms": "^2.0.0" + } + }, + "iconv-lite": { + "version": "0.6.3", + "bundled": true, + "optional": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + }, + "ignore-walk": { + "version": "4.0.1", + "bundled": true, + "requires": { + "minimatch": "^3.0.4" + } + }, + "imurmurhash": { + "version": "0.1.4", + "bundled": true + }, + "indent-string": { + "version": "4.0.0", + "bundled": true + }, + "infer-owner": { + "version": "1.0.4", + "bundled": true + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "bundled": true + }, + "ini": { + "version": "2.0.0", + "bundled": true + }, + "init-package-json": { + "version": "3.0.1", + "bundled": true, + "requires": { + "npm-package-arg": "^9.0.0", + "promzard": "^0.3.0", + "read": "^1.0.7", + "read-package-json": "^5.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4", + "validate-npm-package-name": "^3.0.0" + } + }, + "ip": { + "version": "1.1.5", + "bundled": true + }, + "ip-regex": { + "version": "4.3.0", + "bundled": true + }, + "is-cidr": { + "version": "4.0.2", + "bundled": true, + "requires": { + "cidr-regex": "^3.1.1" + } + }, + "is-core-module": { + "version": "2.8.1", + "bundled": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "bundled": true + }, + "is-lambda": { + "version": "1.0.1", + "bundled": true + }, + "isexe": { + "version": "2.0.0", + "bundled": true + }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "bundled": true + }, + "json-stringify-nice": { + "version": "1.1.4", + "bundled": true + }, + "jsonparse": { + "version": "1.3.1", + "bundled": true + }, + "just-diff": { + "version": "5.0.1", + "bundled": true + }, + "just-diff-apply": { + "version": "4.0.1", + "bundled": true + }, + "libnpmaccess": { + "version": "6.0.2", + "bundled": true, + "requires": { + "aproba": "^2.0.0", + "minipass": "^3.1.1", + "npm-package-arg": "^9.0.1", + "npm-registry-fetch": "^13.0.0" + } + }, + "libnpmdiff": { + "version": "4.0.2", + "bundled": true, + "requires": { + "@npmcli/disparity-colors": "^1.0.1", + "@npmcli/installed-package-contents": "^1.0.7", + "binary-extensions": "^2.2.0", + "diff": "^5.0.0", + "minimatch": "^3.0.4", + "npm-package-arg": "^9.0.1", + "pacote": "^13.0.5", + "tar": "^6.1.0" + } + }, + "libnpmexec": { + "version": "4.0.2", + "bundled": true, + "requires": { + "@npmcli/arborist": "^5.0.0", + "@npmcli/ci-detect": "^2.0.0", + "@npmcli/run-script": "^3.0.0", + "chalk": "^4.1.0", + "mkdirp-infer-owner": "^2.0.0", + "npm-package-arg": "^9.0.1", + "npmlog": "^6.0.1", + "pacote": "^13.0.5", + "proc-log": "^2.0.0", + "read": "^1.0.7", + "read-package-json-fast": "^2.0.2", + "walk-up-path": "^1.0.0" + } + }, + "libnpmfund": { + "version": "3.0.1", + "bundled": true, + "requires": { + "@npmcli/arborist": "^5.0.0" + } + }, + "libnpmhook": { + "version": "8.0.2", + "bundled": true, + "requires": { + "aproba": "^2.0.0", + "npm-registry-fetch": "^13.0.0" + } + }, + "libnpmorg": { + "version": "4.0.2", + "bundled": true, + "requires": { + "aproba": "^2.0.0", + "npm-registry-fetch": "^13.0.0" + } + }, + "libnpmpack": { + "version": "4.0.2", + "bundled": true, + "requires": { + "@npmcli/run-script": "^3.0.0", + "npm-package-arg": "^9.0.1", + "pacote": "^13.0.5" + } + }, + "libnpmpublish": { + "version": "6.0.2", + "bundled": true, + "requires": { + "normalize-package-data": "^4.0.0", + "npm-package-arg": "^9.0.1", + "npm-registry-fetch": "^13.0.0", + "semver": "^7.1.3", + "ssri": "^8.0.1" + } + }, + "libnpmsearch": { + "version": "5.0.2", + "bundled": true, + "requires": { + "npm-registry-fetch": "^13.0.0" + } + }, + "libnpmteam": { + "version": "4.0.2", + "bundled": true, + "requires": { + "aproba": "^2.0.0", + "npm-registry-fetch": "^13.0.0" + } + }, + "libnpmversion": { + "version": "3.0.1", + "bundled": true, + "requires": { + "@npmcli/git": "^3.0.0", + "@npmcli/run-script": "^3.0.0", + "json-parse-even-better-errors": "^2.3.1", + "proc-log": "^2.0.0", + "semver": "^7.3.5", + "stringify-package": "^1.0.1" + } + }, + "lru-cache": { + "version": "7.5.1", + "bundled": true + }, + "make-fetch-happen": { + "version": "10.0.6", + "bundled": true, + "requires": { + "agentkeepalive": "^4.2.1", + "cacache": "^16.0.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.5.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^2.0.3", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^6.1.1", + "ssri": "^8.0.1" + } + }, + "minimatch": { + "version": "3.1.2", + "bundled": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minipass": { + "version": "3.1.6", + "bundled": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "minipass-collect": { + "version": "1.0.2", + "bundled": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "minipass-fetch": { + "version": "2.0.3", + "bundled": true, + "requires": { + "encoding": "^0.1.13", + "minipass": "^3.1.6", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + } + }, + "minipass-flush": { + "version": "1.0.5", + "bundled": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "minipass-json-stream": { + "version": "1.0.1", + "bundled": true, + "requires": { + "jsonparse": "^1.3.1", + "minipass": "^3.0.0" + } + }, + "minipass-pipeline": { + "version": "1.2.4", + "bundled": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "minipass-sized": { + "version": "1.0.3", + "bundled": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "minizlib": { + "version": "2.1.2", + "bundled": true, + "requires": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + } + }, + "mkdirp": { + "version": "1.0.4", + "bundled": true + }, + "mkdirp-infer-owner": { + "version": "2.0.0", + "bundled": true, + "requires": { + "chownr": "^2.0.0", + "infer-owner": "^1.0.4", + "mkdirp": "^1.0.3" + } + }, + "ms": { + "version": "2.1.3", + "bundled": true + }, + "mute-stream": { + "version": "0.0.8", + "bundled": true + }, + "negotiator": { + "version": "0.6.3", + "bundled": true + }, + "node-gyp": { + "version": "9.0.0", + "bundled": true, + "requires": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^10.0.3", + "nopt": "^5.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + } + }, + "nopt": { + "version": "5.0.0", + "bundled": true, + "requires": { + "abbrev": "1" + } + }, + "normalize-package-data": { + "version": "4.0.0", + "bundled": true, + "requires": { + "hosted-git-info": "^5.0.0", + "is-core-module": "^2.8.1", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + } + }, + "npm-audit-report": { + "version": "2.1.5", + "bundled": true, + "requires": { + "chalk": "^4.0.0" + } + }, + "npm-bundled": { + "version": "1.1.2", + "bundled": true, + "requires": { + "npm-normalize-package-bin": "^1.0.1" + } + }, + "npm-install-checks": { + "version": "4.0.0", + "bundled": true, + "requires": { + "semver": "^7.1.1" + } + }, + "npm-normalize-package-bin": { + "version": "1.0.1", + "bundled": true + }, + "npm-package-arg": { + "version": "9.0.1", + "bundled": true, + "requires": { + "hosted-git-info": "^5.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^3.0.0" + } + }, + "npm-packlist": { + "version": "4.0.0", + "bundled": true, + "requires": { + "glob": "^7.2.0", + "ignore-walk": "^4.0.1", + "npm-bundled": "^1.1.2", + "npm-normalize-package-bin": "^1.0.1" + } + }, + "npm-pick-manifest": { + "version": "7.0.0", + "bundled": true, + "requires": { + "npm-install-checks": "^4.0.0", + "npm-normalize-package-bin": "^1.0.1", + "npm-package-arg": "^9.0.0", + "semver": "^7.3.5" + } + }, + "npm-profile": { + "version": "6.0.2", + "bundled": true, + "requires": { + "npm-registry-fetch": "^13.0.0", + "proc-log": "^2.0.0" + } + }, + "npm-registry-fetch": { + "version": "13.0.1", + "bundled": true, + "requires": { + "make-fetch-happen": "^10.0.3", + "minipass": "^3.1.6", + "minipass-fetch": "^2.0.1", + "minipass-json-stream": "^1.0.1", + "minizlib": "^2.1.2", + "npm-package-arg": "^9.0.0", + "proc-log": "^2.0.0" + } + }, + "npm-user-validate": { + "version": "1.0.1", + "bundled": true + }, + "npmlog": { + "version": "6.0.1", + "bundled": true, + "requires": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.0", + "set-blocking": "^2.0.0" + } + }, + "once": { + "version": "1.4.0", + "bundled": true, + "requires": { + "wrappy": "1" + } + }, + "opener": { + "version": "1.5.2", + "bundled": true + }, + "p-map": { + "version": "4.0.0", + "bundled": true, + "requires": { + "aggregate-error": "^3.0.0" + } + }, + "pacote": { + "version": "13.0.5", + "bundled": true, + "requires": { + "@npmcli/git": "^3.0.0", + "@npmcli/installed-package-contents": "^1.0.7", + "@npmcli/promise-spawn": "^1.2.0", + "@npmcli/run-script": "^3.0.1", + "cacache": "^16.0.0", + "chownr": "^2.0.0", + "fs-minipass": "^2.1.0", + "infer-owner": "^1.0.4", + "minipass": "^3.1.6", + "mkdirp": "^1.0.4", + "npm-package-arg": "^9.0.0", + "npm-packlist": "^4.0.0", + "npm-pick-manifest": "^7.0.0", + "npm-registry-fetch": "^13.0.1", + "proc-log": "^2.0.0", + "promise-retry": "^2.0.1", + "read-package-json": "^5.0.0", + "read-package-json-fast": "^2.0.3", + "rimraf": "^3.0.2", + "ssri": "^8.0.1", + "tar": "^6.1.11" + } + }, + "parse-conflict-json": { + "version": "2.0.1", + "bundled": true, + "requires": { + "json-parse-even-better-errors": "^2.3.1", + "just-diff": "^5.0.1", + "just-diff-apply": "^4.0.1" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true + }, + "proc-log": { + "version": "2.0.0", + "bundled": true + }, + "promise-all-reject-late": { + "version": "1.0.1", + "bundled": true + }, + "promise-call-limit": { + "version": "1.0.1", + "bundled": true + }, + "promise-inflight": { + "version": "1.0.1", + "bundled": true + }, + "promise-retry": { + "version": "2.0.1", + "bundled": true, + "requires": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + } + }, + "promzard": { + "version": "0.3.0", + "bundled": true, + "requires": { + "read": "1" + } + }, + "qrcode-terminal": { + "version": "0.12.0", + "bundled": true + }, + "read": { + "version": "1.0.7", + "bundled": true, + "requires": { + "mute-stream": "~0.0.4" + } + }, + "read-cmd-shim": { + "version": "2.0.0", + "bundled": true + }, + "read-package-json": { + "version": "5.0.0", + "bundled": true, + "requires": { + "glob": "^7.2.0", + "json-parse-even-better-errors": "^2.3.1", + "normalize-package-data": "^4.0.0", + "npm-normalize-package-bin": "^1.0.1" + } + }, + "read-package-json-fast": { + "version": "2.0.3", + "bundled": true, + "requires": { + "json-parse-even-better-errors": "^2.3.0", + "npm-normalize-package-bin": "^1.0.1" + } + }, + "readable-stream": { + "version": "3.6.0", + "bundled": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "readdir-scoped-modules": { + "version": "1.1.0", + "bundled": true, + "requires": { + "debuglog": "^1.0.1", + "dezalgo": "^1.0.0", + "graceful-fs": "^4.1.2", + "once": "^1.3.0" + } + }, + "retry": { + "version": "0.12.0", + "bundled": true + }, + "rimraf": { + "version": "3.0.2", + "bundled": true, + "requires": { + "glob": "^7.1.3" + } + }, + "safe-buffer": { + "version": "5.2.1", + "bundled": true + }, + "safer-buffer": { + "version": "2.1.2", + "bundled": true, + "optional": true + }, + "semver": { + "version": "7.3.5", + "bundled": true, + "requires": { + "lru-cache": "^6.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "bundled": true, + "requires": { + "yallist": "^4.0.0" + } + } + } + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true + }, + "signal-exit": { + "version": "3.0.7", + "bundled": true + }, + "smart-buffer": { + "version": "4.2.0", + "bundled": true + }, + "socks": { + "version": "2.6.2", + "bundled": true, + "requires": { + "ip": "^1.1.5", + "smart-buffer": "^4.2.0" + } + }, + "socks-proxy-agent": { + "version": "6.1.1", + "bundled": true, + "requires": { + "agent-base": "^6.0.2", + "debug": "^4.3.1", + "socks": "^2.6.1" + } + }, + "spdx-correct": { + "version": "3.1.1", + "bundled": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.3.0", + "bundled": true + }, + "spdx-expression-parse": { + "version": "3.0.1", + "bundled": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.11", + "bundled": true + }, + "ssri": { + "version": "8.0.1", + "bundled": true, + "requires": { + "minipass": "^3.1.1" + } + }, + "string_decoder": { + "version": "1.3.0", + "bundled": true, + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "string-width": { + "version": "4.2.3", + "bundled": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "stringify-package": { + "version": "1.0.1", + "bundled": true + }, + "strip-ansi": { + "version": "6.0.1", + "bundled": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "supports-color": { + "version": "7.2.0", + "bundled": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "tar": { + "version": "6.1.11", + "bundled": true, + "requires": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^3.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + } + }, + "text-table": { + "version": "0.2.0", + "bundled": true + }, + "tiny-relative-date": { + "version": "1.3.0", + "bundled": true + }, + "treeverse": { + "version": "1.0.4", + "bundled": true + }, + "unique-filename": { + "version": "1.1.1", + "bundled": true, + "requires": { + "unique-slug": "^2.0.0" + } + }, + "unique-slug": { + "version": "2.0.2", + "bundled": true, + "requires": { + "imurmurhash": "^0.1.4" + } + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true + }, + "validate-npm-package-license": { + "version": "3.0.4", + "bundled": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "validate-npm-package-name": { + "version": "3.0.0", + "bundled": true, + "requires": { + "builtins": "^1.0.3" + } + }, + "walk-up-path": { + "version": "1.0.0", + "bundled": true + }, + "wcwidth": { + "version": "1.0.1", + "bundled": true, + "requires": { + "defaults": "^1.0.3" + } + }, + "which": { + "version": "2.0.2", + "bundled": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "wide-align": { + "version": "1.1.5", + "bundled": true, + "requires": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true + }, + "write-file-atomic": { + "version": "4.0.1", + "bundled": true, + "requires": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + } + }, + "yallist": { + "version": "4.0.0", + "bundled": true + } + } + }, + "time-cost": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/time-cost/-/time-cost-1.0.0.tgz", + "integrity": "sha512-GOxm/HrgYxVRIUXq4nP2s46zcaESp2H/vPQMj9zeuvODiXE1Slr/Ey7BnagD3QeKmkdzSYLJ7zaSMfzBeeJKvQ==" + }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" + }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "ws": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.4.0.tgz", + "integrity": "sha512-IHVsKe2pjajSUIl4KYMQOdlyliovpEPquKkqbwswulszzI7r0SfQrxnXdWAEqOlDCLrVSJzo+O1hAwdog2sKSQ==", + "requires": {} + }, + "zlib": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/zlib/-/zlib-1.0.5.tgz", + "integrity": "sha1-bnyXL8NxxkWmr7A6sUdp3vEU/MA=" + } + } +} diff --git a/packages/hyper-express/tests/package.json b/packages/hyper-express/tests/package.json new file mode 100644 index 0000000..2aa2eda --- /dev/null +++ b/packages/hyper-express/tests/package.json @@ -0,0 +1,21 @@ +{ + "name": "hyper-express-tests", + "version": "1.0.0", + "description": "Unit/API Tests For HyperExpress", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "dependencies": { + "abort-controller": "^3.0.0", + "eventsource": "^2.0.0", + "form-data": "^4.0.0", + "i": "^0.3.7", + "node-fetch": "^2.6.6", + "npm": "^8.5.5", + "ws": "^8.2.3", + "zlib": "^1.0.5" + } +} diff --git a/packages/hyper-express/tests/performance.js b/packages/hyper-express/tests/performance.js new file mode 100644 index 0000000..7d1ba4d --- /dev/null +++ b/packages/hyper-express/tests/performance.js @@ -0,0 +1,74 @@ +class PerformanceMeasurement { + #data = []; + + /** + * Name for this performance measurement. + * @param {string} name + */ + constructor(name) { + // Register graceful shutdown handlers + let context = this; + let in_shutdown = false; + [`exit`, `SIGINT`, `SIGUSR1`, `SIGUSR2`, `SIGTERM`].forEach((type) => + process.on(type, () => { + // Mark the server as shutting down + if (in_shutdown) return; + in_shutdown = true; + + // Log the performance measurements + console.log(name, JSON.stringify(context.measurements)); + + // Set a timeout to exit the process after 1 second + setTimeout(() => process.exit(0), 1000); + }) + ); + } + + /** + * Records the amount of time it took to execute a function. + * Use `process.hrtime.bigint()` to get the start time. + * @param {BigInt} start_time + */ + record(start_time) { + const delta = process.hrtime.bigint() - start_time; + if (delta > 0) this.#data.push(delta); + } + + /** + * Returns the measurements of this performance measurement. + */ + get measurements() { + // Initialize the individual statistics + let average = 0; + let sum = BigInt(0); + let min = BigInt(Number.MAX_SAFE_INTEGER); + let max = BigInt(Number.MIN_SAFE_INTEGER); + + // Iterate over all of the measurements + for (const measurement of this.#data) { + // Do not consider measurements that are less than 0ns (invalid) + if (measurement >= 0) { + // Update the sum + sum += BigInt(measurement); + + // Update the min and max + if (measurement < min) min = measurement; + if (measurement > max) max = measurement; + } + } + + // Calculate the average + average = sum / BigInt(this.#data.length); + + // Return the statistics object + return { + min: min.toString(), + max: max.toString(), + sum: sum.toString(), + count: this.#data.length.toString(), + average: average.toString(), + }; + } +} + +module.exports = PerformanceMeasurement; diff --git a/packages/hyper-express/tests/scripts/MemoryStore.js b/packages/hyper-express/tests/scripts/MemoryStore.js new file mode 100644 index 0000000..7f79e8c --- /dev/null +++ b/packages/hyper-express/tests/scripts/MemoryStore.js @@ -0,0 +1,80 @@ +// Memory store with simulated functionalities similar to SQL databases +class MemoryStore { + #container = {}; + constructor() {} + + /** + * This method can be used to lookup/select specific keys from store + * + * @param {String} key + * @returns {Any} Any OR undefined + */ + select(key) { + return this.#container?.[key]?.data; + } + + /** + * + * @param {String} key + * @param {Object} data + * @param {Number} expiry_ts In Milliseconds + */ + insert(key, data, expiry_ts) { + // Throw on overwrites + if (this.#container[key]) + throw new Error('MemoryStore: key ' + key + ' already exists. Use update() method.'); + + this.#container[key] = { + data: data, + expiry: expiry_ts, + }; + } + + update(key, data, expiry_ts) { + // Throw on non existent source + if (this.#container[key] == undefined) + throw new Error( + 'MemoryStore: key ' + key + ' does not exist in store. Use insert() method.' + ); + + this.#container[key].data = data; + if (typeof expiry_ts == 'number') this.#container[key].expiry = expiry_ts; + } + + touch(key, expiry_ts) { + // Throw on non existent source + if (this.#container[key] == undefined) + throw new Error( + 'MemoryStore: cannot touch key ' + key + ' because it does not exist in store.' + ); + + this.#container[key].expiry = expiry_ts; + } + + delete(key) { + delete this.#container[key]; + } + + empty() { + this.#container = {}; + } + + cleanup() { + let removed = 0; + Object.keys(this.#container).forEach((key) => { + let data = this.#container[key]; + let expiry = data.expiry; + if (expiry < Date.now()) { + delete this.#container[key]; + removed++; + } + }); + return removed; + } + + get data() { + return this.#container; + } +} + +module.exports = MemoryStore; diff --git a/packages/hyper-express/tests/scripts/operators.js b/packages/hyper-express/tests/scripts/operators.js new file mode 100644 index 0000000..969c75b --- /dev/null +++ b/packages/hyper-express/tests/scripts/operators.js @@ -0,0 +1,82 @@ +const crypto = require('crypto'); +const HTTP = require('http'); + +function log(logger = 'SYSTEM', message) { + let dt = new Date(); + let timeStamp = dt.toLocaleString([], { hour12: true, timeZone: 'America/New_York' }).replace(', ', ' ').split(' '); + timeStamp[1] += ':' + dt.getMilliseconds().toString().padStart(3, '0') + 'ms'; + timeStamp = timeStamp.join(' '); + console.log(`[${timeStamp}][${logger}] ${message}`); +} + +function random_string(length = 7) { + var result = []; + var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + var charactersLength = characters.length; + for (var i = 0; i < length; i++) { + result.push(characters.charAt(Math.floor(Math.random() * charactersLength))); + } + return result.join(''); +} + +async function assert_log(group, target, assertion) { + try { + let result = await assertion(); + if (result) { + log(group, 'Verified ' + target); + } else { + throw new Error('Failed To Verify ' + target + ' @ ' + group + ' -> ' + assertion.toString()); + } + } catch (error) { + console.log(error); + throw new Error('Failed To Verify ' + target + ' @ ' + group + ' -> ' + assertion.toString()); + } +} + +function async_for_each(items, handler, cursor = 0, final) { + if (final == undefined) return new Promise((resolve, reject) => async_for_each(items, handler, cursor, resolve)); + if (cursor < items.length) return handler(items[cursor], () => async_for_each(items, handler, cursor + 1, final)); + return final(); // Resolve master promise +} + +function http_post_headers({ host, port, path, method = 'GET', body, headers = {}, silence_errors = false }) { + return new Promise((resolve, reject) => { + const request = HTTP.request({ + host, + port, + path, + method, + headers, + }); + + if (body) request.write(body); + + request.on('response', (response) => + resolve({ + url: response.url, + status: response.statusCode, + headers: response.headers, + }) + ); + + if (!silence_errors) request.on('error', reject); + }); +} + +function async_wait(delay) { + return new Promise((resolve, reject) => setTimeout((res) => res(), delay, resolve)); +} + +function md5_from_buffer(buffer) { + return crypto.createHash('md5').update(buffer).digest('hex'); +} + +module.exports = { + log: log, + random_string: random_string, + assert_log: assert_log, + async_for_each: async_for_each, + http_post_headers: http_post_headers, + async_wait: async_wait, + md5_from_buffer: md5_from_buffer, +}; diff --git a/packages/hyper-express/tests/ssl/dummy-cert.pem b/packages/hyper-express/tests/ssl/dummy-cert.pem new file mode 100644 index 0000000..c90b84c --- /dev/null +++ b/packages/hyper-express/tests/ssl/dummy-cert.pem @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +MIIF7zCCA9egAwIBAgIUHQd5vSaP28tenjjSWoPLQ31Kf7IwDQYJKoZIhvcNAQEL +BQAwgYYxCzAJBgNVBAYTAlhYMRIwEAYDVQQIDAlTdGF0ZU5hbWUxETAPBgNVBAcM +CENpdHlOYW1lMRQwEgYDVQQKDAtDb21wYW55TmFtZTEbMBkGA1UECwwSQ29tcGFu +eVNlY3Rpb25OYW1lMR0wGwYDVQQDDBRDb21tb25OYW1lT3JIb3N0bmFtZTAeFw0y +MzA1MTYyMzMyNTVaFw0zMzA1MTMyMzMyNTVaMIGGMQswCQYDVQQGEwJYWDESMBAG +A1UECAwJU3RhdGVOYW1lMREwDwYDVQQHDAhDaXR5TmFtZTEUMBIGA1UECgwLQ29t +cGFueU5hbWUxGzAZBgNVBAsMEkNvbXBhbnlTZWN0aW9uTmFtZTEdMBsGA1UEAwwU +Q29tbW9uTmFtZU9ySG9zdG5hbWUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK +AoICAQC/48UKxs++PJD+MniBEF0yc2crUS2fPg1NA0j92sg0Vlh28maZBDtQMJw6 +DZDVb74paw8jLSZHdmTQEKsE0uvAl++gvN6pZulpbTWlNlLcvSG2FEYnOCb/Vxwu +Mfqx3ijANtrH4fx3eYB2TV1gZkXcmncO7a048WuBlpaVFDqvmJiObKeKA6XXnqC4 +ih19R4Yqq+bBzIdSYeS89n8GaOrO9Y7AbMtuMT+x1YtfUPsipbz4/76qqX4WC9Ph +qpLAHY71suQTT5l2N+eY3uuvqTRwxxda7Xdq/ABp1/kmStQwVTw/iRQPrCr1DxJy +8jm1Dyk3IlvBNv8oMCdrf+rfmljPIxA65LiRn/8oE+D32NbhDAo8zGzXS9BhBqtY +6Qj8HgSTiJhLw2ZzIfLWKRbfUO6dv3XsKx8vbF+kMqPZ1E+bACv3lCP6XJsihugM +Y3uVyyAFIAyRuczbRsLAtrZwbxYPQ/gvKWxNAg63Iwp/kluGwO6om+pp913z9YJW +Qr9pIO/ISayiKzXN7tqJ/BgTgLlD0fWexJOXkccfT9NjSV3DIq9m2McwN8GHspnh +lf8aZO4X48RGIS+vkH1oxgLJaZt0zmX43aZm83Aip4ykQyYg6ixDObrrpSG76UiT +H98s7+CAFFWO5usPIiHE5yQ9NTaBjHwMXm/3uLpEQ+ESKMCkwQIDAQABo1MwUTAd +BgNVHQ4EFgQULzXMAU/+wT/ukDydvZF6qhMv2kwwHwYDVR0jBBgwFoAULzXMAU/+ +wT/ukDydvZF6qhMv2kwwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC +AgEAjcDsY7fFij4iiU0KpxecTJFg3nGGz4xFsln6IedUV9K2Gw5ZoAwyGc6BO29j ++bFTAbzbs+OHmExDyX1Y0t++Gj2MN6NP66yLvVaTJCQZHIshg6PTQx4u4EthwVb8 +k5NR0moSt2kk7rBWXlcWiZLAASruEFoZGlT1ofNMjGiRd1l1iQEm1+QQN95GTF8L +M8m/s36dHz1bsRa2BdBZnN9Qv0KvS43zigjW/d6+jJLePVJ+enwSn4fLCW+Nyx3i +7XSVqAUt/lW+h4jza3TF5jkvlRPOAc5+MJOemKoS1uGS+lPg0Z8jv+nPNkEmUcd8 +t+KKJAI/0E1nvEMpW3Rr6XVgIqlKSketddeDG+3t3m4ZOU14tiSpp6MTS4DGcPO/ +vhgWIFOi2ABeYhhzBrYs+jgpc1ogT6OHXBrhVNfLBXQ15II+cfQDBkhK526TC/EW +P9oYFX5RD5TTVMWYKfSDfL9nIPH7YtA0AeNx9iNb91V0mz2Ma5+/WVi7DAijdVFn +0n0+isn52QqNcUUrXvesY8Gm/QaClZipKk/Vq6DLdPOLZuZR2YcKh5OGRlxP8ouY +IrNf9XSm5yAm0qbbHQiQlTeg+pIu8UJkPpg20/vhqiLPygYOkD5YKrW0cwVZXFZq ++EzEmM77LOI6sp1aRq+GAVv5gfB4AqrfhxGAXK9TYnVQ7w4= +-----END CERTIFICATE----- diff --git a/packages/hyper-express/tests/ssl/dummy-key.pem b/packages/hyper-express/tests/ssl/dummy-key.pem new file mode 100644 index 0000000..c1f1057 --- /dev/null +++ b/packages/hyper-express/tests/ssl/dummy-key.pem @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQC/48UKxs++PJD+ +MniBEF0yc2crUS2fPg1NA0j92sg0Vlh28maZBDtQMJw6DZDVb74paw8jLSZHdmTQ +EKsE0uvAl++gvN6pZulpbTWlNlLcvSG2FEYnOCb/VxwuMfqx3ijANtrH4fx3eYB2 +TV1gZkXcmncO7a048WuBlpaVFDqvmJiObKeKA6XXnqC4ih19R4Yqq+bBzIdSYeS8 +9n8GaOrO9Y7AbMtuMT+x1YtfUPsipbz4/76qqX4WC9PhqpLAHY71suQTT5l2N+eY +3uuvqTRwxxda7Xdq/ABp1/kmStQwVTw/iRQPrCr1DxJy8jm1Dyk3IlvBNv8oMCdr +f+rfmljPIxA65LiRn/8oE+D32NbhDAo8zGzXS9BhBqtY6Qj8HgSTiJhLw2ZzIfLW +KRbfUO6dv3XsKx8vbF+kMqPZ1E+bACv3lCP6XJsihugMY3uVyyAFIAyRuczbRsLA +trZwbxYPQ/gvKWxNAg63Iwp/kluGwO6om+pp913z9YJWQr9pIO/ISayiKzXN7tqJ +/BgTgLlD0fWexJOXkccfT9NjSV3DIq9m2McwN8GHspnhlf8aZO4X48RGIS+vkH1o +xgLJaZt0zmX43aZm83Aip4ykQyYg6ixDObrrpSG76UiTH98s7+CAFFWO5usPIiHE +5yQ9NTaBjHwMXm/3uLpEQ+ESKMCkwQIDAQABAoICAF+mjeXdTFiroCrVxbOwEITB +eb/h6zfhmoe1B4FiuUE9eUNxeSr1LQu/72AQuw1pcgT7VMRYESi2H3KHnHf/G30Z +P12ESAlxPxBKW99KwOs/a7pzSLTsDKRjK6zrROe8sdt+fHf+cfasHhjaX51Z3aEl +bguG9j3YOZqTEeSl/Mri6ci06J6nSte8Pqk+T4zPRlWm8pPP+/RYz8hRpufvDHy1 +cr8AfDclXXar15lfqI+Qxi3obYZsjmk25BstB5G0KjrXPVFS8FA5dbyCAkHBul4t +H7s3e7tcemhIO+2Wh0bAdhPFpLZbP95/8NZTX+ic8hKFke8yFuZVepDfZpinO3TH +LAJvmnWwKnYgn3jE8ffLytTzek2Mpo+K/87ACgr8wQn5gXLekxtyrX8OQ+zitqSf ++w8Wzv8x5NOOtfcUYSRTzpPxdCjx0ZWa9CU+w23rJPJgjAGYLbNoVJmI12ZSLNGR +j/ETODrSUFRKFxELNfCdZEH1QBE+PbaE/pyufUlsR1g5nY9jSg9jgG7Yco9CNvS4 +mf1FoOmV2nC+8ooeAAbCjHbbmS8OERqhInTJz+CrMThX+L4t89B4vXWOb/M7Yed5 +QXqcpWbk1Goai8Enz6hMYkXpwFkgcVVMYNiY5fOCLF5+GDOYQFUgzUeUaNK+V0Wd +T4XjvbwsWQBRNuuzfU9xAoIBAQD0E+62on44lYQe4GIeBKJX5St6325sBXtAYX8D +dY7d5qPsJ8RHvbXFnIXH1r+lmq6vTgGehCa5OMlBnt6yJJr9kqs9ujcrG+SBpLRN +LXA8g7n2BrCeEmEj8JfgW2ekIk3Vso2IoWnYMK+zIXiquNaBw0yvh6uMpy4aqrmF +cPGnuMLz/txSzGz2W6jkI3ci7WdlcBBSQLDPxIwXNzdVSTLorvfED8CzPPOke9W6 +cRaB4HbmdmIc70sVgBUUyn2FR1W6JKRu8t/FuOoOyyrkSwpiAwJEhNjzbJAI1zzz +Kg+us7orpAm4tpat4EA7F/lavvt7A5SRuBp/r/xmvfM45ykHAoIBAQDJQ0Blp7KW +hiWkJjhZCF+CQNWGOAwhbfTG456HgFhKJbg8FmhTRTuTX2wHU+jTNAd1+BcLoho3 +DdyDLB4g0YCtOMlwFZynblHzwirFFVfQujd/rAagq7906kfzyCe39qQyrAxd2/xw +hu4vV988FrM61jq3bVGxJ+S1iDNYw/t1Mlis3lHEBo5o3weENmBVWNSvXFqCgy4M +LHa/lw3TdstzHpvXj5MRyqFkDwRFFyIMeja/68MNG1Kr97q57BUCyaUutBZZ80np +YzkbO7FziZ6qbuLDksVbtRBPe8xllHLr2lwxLMIU4NRZyAT86qNGsApPnr9ODahr +n3MSdk4X3rn3AoIBAQDTsvIqsJe/5lcZHM+db7GLgPcMdPzmbn6voaCz1GQdLW3i +Z7+D5hTiGFektCu3rIl0/bjDz6Vyo8FTzEMlykAwTeV+/aPaHTA+DihghFfD9RD3 +RmgsQo7EyGpCq6UiJKrT/jFqX25ZmCjcutxZX0aWeFlsKcVukpaXhJqzFfpT2hol +3Vkl669aorfDYMt1nOpAfkl5vihdnQFRJZA1xe6FCTVXdb5S+Dvu34XKV0oJTjJy +xB1nMVozhMtEJDlovy2o7R0+KiRS74b7W9aQ+lFAH5H48izmPbRUJrPzyPifM733 +Gilgb+YTW9z6JFogDmQ7FyjmlwNM2syWJIzwPvdDAoIBAQC0z1tCODdD7X5Rixii +O9h6Dz8E1sNnIP5/06vvNcmby2lJaiQNcyxDiL1nk+WeIKb3P4uMovQEM8rAeVkT +yMNeW570uCXFcWHkqLJ93l/HIBSN+YD2xXU6VuOPSmkMZ2M6NsDhbanLehzvoXTm +6cnY+O9FLMvwaNOalqLygxccQb/SheRVREKaSovZJnTDGAvzAvg5OhqbSzLfipgc +OyQp5vzA2raYjD8Twj3myBKJvR4Eq4zO8JYD8onpUAPMPlXMsHNIGj5zkvWR1r3j ++2X03auRYgE2E2N01NZbB9N6ufCLKRevZBDCG+UHRtCqx6prv0VEnRaKoXPiyS/9 +V9YfAoIBAHBh1gQ67T0FZt05oenLdhH7skS/m2oSPD1qsdgOxya+e1pjnI9krtKg +QY7heQNPWUtcC0YLwC29PoWe125eYWzYLH0pTCIOe1l+MJoAvL2EI/+yvXvjEoFn +FkhpO3N0h9JTMmrBKpVTkPVfDQGdHb1Vtm8Jx1Qo+Gzj5gix/QUfqn0Gm4yQiHQU +5WGrp/7EQ/NR/IPkZQfLmpzq/oIlzoN5IqtSq/LmuNXZacmXZVqkI5UfjPS1oR/C +QtTD60R4/QrAzWmfFp3Wo7CpbOhk6WbAMdifxY5V3JtBHuxn1vdmFkqZlEkGE7bY +qK9UJyw/XeyqX7BsMcKq5pQ1ywSq6yo= +-----END PRIVATE KEY----- diff --git a/packages/hyper-express/tests/types/Router.ts b/packages/hyper-express/tests/types/Router.ts new file mode 100644 index 0000000..5d8c2a8 --- /dev/null +++ b/packages/hyper-express/tests/types/Router.ts @@ -0,0 +1,127 @@ +// THIS FILE CAN BE TYPE CHECKED TO ENSURE THE TYPES ARE CORRECT + +import { Router, Request, Response, MiddlewareNext } from '../../types/index'; + +// Create a new router instance +const router = new Router(); + +// Pattern + Handler +router.any('/', async (request, response) => { + const body = await request.json(); +}); + +// Pattern + Options + Handler +router.all( + '/', + { + max_body_length: 250, + }, + async (request, response) => { + const body = await request.json(); + } +); + +const middleware = (request: Request, response: Response, next: MiddlewareNext) => {}; + +// Pattern + 2 Middlewares + Handler +router.connect( + '/', + middleware, + async (request, repsonse, next) => { + await request.text(); + next(); + }, + async (request, response) => { + const body = await request.json(); + } +); + +// Pattern + options + 4 Middlewares + Handler +router.post( + '/', + { + max_body_length: 250, + }, + middleware, + middleware, + middleware, + async (request, repsonse, next) => { + await request.text(); + next(); + }, + async (request, response) => { + const body = await request.json(); + } +); + +// Pattern + 4 Middlewares (Array) + Handler +router.put( + '/', + [ + middleware, + middleware, + middleware, + async (request, repsonse, next) => { + await request.text(); + next(); + }, + ], + async (request, response) => { + const body = await request.json(); + } +); + +// Pattern + options + 4 Middlewares (Array) + Handler +router.delete( + '/', + { + max_body_length: 250, + }, + [ + middleware, + middleware, + middleware, + async (request, repsonse, next) => { + await request.text(); + next(); + }, + ], + async (request, response) => { + const body = await request.json(); + } +); + +// Handler +router + .route('/api/v1') + .get(async (request, response) => { + const body = await request.json(); + }) + .post( + { + max_body_length: 250, + }, + async (request, response, next) => { + const body = await request.json(); + }, + async (request, response) => { + const body = await request.json(); + } + ) + .delete( + { + max_body_length: 250, + }, + middleware, + [middleware, middleware], + async (request, response) => { + const body = await request.json(); + } + ); + +// Ensures router usage is valid in all possible forms +router.use(router); +router.use('/something', router); +router.use('/something', middleware); +router.use(middleware, middleware, middleware); +router.use('else', middleware, [middleware, middleware, middleware], middleware); diff --git a/packages/hyper-express/tsconfig.json b/packages/hyper-express/tsconfig.json new file mode 100644 index 0000000..e80a450 --- /dev/null +++ b/packages/hyper-express/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2021", + "moduleResolution": "node", + "typeRoots": [ + "./types/*.d.ts", + "./node_modules/@types" + ], + "types": [ + "node", + "express" + ] + }, + "exclude": [ + "./src" + ] +} \ No newline at end of file diff --git a/packages/hyper-express/types/components/Server.d.ts b/packages/hyper-express/types/components/Server.d.ts new file mode 100644 index 0000000..82141f0 --- /dev/null +++ b/packages/hyper-express/types/components/Server.d.ts @@ -0,0 +1,145 @@ +import { ReadableOptions, WritableOptions } from 'stream'; +import * as uWebsockets from 'uWebSockets.js'; +import { SendableData } from './http/Response'; +import { Request } from './http/Request'; +import { Response } from './http/Response'; +import { Router } from './router/Router'; +import { HostManager } from './plugins/HostManager'; + +export interface ServerConstructorOptions { + key_file_name?: string; + cert_file_name?: string; + passphrase?: string; + dh_params_file_name?: string; + ssl_prefer_low_memory_usage?: boolean; + auto_close?: boolean; + fast_buffers?: boolean; + fast_abort?: boolean; + trust_proxy?: boolean; + max_body_buffer?: number; + max_body_length?: number; + streaming?: { + readable?: ReadableOptions; + writable?: WritableOptions; + }; +} + +export type GlobalErrorHandler = (request: Request, response: Response, error: Error) => void; +export type GlobalNotFoundHandler = (request: Request, response: Response) => void; + +export class Server extends Router { + constructor(options?: ServerConstructorOptions); + + /** + * This object can be used to store properties/references local to this Server instance. + */ + locals: Object; + + /* Server Methods */ + + /** + * Starts HyperExpress webserver on specified port and host. + * + * @param {Number} port + * @param {String=} host Optional. Default: 0.0.0.0 + * @param {Function=} callback Optional. Callback to be called when the server is listening. Default: "0.0.0.0" + * @returns {Promise} Promise + */ + listen( + port: number, + callback?: (listen_socket: uWebsockets.us_listen_socket) => void + ): Promise; + listen( + port: number, + host?: string, + callback?: (listen_socket: uWebsockets.us_listen_socket) => void + ): Promise; + listen( + unix_path: string, + callback?: (listen_socket: uWebsockets.us_listen_socket) => void + ): Promise; + + /** + * Performs a graceful shutdown of the server and closes the listen socket once all pending requests have been completed. + * + * @param {uWebSockets.us_listen_socket=} [listen_socket] Optional + * @returns {Promise} + */ + shutdown(listen_socket?: uWebsockets.us_listen_socket): Promise; + + /** + * Stops/Closes HyperExpress webserver instance. + * + * @param {uWebSockets.us_listen_socket=} [listen_socket] Optional + * @returns {Boolean} + */ + close(listen_socket?: uWebsockets.us_listen_socket): boolean; + + /** + * Sets a global error handler which will catch most uncaught errors across all routes/middlewares. + * + * @param {GlobalErrorHandler} handler + */ + set_error_handler(handler: GlobalErrorHandler): void; + + /** + * Sets a global not found handler which will handle all requests that are unhandled by any registered route. + * Note! This handler must be registered after all routes and routers. + * + * @param {GlobalNotFoundHandler} handler + */ + set_not_found_handler(handler: GlobalNotFoundHandler): void; + + /** + * Publish a message to a topic in MQTT syntax to all WebSocket connections on this Server instance. + * You cannot publish using wildcards, only fully specified topics. + * + * @param {String} topic + * @param {String|Buffer|ArrayBuffer} message + * @param {Boolean=} is_binary + * @param {Boolean=} compress + * @returns {Boolean} + */ + publish(topic: string, message: SendableData, is_binary?: boolean, compress?: boolean): boolean; + + /** + * Returns the number of subscribers to a topic across all WebSocket connections on this Server instance. + * + * @param {String} topic + * @returns {Number} + */ + num_of_subscribers(topic: string): number; + + /* Server Properties */ + + /** + * Returns the local server listening port of the server instance. + * @returns {Number} + */ + get port(): number; + + /** + * Returns the server's internal uWS listening socket. + * @returns {uWebSockets.us_listen_socket=} + */ + get socket(): uWebsockets.us_listen_socket | null; + + /** + * Underlying uWS instance. + * @returns {uWebSockets.TemplatedApp} + */ + get uws_instance(): uWebsockets.TemplatedApp; + + /** + * Server instance global handlers. + * @returns {Object} + */ + get handlers(): Object; + + /** + * Returns the Server Hostnames manager for this instance. + * Use this to support multiple hostnames on the same server with different SSL configurations. + * @returns {HostManager} + */ + get hosts(): HostManager; +} diff --git a/packages/hyper-express/types/components/http/Request.d.ts b/packages/hyper-express/types/components/http/Request.d.ts new file mode 100644 index 0000000..41555cf --- /dev/null +++ b/packages/hyper-express/types/components/http/Request.d.ts @@ -0,0 +1,258 @@ +import { Readable } from 'stream'; +import { Server } from '../Server'; +import { BusboyConfig } from 'busboy'; +import { HttpRequest } from 'uWebSockets.js'; +import { Options, Ranges, Result } from 'range-parser'; +import { MultipartHandler } from '../plugins/MultipartField'; +import { UploadedFile } from '../../shared/uploaded-file'; + +type default_value = any; + +interface ParamsDictionary { + [key: string]: string; +} + +interface ParsedQs { + [key: string]: undefined | string | string[] | ParsedQs | ParsedQs[]; +} + +type DefaultRequestLocals = { + [key: string]: any; +}; + +export class Request extends Readable { + /** + * Underlying raw lazy initialized readable body stream. + */ + _readable: null | Readable; + + /** + * Returns whether all expected incoming request body chunks have been received. + * @returns {Boolean} + */ + received: boolean; + + /* HyperExpress Methods */ + + /** + * Returns the raw uWS.HttpRequest instance. + * Note! This property is unsafe and should not be used unless you have no asynchronous code or you are accessing from the first top level synchronous middleware before any asynchronous code. + * @returns {import('uWebSockets.js').HttpRequest} + */ + get raw(): HttpRequest; + + /** + * Securely signs a value with provided secret and returns the signed value. + * + * @param {String} string + * @param {String} secret + * @returns {String} String OR undefined + */ + sign(string: string, secret: string): string | void; + + /** + * Securely unsigns a value with provided secret and returns its original value upon successful verification. + * + * @param {String} signed_value + * @param {String} secret + * @returns {String=} String OR undefined + */ + unsign(signed_value: string, secret: string): string | void; + + /** + * Downloads and returns request body as a Buffer. + * @returns {Promise} + */ + buffer(): Promise; + + /** + * Downloads and parses the request body as a String. + * @returns {Promise} + */ + text(): Promise; + + /** + * Downloads and parses the request body as a JSON object. + * Passing default_value as undefined will lead to the function throwing an exception if invalid JSON is received. + * + * @param {Any} default_value Default: {} + * @returns {Promise} + */ + json(default_value?: D): Promise; + + /** + * Parses and resolves an Object of urlencoded values from body. + * @returns {Promise} + */ + urlencoded(): Promise; + + /** + * Parses incoming multipart form and allows for easy consumption of fields/values including files. + * + * @param {MultipartHandler} handler + * @returns {Promise} A promise which is resolved once all multipart fields have been processed + */ + multipart(handler: MultipartHandler): Promise; + + /** + * Parses incoming multipart form and allows for easy consumption of fields/values including files. + * + * @param {BusboyConfig} options + * @param {MultipartHandler} handler + * @returns {Promise} A promise which is resolved once all multipart fields have been processed + */ + multipart(options: BusboyConfig, handler: MultipartHandler): Promise; + + /* HyperExpress Properties */ + + /** + * Returns the HyperExpress.Server instance this Request object originated from. + * @returns {Server} + */ + get app(): Server; + + /** + * Returns whether this request is in a paused state and thus not consuming any body chunks. + * @returns {Boolean} + */ + get paused(): boolean; + + /** + * Returns HTTP request method for incoming request in all uppercase. + * @returns {String} + */ + get method(): string; + + /** + * Returns full request url for incoming request (path + query). + * @returns {String} + */ + get url(): string; + + /** + * Returns path for incoming request. + * @returns {String} + */ + get path(): string; + + /** + * Returns query for incoming request without the '?'. + * @returns {String} + */ + get path_query(): string; + + /** + * Returns request headers from incoming request. + * @returns {Object.} + */ + get headers(): { [key: string]: string }; + + /** + * Returns request cookies from incoming request. + * @returns {Object.} + */ + get cookies(): { [key: string]: string }; + + /** + * Returns path parameters from incoming request. + * @returns {Object.} + */ + get path_parameters(): { [key: string]: string }; + + /** + * Returns query parameters from incoming request. + * @returns {Object.} + */ + get query_parameters(): { [key: string]: string }; + + /** + * Returns remote IP address in string format from incoming request. + * @returns {String} + */ + get ip(): string; + + /** + * Returns remote proxy IP address in string format from incoming request. + * @returns {String} + */ + get proxy_ip(): string; + + /* ExpressJS Methods */ + get(name: 'set-cookie'): string[]; + get(name: string): string; + header(name: 'set-cookie'): string[]; + header(name: string): string; + accepts(): string[]; + accepts(type: string): string | false; + accepts(type: string[]): string | false; + accepts(...type: string[]): string | false; + acceptsCharsets(): string[]; + acceptsCharsets(charset: string): string | false; + acceptsCharsets(charset: string[]): string | false; + acceptsCharsets(...charset: string[]): string | false; + acceptsEncodings(): string[]; + acceptsEncodings(encoding: string): string | false; + acceptsEncodings(encoding: string[]): string | false; + acceptsEncodings(...encoding: string[]): string | false; + acceptsLanguages(): string[]; + acceptsLanguages(lang: string): string | false; + acceptsLanguages(lang: string[]): string | false; + acceptsLanguages(...lang: string[]): string | false; + range(size: number, options?: Options): Ranges | Result; + param(name: string, defaultValue?: any): string; + is(type: string | string[]): string | false; + + /* ExpressJS Properties */ + locals: Locals; + protocol: string; + secure: boolean; + ips: string[]; + subdomains: string[]; + hostname: string; + fresh: boolean; + stale: boolean; + xhr: boolean; + body: any; + params: ParamsDictionary; + query: ParsedQs; + originalUrl: string; + baseUrl: string; + + $dto: any; + $user: any; + // uploadedFiles: Map; + setDto(dto: any): void; + dto(): any; + + processBody(): Promise; + all(): Promise>; + input: (name: string, defaultValue?: T) => T; + string: (name: string) => string; + number: (name: string) => number; + boolean: (name: string) => boolean; + has: (...keys: string[]) => boolean; + hasAny: (...keys: string[]) => boolean; + hasHeader(name: string): boolean; + bearerToken(): string; + httpHost(): string; + isHttp(): boolean; + isHttps(): boolean; + fullUrl(): string; + isMethod(method: string): boolean; + contentType(): string; + getAcceptableContentTypes(): string; + // accepts(): string[]; + expectsJson(): boolean; + setUser(user: any): void; + user(): T; + isPath(pathPattern: string): boolean; + hasHeaders(...keys: string[]): boolean; + hasIncludes(): boolean; + includes(): string; + + setValidator(cls: any): void; + validate(schema: any): Promise; + + // files(keys: string): Record; + file(key: string): Promise; +} diff --git a/packages/hyper-express/types/components/http/Response.d.ts b/packages/hyper-express/types/components/http/Response.d.ts new file mode 100644 index 0000000..94c90d4 --- /dev/null +++ b/packages/hyper-express/types/components/http/Response.d.ts @@ -0,0 +1,275 @@ +import { Readable, Writable } from 'stream'; +import * as uWebsockets from 'uWebSockets.js'; +import { LiveFile } from '../plugins/LiveFile'; +import { Server } from '../Server'; + +export type SendableData = string | Buffer | ArrayBuffer; +export type FileCachePool = { + [key: string]: LiveFile; +}; + +export interface CookieOptions { + domain?: string; + path?: string; + maxAge?: number; + secure?: boolean; + httpOnly?: boolean; + sameSite?: boolean | 'none' | 'lax' | 'strict'; + secret?: string; +} + +type DefaultResponseLocals = { + [key: string]: any; +}; + +export class Response extends Writable { + /** + * Underlying raw lazy initialized writable stream. + */ + _writable: null | Writable; + + /** + * Alias of aborted property as they both represent the same request state in terms of inaccessibility. + */ + completed: boolean; + + /* HyperExpress Methods */ + + /** + * Alias of `uWS.HttpResponse.cork()` which allows for manual corking of the response. + * This is required by `uWebsockets.js` to maximize network performance with batched writes. + * + * @param {Function} handler + * @returns {Response} Response (Chainable) + */ + atomic(handler: () => void): Response; + + /** + * This method is used to set a custom response code. + * + * @param {Number} code Example: response.status(403) + * @param {String=} message Example: response.status(403, 'Forbidden') + * @returns {Response} Response (Chainable) + */ + status(code: number, message?: string): Response; + + /** + * This method is used to set the response content type header + * based on the provided mime type. Example: type('json') + * + * @param {String} mime_type Mime type + * @returns {Response} Response (Chainable) + */ + type(mime_type: string): Response; + + /** + * This method can be used to write a response header and supports chaining. + * + * @param {String} name Header Name + * @param {String|Array} value Header Value + * @param {Boolean=} overwrite If true, overwrites existing header value with same name + * @returns {Response} Response (Chainable) + */ + header(name: string, value: string | Array, overwrite?: boolean): Response; + + /** + * This method is used to write a cookie to incoming request. + * To delete a cookie, set the value to null. + * + * @param {String} name Cookie Name + * @param {String|null} value Cookie Value + * @param {Number=} expiry In milliseconds + * @param {CookieOptions=} options Cookie Options + * @param {Boolean=} sign_cookie Enables/Disables Cookie Signing + * @returns {Response} Response (Chainable) + */ + cookie( + name: string, + value: string | null, + expiry?: number, + options?: CookieOptions, + sign_cookie?: boolean + ): Response; + + /** + * This method is used to upgrade an incoming upgrade HTTP request to a Websocket connection. + * @param {Object=} context Store information about the websocket connection + */ + upgrade(context?: Object): void; + + /** + * Binds a drain handler which gets called with a byte offset that can be used to try a failed chunk write. + * You MUST perform a write call inside the handler for uWS chunking to work properly. + * You MUST return a boolean value indicating if the write was successful or not. + * + * @param {function(number):boolean} handler Synchronous callback only + */ + drain(handler: (offset: number) => boolean): void; + + /** + * This method is used to end the current request and send response with specified body and headers. + * + * @param {String|Buffer|ArrayBuffer} body Optional + * @returns {Boolean} 'false' signifies that the result was not sent due to built up backpressure. + */ + send(body?: SendableData, close_connection?: boolean): Response; + + /** + * This method is used to pipe a readable stream as response body and send response. + * By default, this method will use chunked encoding transfer to stream data. + * If your use-case requires a content-length header, you must specify the total payload size. + * + * @param {Readable} readable A Readable stream which will be piped as response body + * @param {Number=} total_size Total size of the Readable stream source in bytes (Optional) + */ + stream(readable: Readable, total_size?: number): Promise; + + /** + * Instantly aborts/closes current request without writing a status response code. + * Use this only in extreme situations to abort a request where a proper response is not neccessary. + */ + close(): void; + + /** + * This method is used to redirect an incoming request to a different url. + * + * @param {String} url Redirect URL + * @returns {Boolean} + */ + redirect(url: string): boolean; + + /** + * This method is an alias of send() method except it accepts an object and automatically stringifies the passed payload object. + * + * @param {Object} body JSON body + * @returns {Boolean} Boolean + */ + json(body: any): boolean; + + /** + * This method is an alias of send() method except it accepts an object + * and automatically stringifies the passed payload object with a callback name. + * Note! This method uses 'callback' query parameter by default but you can specify 'name' to use something else. + * + * @param {Object} body + * @param {String=} name + * @returns {Boolean} Boolean + */ + jsonp(body: any, name?: string): boolean; + + /** + * This method is an alias of send() method except it automatically sets + * html as the response content type and sends provided html response body. + * + * @param {String} body + * @returns {Boolean} Boolean + */ + html(body: string): boolean; + + /** + * This method is an alias of send() method except it sends the file at specified path. + * This method automatically writes the appropriate content-type header if one has not been specified yet. + * This method also maintains its own cache pool in memory allowing for fast performance. + * Avoid using this method to a send a large file as it will be kept in memory. + * + * @param {String} path + * @param {function(Object):void=} callback Executed after file has been served with the parameter being the cache pool. + */ + file(path: string, callback?: (pool: FileCachePool) => void): void; + + /** + * Writes approriate headers to signify that file at path has been attached. + * + * @param {String} path + * @param {String=} name + * @returns {Response} Chainable + */ + attachment(path: string, name?: string): Response; + + /** + * Writes appropriate attachment headers and sends file content for download on user browser. + * This method combined Response.attachment() and Response.file() under the hood, so be sure to follow the same guidelines for usage. + * + * @param {String} path + * @param {String=} filename + */ + download(path: string, filename?: string): void; + + /** + * This method allows you to throw an error which will be caught by the global error handler (If one was setup with the Server instance). + * + * @param {Error} error + */ + throw(error: Error): Response; + + /* HyperExpress Properties */ + + /** + * Returns the underlying raw uWS.Response object. + * @returns {uWebsockets.Response} + */ + get raw(): uWebsockets.HttpResponse; + + /** + * Returns the HyperExpress.Server instance this Response object originated from. + * + * @returns {Server} + */ + get app(): Server; + + /** + * Returns whether response has been initiated by writing the HTTP status code and headers. + * Note! No changes can be made to the HTTP status code or headers after a response has been initiated. + * @returns {Boolean} + */ + get initiated(): boolean; + + /** + * Returns current state of request in regards to whether the source is still connected. + * @returns {Boolean} + */ + get aborted(): boolean; + + /** + * Returns the current response body content write offset in bytes. + * Use in conjunction with the drain() offset handler to retry writing failed chunks. + * @returns {Number} + */ + get write_offset(): number; + + /** + * Upgrade socket context for upgrade requests. + * @returns {uWebsockets.ux_socket_context} + */ + get upgrade_socket(): uWebsockets.us_socket_context_t; + + /* ExpressJS Methods */ + append(name: string, values: string | Array): Response; + writeHead(name: string, values: string | Array): Response; + setHeader(name: string, values: string | Array): Response; + writeHeaders(headers: Object): void; + setHeaders(headers: Object): void; + writeHeaderValues(name: string, values: Array): void; + getHeader(name: string): string | Array | void; + getHeaders(): { [key: string]: Array }; + removeHeader(name: string): void; + setCookie(name: string, value: string, options?: CookieOptions): Response; + hasCookie(name: string): Boolean; + removeCookie(name: string): Response; + clearCookie(name: string): Response; + get(name: string): string | Array; + links(links: Object): string; + location(path: string): Response; + sendFile(path: string): void; + sendStatus(status_code: number): Response; + set(field: string | object, value?: string | Array): Response | void; + vary(name: string): Response; + + /* ExpressJS Properties */ + get headersSent(): boolean; + get statusCode(): number | undefined; + set statusCode(value: number | undefined); + get statusMessage(): string | undefined; + set statusMessage(value: string | undefined); + locals: Locals; +} diff --git a/packages/hyper-express/types/components/middleware/MiddlewareHandler.d.ts b/packages/hyper-express/types/components/middleware/MiddlewareHandler.d.ts new file mode 100644 index 0000000..d9ee96b --- /dev/null +++ b/packages/hyper-express/types/components/middleware/MiddlewareHandler.d.ts @@ -0,0 +1,10 @@ +import { MiddlewareNext } from './MiddlewareNext'; +import { Request, DefaultRequestLocals } from '../http/Request'; +import { Response, DefaultResponseLocals } from '../http/Response'; + +export type MiddlewarePromise = Promise; +export type MiddlewareHandler = ( + request: Request, + response: Response, + next: MiddlewareNext +) => MiddlewarePromise | any; diff --git a/packages/hyper-express/types/components/middleware/MiddlewareNext.d.ts b/packages/hyper-express/types/components/middleware/MiddlewareNext.d.ts new file mode 100644 index 0000000..a1b13cb --- /dev/null +++ b/packages/hyper-express/types/components/middleware/MiddlewareNext.d.ts @@ -0,0 +1 @@ +export type MiddlewareNext = (error?: Error) => void; \ No newline at end of file diff --git a/packages/hyper-express/types/components/plugins/HostManager.d.ts b/packages/hyper-express/types/components/plugins/HostManager.d.ts new file mode 100644 index 0000000..111904e --- /dev/null +++ b/packages/hyper-express/types/components/plugins/HostManager.d.ts @@ -0,0 +1,36 @@ +import { EventEmitter } from 'events'; + +interface HostOptions { + passphrase?: string, + cert_file_name?: string, + key_file_name?: string, + dh_params_file_name?: string, + ssl_prefer_low_memory_usage?: boolean, +} + +export class HostManager extends EventEmitter { + /** + * Registers the unique host options to use for the specified hostname for incoming requests. + * + * @param {String} hostname + * @param {HostOptions} options + * @returns {HostManager} + */ + add(hostname: string, options: HostOptions): HostManager; + + /** + * Un-Registers the unique host options to use for the specified hostname for incoming requests. + * + * @param {String} hostname + * @returns {HostManager} + */ + remove(hostname: string): HostManager; + + /* HostManager Getters & Properties */ + + /** + * Returns all of the registered hostname options. + * @returns {Object.} + */ + get registered(): {[hostname: string]: HostOptions}; +} \ No newline at end of file diff --git a/packages/hyper-express/types/components/plugins/LiveFile.d.ts b/packages/hyper-express/types/components/plugins/LiveFile.d.ts new file mode 100644 index 0000000..bd7b70a --- /dev/null +++ b/packages/hyper-express/types/components/plugins/LiveFile.d.ts @@ -0,0 +1,48 @@ +import * as FileSystem from 'fs'; +import { EventEmitter} from 'events'; + +export interface LiveFileOptions { + path: string, + retry: { + every: number, + max: number + } +} + +export class LiveFile extends EventEmitter { + constructor(options: LiveFileOptions) + + /** + * Reloads buffer/content for file asynchronously with retry policy. + * + * @private + * @param {Boolean} fresh + * @param {Number} count + * @returns {Promise} + */ + reload(fresh: boolean, count: number): Promise; + + /** + * Returns a promise which resolves once first reload is complete. + * + * @returns {Promise} + */ + ready(): Promise + + /* LiveFile Getters */ + get is_ready(): boolean; + + get name(): string; + + get path(): string; + + get extension(): string; + + get content(): string; + + get buffer(): Buffer; + + get last_update(): number; + + get watcher(): FileSystem.FSWatcher; +} diff --git a/packages/hyper-express/types/components/plugins/MultipartField.d.ts b/packages/hyper-express/types/components/plugins/MultipartField.d.ts new file mode 100644 index 0000000..db3db28 --- /dev/null +++ b/packages/hyper-express/types/components/plugins/MultipartField.d.ts @@ -0,0 +1,73 @@ +import { Readable, WritableOptions } from 'stream'; + +export type MultipartFile = { + name?: string, + stream: Readable +} + +export type Truncations = { + name: boolean, + value: boolean +} + +export class MultipartField { + /* MultipartField Methods */ + + /** + * Saves this multipart file content to the specified path. + * Note! You must specify the file name and extension in the path itself. + * + * @param {String} path Path with file name to which you would like to save this file. + * @param {WritableOptions} options Writable stream options + * @returns {Promise} + */ + write(path: string, options?: WritableOptions): Promise; + + /* MultipartField Properties */ + + /** + * Field name as specified in the multipart form. + * @returns {String} + */ + get name(): string; + + /** + * Field encoding as specified in the multipart form. + * @returns {String} + */ + get encoding(): string; + + /** + * Field mime type as specified in the multipart form. + * @returns {String} + */ + get mime_type(): string; + + /** + * Returns file information about this field if it is a file type. + * Note! This property will ONLY be defined if this field is a file type. + * + * @returns {MultipartFile} + */ + get file(): MultipartFile | void; + + /** + * Returns field value if this field is a non-file type. + * Note! This property will ONLY be defined if this field is a non-file type. + * + * @returns {String} + */ + get value(): string | void; + + /** + * Returns information about truncations in this field. + * Note! This property will ONLY be defined if this field is a non-file type. + * + * @returns {Truncations} + */ + get truncated(): Truncations | void; +} + +export type MultipartHandler = (field: MultipartField) => void | Promise; + +export type MultipartLimitReject = "PARTS_LIMIT_REACHED" | "FILES_LIMIT_REACHED" | "FIELDS_LIMIT_REACHED"; \ No newline at end of file diff --git a/packages/hyper-express/types/components/plugins/SSEventStream.d.ts b/packages/hyper-express/types/components/plugins/SSEventStream.d.ts new file mode 100644 index 0000000..a6ff6db --- /dev/null +++ b/packages/hyper-express/types/components/plugins/SSEventStream.d.ts @@ -0,0 +1,50 @@ +import { Response } from "../http/Response"; + +export class SSEventStream { + constructor(response: Response) + + /** + * Opens the "Server-Sent Events" connection to the client. + * + * @returns {Boolean} + */ + open(): boolean; + + /** + * Closes the "Server-Sent Events" connection to the client. + * + * @returns {Boolean} + */ + close(): boolean; + + /** + * Sends a comment-type message to the client that will not be emitted by EventSource. + * This can be useful as a keep-alive mechanism if messages might not be sent regularly. + * + * @param {String} data + * @returns {Boolean} + */ + comment(data: string): boolean; + + /** + * Sends a message to the client based on the specified event and data. + * Note! You must retry failed messages if you receive a false output from this method. + * + * @param {String} id + * @param {String=} event + * @param {String=} data + * @returns {Boolean} + */ + send(data: string): boolean; + send(event: string, data: string): boolean; + send(id: string, event: string, data: string): boolean; + + /* SSEventStream properties */ + + /** + * Whether this Server-Sent Events stream is still active. + * + * @returns {Boolean} + */ + get active(): boolean; +} \ No newline at end of file diff --git a/packages/hyper-express/types/components/router/Route.d.ts b/packages/hyper-express/types/components/router/Route.d.ts new file mode 100644 index 0000000..10a64f4 --- /dev/null +++ b/packages/hyper-express/types/components/router/Route.d.ts @@ -0,0 +1 @@ +// Since this a private component no types have been defined \ No newline at end of file diff --git a/packages/hyper-express/types/components/router/Router.d.ts b/packages/hyper-express/types/components/router/Router.d.ts new file mode 100644 index 0000000..ed02b08 --- /dev/null +++ b/packages/hyper-express/types/components/router/Router.d.ts @@ -0,0 +1,188 @@ +import { ReadableOptions } from 'stream'; +import { Request } from '../http/Request'; +import { Response } from '../http/Response'; +import { Websocket } from '../ws/Websocket'; +import { CompressOptions } from 'uWebSockets.js'; +import { MiddlewareHandler } from '../middleware/MiddlewareHandler'; + +// Define types for HTTP Route Creators +export type UserRouteHandler = (request: Request, response: Response) => void; +export interface UserRouteOptions { + middlewares?: Array; + stream_options?: ReadableOptions; + max_body_length?: number; +} + +// Define types for Websocket Route Creator +export type WSRouteHandler = (websocket: Websocket) => void; +export interface WSRouteOptions { + message_type?: 'String' | 'Buffer' | 'ArrayBuffer'; + compression?: CompressOptions; + idle_timeout?: number; + max_backpressure?: number; + max_payload_length?: number; +} + +// Define types for internal route/middleware records +export interface RouteRecord { + method: string; + pattern: string; + options: UserRouteOptions | WSRouteOptions; + handler: UserRouteHandler; +} + +// Defines the type for internal middleware records +export interface MiddlewareRecord { + pattern: string; + middleware: MiddlewareHandler; +} + +type UsableSpreadableArguments = (string | Router | MiddlewareHandler | MiddlewareHandler[])[]; +type RouteSpreadableArguments = ( + | string + | UserRouteOptions + // | UserRouteHandler - Temporarily disabled because Typescript cannot do "UserRouteHandler | MiddlewareHandler" due to the next parameter confusing it + | MiddlewareHandler + | MiddlewareHandler[] +)[]; + +export class Router { + constructor(); + + /** + * Returns a chainable Router instance which can be used to bind multiple method routes or middlewares on the same path easily. + * Example: `Router.route('/api/v1').get(getHandler).post(postHandler).delete(destroyHandler)` + * Example: `Router.route('/api/v1').use(middleware).user(middleware2)` + * @param {String} pattern + * @returns {Router} A chainable Router instance with a context pattern set to this router's pattern. + */ + route(pattern: string): this; + + /** + * Registers middlewares and router instances on the specified pattern if specified. + * If no pattern is specified, the middleware/router instance will be mounted on the '/' root path by default of this instance. + * + * @param {...(String|MiddlewareHandler|Router)} args (request, response, next) => {} OR (request, response) => new Promise((resolve, reject) => {}) + */ + use(...args: UsableSpreadableArguments): this; + + /** + * Creates an HTTP route that handles any HTTP method requests. + * Note! ANY routes do not support route specific middlewares. + * + * @param {String} pattern + * @param {...(RouteOptions|MiddlewareHandler)} args + */ + any(...args: RouteSpreadableArguments): this; + + /** + * Alias of any() method. + * Creates an HTTP route that handles any HTTP method requests. + * Note! ANY routes do not support route specific middlewares. + * + * @param {String} pattern + * @param {...(RouteOptions|MiddlewareHandler)} args + */ + all(...args: RouteSpreadableArguments): this; + + /** + * Creates an HTTP route that handles GET method requests. + * + * @param {String} pattern + * @param {...(RouteOptions|MiddlewareHandler)} args + */ + get(...args: RouteSpreadableArguments): this; + + /** + * Creates an HTTP route that handles POST method requests. + * + * @param {String} pattern + * @param {...(RouteOptions|MiddlewareHandler)} args + */ + post(...args: RouteSpreadableArguments): this; + + /** + * Creates an HTTP route that handles PUT method requests. + * + * @param {String} pattern + * @param {...(RouteOptions|MiddlewareHandler)} args + */ + put(...args: RouteSpreadableArguments): this; + + /** + * Creates an HTTP route that handles DELETE method requests. + * + * @param {String} pattern + * @param {...(RouteOptions|MiddlewareHandler)} args + */ + delete(...args: RouteSpreadableArguments): this; + + /** + * Creates an HTTP route that handles HEAD method requests. + * + * @param {String} pattern + * @param {...(RouteOptions|MiddlewareHandler)} args + */ + head(...args: RouteSpreadableArguments): this; + + /** + * Creates an HTTP route that handles OPTIONS method requests. + * + * @param {String} pattern + * @param {...(RouteOptions|MiddlewareHandler)} args + */ + options(...args: RouteSpreadableArguments): this; + + /** + * Creates an HTTP route that handles PATCH method requests. + * + * @param {String} pattern + * @param {...(RouteOptions|MiddlewareHandler)} args + */ + patch(...args: RouteSpreadableArguments): this; + + /** + * Creates an HTTP route that handles TRACE method requests. + * + * @param {String} pattern + * @param {...(RouteOptions|MiddlewareHandler)} args + */ + trace(...args: RouteSpreadableArguments): this; + + /** + * Creates an HTTP route that handles CONNECT method requests. + * + * @param {String} pattern + * @param {...(RouteOptions|MiddlewareHandler)} args + */ + connect(...args: RouteSpreadableArguments): this; + + /** + * Intercepts and handles upgrade requests for incoming websocket connections. + * Note! You must call response.upgrade(data) at some point in this route to open a websocket connection. + * + * @param {String} pattern + * @param {...(RouteOptions|MiddlewareHandler)} args + */ + upgrade(...args: RouteSpreadableArguments): this; + + /** + * @param {String} pattern + * @param {WSRouteOptions|WSRouteHandler} options + * @param {WSRouteHandler} handler + */ + ws(pattern: string, handler: WSRouteHandler): this; + ws(pattern: string, options: WSRouteOptions, handler: WSRouteHandler): this; + + /** + * Returns All routes in this router in the order they were registered. + * @returns {Array} + */ + get routes(): Array; + + /** + * Returns all middlewares in this router in the order they were registered. + * @returns {Array} + */ + get middlewares(): Array; +} diff --git a/packages/hyper-express/types/components/ws/Websocket.d.ts b/packages/hyper-express/types/components/ws/Websocket.d.ts new file mode 100644 index 0000000..dfe3fea --- /dev/null +++ b/packages/hyper-express/types/components/ws/Websocket.d.ts @@ -0,0 +1,157 @@ +import * as uWebsockets from 'uWebSockets.js'; +import { EventEmitter } from "events"; +import { Readable, Writable } from 'stream'; +import TypedEmitter from 'typed-emitter'; +import { SendableData } from "../http/Response"; + +export type WebsocketContext = { + [key: string]: string +} + +type Events = { + message: (...args: any[]) => void | Promise; + close: (...args: any[]) => void | Promise; + drain: (...args: any[]) => void | Promise; + ping: (...args: any[]) => void | Promise; + pong: (...args: any[]) => void | Promise; +} + +export class Websocket extends (EventEmitter as new () => TypedEmitter) { + /** + * Alias of uWS.cork() method. Accepts a callback with multiple operations for network efficiency. + * + * @param {Function} callback + * @returns {Websocket} + */ + atomic(callback: () => void): Websocket; + + /** + * Sends a message to websocket connection. + * Returns true if message was sent successfully. + * Returns false if message was not sent due to buil up backpressure. + * + * @param {String|Buffer|ArrayBuffer} message + * @param {Boolean=} is_binary + * @param {Boolean=} compress + * @returns {Boolean} + */ + send(message: SendableData, is_binary?: boolean, compress?: boolean): boolean; + + /** + * Sends a ping control message. + * Returns Boolean depending on backpressure similar to send(). + * + * @param {String|Buffer|ArrayBuffer=} message + * @returns {Boolean} + */ + ping(message?: SendableData): void; + + /** + * Gracefully closes websocket connection by sending specified code and short message. + * + * @param {Number=} code + * @param {(String|Buffer|ArrayBuffer)=} message + */ + close(code?: number, message?: SendableData): void; + + /** + * Forcefully closes websocket connection. + * No websocket close code/message is sent. + * This will immediately emit the 'close' event. + */ + destroy(): void; + + /** + * Returns whether this `Websocket` is subscribed to the specified topic. + * + * @param {String} topic + * @returns {Boolean} + */ + is_subscribed(topic: string): boolean; + + /** + * Subscribe to a topic in MQTT syntax. + * MQTT syntax includes things like "root/child/+/grandchild" where "+" is a wildcard and "root/#" where "#" is a terminating wildcard. + * + * @param {String} topic + * @returns {Boolean} + */ + subscribe(topic: string): boolean; + + /** + * Unsubscribe from a topic. + * Returns true on success, if the WebSocket was subscribed. + * + * @param {String} topic + * @returns {Boolean} + */ + unsubscribe(topic: string): boolean; + + /** + * Publish a message to a topic in MQTT syntax. + * You cannot publish using wildcards, only fully specified topics. + * + * @param {String} topic + * @param {String|Buffer|ArrayBuffer} message + * @param {Boolean=} is_binary + * @param {Boolean=} compress + */ + publish(topic: string, message: SendableData, is_binary?: boolean, compress?: boolean): boolean; + + /** + * This method is used to stream a message to the receiver. + * Note! The data is streamed as binary by default due to how partial fragments are sent. + * This is done to prevent processing errors depending on client's receiver's incoming fragment processing strategy. + * + * @param {Readable} readable A Readable stream which will be consumed as message + * @param {Boolean=} is_binary Whether data being streamed is in binary. Default: true + * @returns {Promise} + */ + stream(readable: Readable, is_binary?: boolean): Promise; + + /* Websocket Properties */ + + /** + * Underlying uWS.Websocket object + */ + get raw(): uWebsockets.WebSocket; + + /** + * Returns IP address of this websocket connection. + * @returns {String} + */ + get ip(): string; + + /** + * Returns context values from the response.update(context) connection upgrade call. + * @returns {Object} + */ + get context(): WebsocketContext; + + /** + * Returns whether is websocket connection is closed. + * @returns {Boolean} + */ + get closed(): boolean; + + /** + * Returns the bytes buffered in backpressure. + * This is similar to the bufferedAmount property in the browser counterpart. + * @returns {Number} + */ + get buffered(): number; + + /** + * Returns a list of topics this websocket is subscribed to. + * @returns {Array.} + */ + get topics(): Array; + + /** + * Returns a Writable stream associated with this response to be used for piping streams. + * Note! You can only retrieve/use only one writable at any given time. + * + * @returns {Writable} + */ + get writable(): Writable; +} diff --git a/packages/hyper-express/types/components/ws/WebsocketRoute.d.ts b/packages/hyper-express/types/components/ws/WebsocketRoute.d.ts new file mode 100644 index 0000000..10a64f4 --- /dev/null +++ b/packages/hyper-express/types/components/ws/WebsocketRoute.d.ts @@ -0,0 +1 @@ +// Since this a private component no types have been defined \ No newline at end of file diff --git a/packages/hyper-express/types/index.d.ts b/packages/hyper-express/types/index.d.ts new file mode 100644 index 0000000..f0fb2c7 --- /dev/null +++ b/packages/hyper-express/types/index.d.ts @@ -0,0 +1,16 @@ +import { Server } from './components/Server'; + +export * as compressors from 'uWebSockets.js'; +export * from './components/Server'; +export * from './components/router/Router'; +export * from './components/http/Request'; +export * from './components/http/Response'; +export * from './components/middleware/MiddlewareHandler'; +export * from './components/middleware/MiddlewareNext'; +export * from './components/plugins/LiveFile'; +export * from './components/plugins/MultipartField'; +export * from './components/plugins/SSEventStream'; +export * from './components/ws/Websocket'; +export * from './shared/uploaded-file'; + +export const express: (...args: ConstructorParameters) => Server; diff --git a/packages/hyper-express/types/shared/operators.d.ts b/packages/hyper-express/types/shared/operators.d.ts new file mode 100644 index 0000000..b43f457 --- /dev/null +++ b/packages/hyper-express/types/shared/operators.d.ts @@ -0,0 +1,10 @@ +export function wrap_object(original: object, target: object): void; + +export type PathKeyItem = [key: string, index: number]; +export function parse_path_parameters(pattern: string): PathKeyItem[]; + +export function array_buffer_to_string(array_buffer: ArrayBuffer, encoding?: string): string; + +export function async_wait(delay: number): Promise; + +export function merge_relative_paths(base_path: string, new_path: string): string; \ No newline at end of file diff --git a/packages/hyper-express/types/shared/uploaded-file.d.ts b/packages/hyper-express/types/shared/uploaded-file.d.ts new file mode 100644 index 0000000..0be241b --- /dev/null +++ b/packages/hyper-express/types/shared/uploaded-file.d.ts @@ -0,0 +1,19 @@ +import { readFileSync } from 'fs-extra'; + +export class UploadedFile { + _filename: string; + _size: number; + _mimeType: string; + _tempName: string; + _tempPath: string; + + get filename(): string; + get size(): string; + get mimeType(): string; + get tempName(): string; + get tempPath(): string; + + get extension(): string; + + toBuffer(): Promise; +}