change upload mechanisms

This commit is contained in:
rubikscraft
2022-09-18 15:16:34 +02:00
parent 471a66aa81
commit 9b0ab6adef
21 changed files with 271 additions and 197 deletions

View File

@@ -32,9 +32,11 @@
"@nestjs/jwt": "^9.0.0",
"@nestjs/passport": "^9.0.0",
"@nestjs/platform-fastify": "^9.0.11",
"@nestjs/platform-socket.io": "^9.0.11",
"@nestjs/serve-static": "^3.0.0",
"@nestjs/throttler": "^3.0.0",
"@nestjs/typeorm": "^9.0.1",
"@nestjs/websockets": "^9.0.11",
"bcrypt": "^5.0.1",
"bmp-img": "^1.2.1",
"cors": "^2.8.5",

View File

@@ -18,12 +18,12 @@ export class MultipartConfigService {
);
}
public getLimits() {
public getLimits(fileLimit?: number) {
return {
fieldNameSize: 128,
fieldSize: 1024,
fields: 16,
files: 16,
fields: 20,
files: fileLimit ?? 20,
fileSize: this.getMaxFileSize(),
};
}

View File

@@ -1,8 +1,8 @@
import { Module } from '@nestjs/common';
import { EarlyConfigModule } from '../config/early/early-config.module';
import { ImageIdPipe } from './image-id/image-id.pipe';
import { MultiPartPipe } from './multipart/multipart.pipe';
import { PostFilePipe } from './multipart/postfile.pipe';
import { MultiPartPipe } from './multipart/postfiles.pipe';
@Module({
imports: [EarlyConfigModule],

View File

@@ -3,6 +3,9 @@ import { createParamDecorator, ExecutionContext } from '@nestjs/common';
// Since pipes dont have direct access to the request object, we need this decorator to inject it
export const InjectRequest = createParamDecorator(
async (data: any, ctx: ExecutionContext) => {
return ctx.switchToHttp().getRequest();
return {
data: data,
request: ctx.switchToHttp().getRequest(),
};
},
);

View File

@@ -1,7 +1,7 @@
import { InjectRequest } from './inject-request.decorator';
import { MultiPartPipe } from './multipart.pipe';
import { PostFilePipe } from './postfile.pipe';
import { MultiPartPipe } from './postfiles.pipe';
export const PostFile = () => InjectRequest(PostFilePipe);
export const MultiPart = () => InjectRequest(MultiPartPipe);
export const PostFiles = (maxFiles?: number) => InjectRequest(maxFiles, MultiPartPipe);

View File

@@ -1,82 +0,0 @@
import { MultipartFields, MultipartFile } from '@fastify/multipart';
import {
ArgumentMetadata,
Injectable,
Logger,
PipeTransform,
Scope,
} from '@nestjs/common';
import { FastifyRequest } from 'fastify';
import { Fail, FT, HasFailed } from 'picsur-shared/dist/types';
import { ZodDtoStatic } from 'picsur-shared/dist/util/create-zod-dto';
import { MultipartConfigService } from '../../config/early/multipart.config.service';
import {
CreateMultiPartFieldDto,
CreateMultiPartFileDto,
} from '../../models/dto/multipart.dto';
@Injectable({ scope: Scope.REQUEST })
export class MultiPartPipe implements PipeTransform {
private readonly logger = new Logger(MultiPartPipe.name);
constructor(
private readonly multipartConfigService: MultipartConfigService,
) {}
async transform<T extends Object>(
req: FastifyRequest,
metadata: ArgumentMetadata,
) {
let zodSchema = (metadata?.metatype as ZodDtoStatic)?.zodSchema;
if (!zodSchema) {
this.logger.error('Invalid scheme on multipart body');
throw Fail(FT.Internal, 'Invalid scheme on backend');
}
let multipartData = {};
if (!req.isMultipart()) throw Fail(FT.UsrValidation, 'Invalid file');
// Fetch all fields from the request
let fields: MultipartFields | null = null;
try {
fields =
(
await req.file({
limits: this.multipartConfigService.getLimits(),
})
)?.fields ?? null;
} catch (e) {
this.logger.warn(e);
}
if (!fields) throw Fail(FT.UsrValidation, 'Invalid file');
// Loop over every formfield that was sent
for (const key of Object.keys(fields)) {
// Ignore duplicate fields
if (Array.isArray(fields[key])) {
continue;
}
// Use the value property to differentiate between a field and a file
// And then put the value into the correct property on the validatable class
if ((fields[key] as any).value) {
(multipartData as any)[key] = CreateMultiPartFieldDto(
fields[key] as MultipartFile,
);
} else {
const file = await CreateMultiPartFileDto(fields[key] as MultipartFile);
if (HasFailed(file)) throw file;
(multipartData as any)[key] = file;
}
}
// Now validate the class we made, if any properties were invalid, it will error here
const result = zodSchema.safeParse(multipartData);
if (!result.success) {
this.logger.warn(result.error);
throw Fail(FT.UsrValidation, 'Invalid file');
}
return result.data;
}
}

View File

@@ -12,11 +12,11 @@ export class PostFilePipe implements PipeTransform {
private readonly multipartConfigService: MultipartConfigService,
) {}
async transform({ req }: { req: FastifyRequest }) {
if (!req.isMultipart()) throw Fail(FT.UsrValidation, 'Invalid file');
async transform({ request, data }: { data: any; request: FastifyRequest },) {
if (!request.isMultipart()) throw Fail(FT.UsrValidation, 'Invalid file');
// Only one file is allowed
const file = await req.file({
const file = await request.file({
limits: {
...this.multipartConfigService.getLimits(),
files: 1,

View File

@@ -0,0 +1,37 @@
import { MultipartFile } from '@fastify/multipart';
import {
ArgumentMetadata,
Injectable,
Logger,
PipeTransform,
Scope
} from '@nestjs/common';
import { FastifyRequest } from 'fastify';
import { Fail, FT } from 'picsur-shared/dist/types';
import { MultipartConfigService } from '../../config/early/multipart.config.service';
export type FileIterator = AsyncIterableIterator<MultipartFile>;
@Injectable({ scope: Scope.REQUEST })
export class MultiPartPipe implements PipeTransform {
private readonly logger = new Logger(MultiPartPipe.name);
constructor(
private readonly multipartConfigService: MultipartConfigService,
) {}
async transform<T extends Object>(
{ request, data }: { data: any; request: FastifyRequest },
metadata: ArgumentMetadata,
) {
const filesLimit = typeof data === 'number' ? data : undefined;
if (!request.isMultipart()) throw Fail(FT.UsrValidation, 'Invalid file');
const files = request.files({
limits: this.multipartConfigService.getLimits(filesLimit),
});
return files;
}
}

View File

@@ -1,9 +0,0 @@
import { createZodDto } from 'picsur-shared/dist/util/create-zod-dto';
import { z } from 'zod';
import { MultiPartFileDtoSchema } from './multipart.dto';
// A validation class for form based file upload of an image
export const ImageUploadDtoSchema = z.object({
image: MultiPartFileDtoSchema,
});
export class ImageUploadDto extends createZodDto(ImageUploadDtoSchema) {}

View File

@@ -1,48 +0,0 @@
import { MultipartFile } from '@fastify/multipart';
import { AsyncFailable, Fail, FT } from 'picsur-shared/dist/types';
import { z } from 'zod';
export const MultiPartFileDtoSchema = z.object({
fieldname: z.string(),
encoding: z.string(),
filename: z.string(),
mimetype: z.string(),
buffer: z.any(),
file: z.any(),
});
export type MultiPartFileDto = z.infer<typeof MultiPartFileDtoSchema>;
export async function CreateMultiPartFileDto(
file: MultipartFile,
): AsyncFailable<MultiPartFileDto> {
try {
const buffer = await file.toBuffer();
return {
fieldname: file.fieldname,
encoding: file.encoding,
filename: file.filename,
mimetype: file.mimetype,
buffer,
file: file.file,
};
} catch (e) {
return Fail(FT.Internal, e);
}
}
export const MultiPartFieldDtoSchema = z.object({
fieldname: z.string(),
encoding: z.string(),
value: z.string(),
});
export type MultiPartFieldDto = z.infer<typeof MultiPartFieldDtoSchema>;
export function CreateMultiPartFieldDto(
file: MultipartFile,
): MultiPartFieldDto {
return {
fieldname: file.fieldname,
encoding: file.encoding,
value: (file as any).value,
};
}

View File

@@ -1,18 +1,20 @@
import { Controller } from '@nestjs/common';
import { NoPermissions } from '../../../decorators/permissions.decorator';
@Controller('api/experiment')
@NoPermissions()
export class ExperimentController {
constructor() {}
import { WebSocketGateway } from '@nestjs/websockets';
// @Get()
// @Returns(UserInfoResponse)
// async testRoute(
// @Request() req: AuthFastifyRequest,
// @Response({ passthrough: true }) res: FastifyReply,
// ): Promise<UserInfoResponse> {
// res.header('Location', '/error/delete-success');
// res.code(302);
// return req.user;
@WebSocketGateway({
namespace: 'experiment',
})
export class ExperimentController {
constructor() {
console.log('ExperimentController created');
}
// @SubscribeMessage('test')
// async testRoute(@MessageBody() data: any): Promise<WsResponse> {
// console.log('testRoute', data);
// return Promise.resolve({
// event: 'test',
// data: Buffer.from('Hello'),
// })
// }
}

View File

@@ -6,6 +6,6 @@ import { ExperimentController } from './experiment.controller';
@Module({
imports: [],
controllers: [ExperimentController],
providers: [ExperimentController]
})
export class ExperimentModule {}

View File

@@ -21,8 +21,9 @@ import {
ImageUploadResponse
} from 'picsur-shared/dist/dto/api/image-manage.dto';
import { Permission } from 'picsur-shared/dist/dto/permissions.enum';
import { HasFailed, ThrowIfFailed } from 'picsur-shared/dist/types';
import { MultiPart } from '../../decorators/multipart/multipart.decorator';
import { Fail, FT, HasFailed, ThrowIfFailed } from 'picsur-shared/dist/types';
import { PostFiles } from '../../decorators/multipart/multipart.decorator';
import type { FileIterator } from '../../decorators/multipart/postfiles.pipe';
import {
HasPermission,
RequiredPermissions
@@ -30,7 +31,7 @@ import {
import { ReqUserID } from '../../decorators/request-user.decorator';
import { Returns } from '../../decorators/returns.decorator';
import { ImageManagerService } from '../../managers/image/image.service';
import { ImageUploadDto } from '../../models/dto/image-upload.dto';
import { GetNextAsync } from '../../util/iterator';
@Controller('api/image')
@RequiredPermissions(Permission.ImageUpload)
export class ImageManageController {
@@ -42,15 +43,24 @@ export class ImageManageController {
@Returns(ImageUploadResponse)
@Throttle(20)
async uploadImage(
@MultiPart() multipart: ImageUploadDto,
@PostFiles(1) multipart: FileIterator,
@ReqUserID() userid: string,
@HasPermission(Permission.ImageDeleteKey) withDeleteKey: boolean,
): Promise<ImageUploadResponse> {
const file = ThrowIfFailed(await GetNextAsync(multipart));
let buffer: Buffer;
try {
buffer = await file.toBuffer();
} catch (e) {
throw Fail(FT.Internal, e);
};
const image = ThrowIfFailed(
await this.imagesService.upload(
userid,
multipart.image.filename,
multipart.image.buffer,
file.filename,
buffer,
withDeleteKey,
),
);

View File

@@ -1,8 +1,9 @@
import { Controller, Get, Head, Logger, Query, Res } from '@nestjs/common';
import { SkipThrottle } from '@nestjs/throttler';
import type { FastifyReply } from 'fastify';
import {
ImageMetaResponse,
ImageRequestParams,
ImageRequestParams
} from 'picsur-shared/dist/dto/api/image.dto';
import { ImageEntryVariant } from 'picsur-shared/dist/dto/image-entry-variant.enum';
import { FileType2Mime } from 'picsur-shared/dist/dto/mimes.dto';
@@ -21,6 +22,7 @@ import { BrandMessageType, GetBrandMessage } from '../../util/branding';
// This is the only controller with CORS enabled
@Controller('i')
@RequiredPermissions(Permission.ImageView)
@SkipThrottle()
export class ImageController {
private readonly logger = new Logger(ImageController.name);

View File

@@ -0,0 +1,9 @@
import { AsyncFailable, Fail, FT } from 'picsur-shared/dist/types';
export async function GetNextAsync<T>(
iterator: AsyncIterableIterator<T>,
): AsyncFailable<T> {
const { done, value } = await iterator.next();
if (done) return Fail(FT.BadRequest);
return value;
}

View File

@@ -2,7 +2,7 @@ import { BMPdecode, BMPencode } from 'bmp-img';
import {
AnimFileType,
FileType,
ImageFileType,
ImageFileType
} from 'picsur-shared/dist/dto/mimes.dto';
import { QOIdecode, QOIencode } from 'qoi-img';
import sharp, { Sharp, SharpOptions } from 'sharp';

View File

@@ -1,3 +1,9 @@
export interface ProcessingViewMeta {
imageFile: File;
export class ProcessingViewMeta {
private _tag = 'ProcessingViewMeta';
constructor(public imageFiles: File[]) {}
static is(value: any): value is ProcessingViewMeta {
return (value ?? {})._tag === 'ProcessingViewMeta';
}
}

View File

@@ -2,6 +2,7 @@ import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { Fail, FT, HasFailed } from 'picsur-shared/dist/types';
import { ProcessingViewMeta } from 'src/app/models/dto/processing-view-meta.dto';
import { ApiService } from 'src/app/services/api/api.service';
import { ImageService } from 'src/app/services/api/image.service';
import { Logger } from 'src/app/services/logger/logger.service';
import { ErrorService } from 'src/app/util/error-manager/error.service';
@@ -16,11 +17,12 @@ export class ProcessingComponent implements OnInit {
private readonly router: Router,
private readonly imageService: ImageService,
private readonly errorService: ErrorService,
private readonly apiService: ApiService,
) {}
async ngOnInit() {
const state = history.state as ProcessingViewMeta;
if (!state) {
if (!ProcessingViewMeta.is(state)) {
return this.errorService.quitFailure(
Fail(FT.UsrValidation, 'No state provided'),
this.logger,
@@ -29,7 +31,8 @@ export class ProcessingComponent implements OnInit {
history.replaceState(null, '');
const id = await this.imageService.UploadImage(state.imageFile);
const id = await this.imageService.UploadImage(state.imageFiles[0]);
if (HasFailed(id)) return this.errorService.quitFailure(id, this.logger);
this.router.navigate([`/view/`, id], { replaceUrl: true });

View File

@@ -39,14 +39,9 @@ export class UploadComponent implements OnInit {
}
onSelect(event: NgxDropzoneChangeEvent) {
if (event.addedFiles.length > 1)
this.errorService.log(
'You uploaded multiple images, only one has been uploaded',
);
const metadata: ProcessingViewMeta = {
imageFile: event.addedFiles[0],
};
const metadata: ProcessingViewMeta = new ProcessingViewMeta(
event.addedFiles,
);
this.router.navigate(['/processing'], { state: metadata });
}
@@ -64,21 +59,16 @@ export class UploadComponent implements OnInit {
'Your clipboard does not contain any images',
);
const blob = filteredItems[0].getAsFile();
if (!blob)
const blobs = filteredItems.map((item) => item.getAsFile());
if (blobs.some((blob) => blob === null))
return this.errorService.showFailure(
Fail(FT.Internal, 'Error getting image from clipboard'),
this.logger,
);
if (filteredItems.length > 1)
this.errorService.log(
'You pasted multiple images, only one has been uploaded',
);
const safeBlob = blobs as File[];
const metadata: ProcessingViewMeta = {
imageFile: blob,
};
const metadata: ProcessingViewMeta = new ProcessingViewMeta(safeBlob);
this.router.navigate(['/processing'], { state: metadata });
}

View File

@@ -9,6 +9,7 @@ export enum FT {
Database = 'database',
SysValidation = 'sysvalidation',
UsrValidation = 'usrvalidation',
BadRequest = 'badrequest',
Permission = 'permission',
RateLimit = 'ratelimit',
NotFound = 'notfound',
@@ -59,6 +60,11 @@ const FTProps: {
code: 400,
message: 'Validation of user input failed',
},
[FT.BadRequest]: {
important: false,
code: 400,
message: 'Bad request',
},
[FT.Permission]: {
important: false,
code: 403,

149
yarn.lock
View File

@@ -2530,6 +2530,20 @@ __metadata:
languageName: node
linkType: hard
"@nestjs/platform-socket.io@npm:^9.0.11":
version: 9.0.11
resolution: "@nestjs/platform-socket.io@npm:9.0.11"
dependencies:
socket.io: 4.5.1
tslib: 2.4.0
peerDependencies:
"@nestjs/common": ^9.0.0
"@nestjs/websockets": ^9.0.0
rxjs: ^7.1.0
checksum: 0b7eba29e51bd6cedb5613144a725dae7d41bfed65e719a58474912fb111669c4ba4c6e43bc153408d8a3b6218d3e79651c785603ff526bab61170a7782fa71d
languageName: node
linkType: hard
"@nestjs/schematics@npm:^9.0.0, @nestjs/schematics@npm:^9.0.3":
version: 9.0.3
resolution: "@nestjs/schematics@npm:9.0.3"
@@ -2604,6 +2618,26 @@ __metadata:
languageName: node
linkType: hard
"@nestjs/websockets@npm:^9.0.11":
version: 9.0.11
resolution: "@nestjs/websockets@npm:9.0.11"
dependencies:
iterare: 1.2.1
object-hash: 3.0.0
tslib: 2.4.0
peerDependencies:
"@nestjs/common": ^9.0.0
"@nestjs/core": ^9.0.0
"@nestjs/platform-socket.io": ^9.0.0
reflect-metadata: ^0.1.12
rxjs: ^7.1.0
peerDependenciesMeta:
"@nestjs/platform-socket.io":
optional: true
checksum: 1e00f5186408bc0896a5ac6f7fc17379fdc627ea6ae2c39eb321f083ad156d07d2279a91828bae1bfcc07a986ec60fbfa3f803e934a09696ce9cc7673e245af8
languageName: node
linkType: hard
"@ng-web-apis/common@npm:^2.0.1":
version: 2.0.1
resolution: "@ng-web-apis/common@npm:2.0.1"
@@ -2890,6 +2924,13 @@ __metadata:
languageName: node
linkType: hard
"@types/component-emitter@npm:^1.2.10":
version: 1.2.11
resolution: "@types/component-emitter@npm:1.2.11"
checksum: 0e081c5f7a4b113af3732f67ad9ebb487d5c239d440d96938ff9a679d18bb9337a513638e12b5b02a7a921494eef18c5a4d78f1188bc43a12290edd74c42a9c7
languageName: node
linkType: hard
"@types/connect-history-api-fallback@npm:^1.3.5":
version: 1.3.5
resolution: "@types/connect-history-api-fallback@npm:1.3.5"
@@ -2909,6 +2950,13 @@ __metadata:
languageName: node
linkType: hard
"@types/cookie@npm:^0.4.1":
version: 0.4.1
resolution: "@types/cookie@npm:0.4.1"
checksum: 3275534ed69a76c68eb1a77d547d75f99fedc80befb75a3d1d03662fb08d697e6f8b1274e12af1a74c6896071b11510631ba891f64d30c78528d0ec45a9c1a18
languageName: node
linkType: hard
"@types/cookiejar@npm:*":
version: 2.1.2
resolution: "@types/cookiejar@npm:2.1.2"
@@ -3035,7 +3083,7 @@ __metadata:
languageName: node
linkType: hard
"@types/node@npm:^18.7.18":
"@types/node@npm:>=10.0.0, @types/node@npm:^18.7.18":
version: 18.7.18
resolution: "@types/node@npm:18.7.18"
checksum: 8aec61f0f96e2a69ce51f1f40f949ca578bbb4fe05d7c0b8ce3aeeb848e90f755837f17f6ac132ca404d974fe9b2974150ad3b4984fc9dc7c3ceddb10bae0167
@@ -3937,6 +3985,13 @@ __metadata:
languageName: node
linkType: hard
"base64id@npm:2.0.0, base64id@npm:~2.0.0":
version: 2.0.0
resolution: "base64id@npm:2.0.0"
checksum: 581b1d37e6cf3738b7ccdd4d14fe2bfc5c238e696e2720ee6c44c183b838655842e22034e53ffd783f872a539915c51b0d4728a49c7cc678ac5a758e00d62168
languageName: node
linkType: hard
"batch@npm:0.6.1":
version: 0.6.1
resolution: "batch@npm:0.6.1"
@@ -4471,6 +4526,13 @@ __metadata:
languageName: node
linkType: hard
"component-emitter@npm:~1.3.0":
version: 1.3.0
resolution: "component-emitter@npm:1.3.0"
checksum: b3c46de38ffd35c57d1c02488355be9f218e582aec72d72d1b8bbec95a3ac1b38c96cd6e03ff015577e68f550fbb361a3bfdbd9bb248be9390b7b3745691be6b
languageName: node
linkType: hard
"compressible@npm:~2.0.16":
version: 2.0.18
resolution: "compressible@npm:2.0.18"
@@ -4562,6 +4624,13 @@ __metadata:
languageName: node
linkType: hard
"cookie@npm:~0.4.1":
version: 0.4.2
resolution: "cookie@npm:0.4.2"
checksum: a00833c998bedf8e787b4c342defe5fa419abd96b32f4464f718b91022586b8f1bafbddd499288e75c037642493c83083da426c6a9080d309e3bd90fd11baa9b
languageName: node
linkType: hard
"copy-anything@npm:^2.0.1":
version: 2.0.6
resolution: "copy-anything@npm:2.0.6"
@@ -4604,7 +4673,7 @@ __metadata:
languageName: node
linkType: hard
"cors@npm:^2.8.5":
"cors@npm:^2.8.5, cors@npm:~2.8.5":
version: 2.8.5
resolution: "cors@npm:2.8.5"
dependencies:
@@ -4780,7 +4849,7 @@ __metadata:
languageName: node
linkType: hard
"debug@npm:4, debug@npm:4.3.4, debug@npm:^4.0.0, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4":
"debug@npm:4, debug@npm:4.3.4, debug@npm:^4.0.0, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4, debug@npm:~4.3.1, debug@npm:~4.3.2":
version: 4.3.4
resolution: "debug@npm:4.3.4"
dependencies:
@@ -5077,6 +5146,31 @@ __metadata:
languageName: node
linkType: hard
"engine.io-parser@npm:~5.0.3":
version: 5.0.4
resolution: "engine.io-parser@npm:5.0.4"
checksum: d4ad0cef6ff63c350e35696da9bb3dbd180f67b56e93e90375010cc40393e6c0639b780d5680807e1d93a7e2e3d7b4a1c3b27cf75db28eb8cbf605bc1497da03
languageName: node
linkType: hard
"engine.io@npm:~6.2.0":
version: 6.2.0
resolution: "engine.io@npm:6.2.0"
dependencies:
"@types/cookie": ^0.4.1
"@types/cors": ^2.8.12
"@types/node": ">=10.0.0"
accepts: ~1.3.4
base64id: 2.0.0
cookie: ~0.4.1
cors: ~2.8.5
debug: ~4.3.1
engine.io-parser: ~5.0.3
ws: ~8.2.3
checksum: cc485c5ba2e0c4f6ca02dcafd192b22f9dad89d01dc815005298780d3fb910db4cebab4696e8615290c473c2eeb259e8bee2a1fb7ab594d9c80f9f3485771911
languageName: node
linkType: hard
"enhanced-resolve@npm:^5.0.0, enhanced-resolve@npm:^5.10.0, enhanced-resolve@npm:^5.7.0":
version: 5.10.0
resolution: "enhanced-resolve@npm:5.10.0"
@@ -8800,11 +8894,13 @@ __metadata:
"@nestjs/jwt": ^9.0.0
"@nestjs/passport": ^9.0.0
"@nestjs/platform-fastify": ^9.0.11
"@nestjs/platform-socket.io": ^9.0.11
"@nestjs/schematics": ^9.0.3
"@nestjs/serve-static": ^3.0.0
"@nestjs/testing": ^9.0.11
"@nestjs/throttler": ^3.0.0
"@nestjs/typeorm": ^9.0.1
"@nestjs/websockets": ^9.0.11
"@types/bcrypt": ^5.0.0
"@types/cors": ^2.8.12
"@types/multer": ^1.4.7
@@ -10481,6 +10577,38 @@ __metadata:
languageName: node
linkType: hard
"socket.io-adapter@npm:~2.4.0":
version: 2.4.0
resolution: "socket.io-adapter@npm:2.4.0"
checksum: a84639946dce13547b95f6e09fe167cdcd5d80941afc2e46790cc23384e0fd3c901e690ecc9bdd600939ce6292261ee15094a0b486f797ed621cfc8783d87a0c
languageName: node
linkType: hard
"socket.io-parser@npm:~4.0.4":
version: 4.0.5
resolution: "socket.io-parser@npm:4.0.5"
dependencies:
"@types/component-emitter": ^1.2.10
component-emitter: ~1.3.0
debug: ~4.3.1
checksum: 8b60cf3abb9c3571f90cf894d40f41459ab007e6cee7ca8ee28ab107d76ded4a72ca5c4e5dcb82d996d4f78b3689dd3eb36ba0b39a66e25e2e9a9afa276c81c5
languageName: node
linkType: hard
"socket.io@npm:4.5.1":
version: 4.5.1
resolution: "socket.io@npm:4.5.1"
dependencies:
accepts: ~1.3.4
base64id: ~2.0.0
debug: ~4.3.2
engine.io: ~6.2.0
socket.io-adapter: ~2.4.0
socket.io-parser: ~4.0.4
checksum: 86afd6dcce0c96de85b20a0e37fa4a21e2e96bd6e36d2518acfad37597bcb5208feafbbac20cd34ee4b9356d40418a43938bcf4a206ba693ba3c771ffcef724f
languageName: node
linkType: hard
"sockjs@npm:^0.3.24":
version: 0.3.24
resolution: "sockjs@npm:0.3.24"
@@ -11808,6 +11936,21 @@ __metadata:
languageName: node
linkType: hard
"ws@npm:~8.2.3":
version: 8.2.3
resolution: "ws@npm:8.2.3"
peerDependencies:
bufferutil: ^4.0.1
utf-8-validate: ^5.0.2
peerDependenciesMeta:
bufferutil:
optional: true
utf-8-validate:
optional: true
checksum: c869296ccb45f218ac6d32f8f614cd85b50a21fd434caf11646008eef92173be53490810c5c23aea31bc527902261fbfd7b062197eea341b26128d4be56a85e4
languageName: node
linkType: hard
"xml2js@npm:^0.4.23":
version: 0.4.23
resolution: "xml2js@npm:0.4.23"