From 07c3ea573faf69924437740954f0dcf913a4a312 Mon Sep 17 00:00:00 2001 From: Bartosz Herba Date: Thu, 21 Nov 2024 12:57:17 +0100 Subject: [PATCH] feat: add upload functionality in the middleware --- .changeset/hot-maps-unite.md | 29 ++++ .../integration/bootstrap/api/index.ts | 1 + .../integration/bootstrap/api/upload/index.ts | 10 ++ .../integration/bootstrap/serverWithUpload.ts | 23 ++++ .../__tests__/integration/upload.spec.ts | 61 +++++++++ .../unit/services/fileUpload.spec.ts | 126 ++++++++++++++++++ packages/middleware/package.json | 4 +- packages/middleware/src/createServer.ts | 3 + .../middleware/src/services/fileUpload.ts | 51 +++++++ packages/middleware/src/types/common.ts | 14 +- packages/middleware/src/types/fileUpload.ts | 7 + packages/middleware/src/types/index.ts | 1 + packages/middleware/src/types/server.ts | 7 + packages/middleware/tsconfig.json | 3 +- yarn.lock | 58 +++++++- 15 files changed, 390 insertions(+), 8 deletions(-) create mode 100644 .changeset/hot-maps-unite.md create mode 100644 packages/middleware/__tests__/integration/bootstrap/api/upload/index.ts create mode 100644 packages/middleware/__tests__/integration/bootstrap/serverWithUpload.ts create mode 100644 packages/middleware/__tests__/integration/upload.spec.ts create mode 100644 packages/middleware/__tests__/unit/services/fileUpload.spec.ts create mode 100644 packages/middleware/src/services/fileUpload.ts create mode 100644 packages/middleware/src/types/fileUpload.ts diff --git a/.changeset/hot-maps-unite.md b/.changeset/hot-maps-unite.md new file mode 100644 index 0000000000..d7b1057375 --- /dev/null +++ b/.changeset/hot-maps-unite.md @@ -0,0 +1,29 @@ +--- +"@vue-storefront/middleware": minor +--- + +**[ADDED]** Support for file uploads +Now you can upload files to the server with a `multipart/form-data` content type. Files are available in the `req.files` object. + +```ts +// Example of an endpoint that handles file uploads +export const upload = (context) => { + // Files are available in the `req.files` object + const { files } = context.req; + + // Do something with files + + return Promise.resolve({ + status: 200, + message: "ok", + }); +}; +``` + +Available options are: + +- `enabled`: (boolean) Enable/disable file upload functionality. Default: `true`. If you set it to `false`, the middleware will not initialize file upload functionality and the `req.files` object will be empty. That can be useful if you do not want to handle file uploads in your app and want to avoid unnecessary processing. +- `maxFileSize`: (number) Maximum file size in bytes. Default: `5242880` (5MB) +- `maxFiles`: (number) Maximum number of files that can be uploaded at once. Default: `5` +- `allowedMimeTypes`: (string[]) Array of allowed MIME types. Default: `["image/*", "application/pdf"]` +- `fieldNames`: (string[]) Array of accepted form field names for file uploads. Default: `[]` diff --git a/packages/middleware/__tests__/integration/bootstrap/api/index.ts b/packages/middleware/__tests__/integration/bootstrap/api/index.ts index 78ad801130..9ae51edfca 100644 --- a/packages/middleware/__tests__/integration/bootstrap/api/index.ts +++ b/packages/middleware/__tests__/integration/bootstrap/api/index.ts @@ -5,3 +5,4 @@ export { error } from "./error"; export { throwAxiosError } from "./throwAxiosError"; export { throwError } from "./throwError"; export { getConfig } from "./getConfig"; +export { upload } from "./upload"; diff --git a/packages/middleware/__tests__/integration/bootstrap/api/upload/index.ts b/packages/middleware/__tests__/integration/bootstrap/api/upload/index.ts new file mode 100644 index 0000000000..6d42c1a72c --- /dev/null +++ b/packages/middleware/__tests__/integration/bootstrap/api/upload/index.ts @@ -0,0 +1,10 @@ +export const upload = (context) => { + const { files } = context.req; + + return Promise.resolve({ + status: 200, + message: "ok", + error: false, + files, + }); +}; diff --git a/packages/middleware/__tests__/integration/bootstrap/serverWithUpload.ts b/packages/middleware/__tests__/integration/bootstrap/serverWithUpload.ts new file mode 100644 index 0000000000..8d96d65b0e --- /dev/null +++ b/packages/middleware/__tests__/integration/bootstrap/serverWithUpload.ts @@ -0,0 +1,23 @@ +import { apiClientFactory } from "../../../src/apiClientFactory"; +import * as api from "./api"; +import { AlokaiContainer, getLogger } from "../../../src"; + +const onCreate = async ( + config: Record = {}, + alokai: AlokaiContainer +) => { + const logger = getLogger(alokai); + logger.info("oncreate"); + + return { + config, + client: null, + }; +}; + +const { createApiClient } = apiClientFactory({ + onCreate, + api, +}); + +export { createApiClient }; diff --git a/packages/middleware/__tests__/integration/upload.spec.ts b/packages/middleware/__tests__/integration/upload.spec.ts new file mode 100644 index 0000000000..7a5198757f --- /dev/null +++ b/packages/middleware/__tests__/integration/upload.spec.ts @@ -0,0 +1,61 @@ +import { Server } from "http"; +import request from "supertest"; +import { createServer } from "../../src/index"; + +describe("[Integration] Create server", () => { + let app: Server; + + const getServer = async (isUploadEnabled: boolean = true) => { + return await createServer( + { + integrations: { + test_integration: { + location: "./__tests__/integration/bootstrap/serverWithUpload", + configuration: {}, + }, + }, + }, + { + fileUpload: { + enabled: isUploadEnabled, + }, + } + ); + }; + + it("should allow to upload files if fileUpload is enabled", async () => { + app = await getServer(); + + const { status, body } = await request(app) + .post("/test_integration/upload") + .set("Content-Type", "multipart/form-data") + .attach("files", Buffer.from("test file content"), "test.jpg") + .expect(200); + + expect(status).toBe(200); + expect(body).toBeDefined(); + expect(body.files).toEqual([ + { + fieldname: "files", + originalname: "test.jpg", + encoding: "7bit", + mimetype: "image/jpeg", + buffer: expect.any(Object), + size: 17, + }, + ]); + }); + + it("should not allow to upload files if fileUpload is disabled", async () => { + app = await getServer(false); + + const { status, body } = await request(app) + .post("/test_integration/upload") + .set("Content-Type", "multipart/form-data") + .expect(200); + + expect(status).toBe(200); + expect(body).toBeDefined(); + expect(body.files).toBeUndefined(); + }); +}); diff --git a/packages/middleware/__tests__/unit/services/fileUpload.spec.ts b/packages/middleware/__tests__/unit/services/fileUpload.spec.ts new file mode 100644 index 0000000000..266e6f48ed --- /dev/null +++ b/packages/middleware/__tests__/unit/services/fileUpload.spec.ts @@ -0,0 +1,126 @@ +import express from "express"; +import multer from "multer"; +import { configureFileUpload } from "../../../src/services/fileUpload"; + +// Mock multer +jest.mock("fileUpload", () => { + const mockMulter = jest.fn(() => ({ + fields: jest.fn(), + any: jest.fn(), + })) as jest.Mock & { memoryStorage: jest.Mock }; + mockMulter.memoryStorage = jest.fn(); + return mockMulter; +}); + +describe("configureFileUpload", () => { + let app: express.Express; + + beforeEach(() => { + app = express(); + app.use = jest.fn(); + jest.clearAllMocks(); + }); + + it("should not configure upload when enabled is false", () => { + configureFileUpload(app, { enabled: false }); + expect(app.use).not.toHaveBeenCalled(); + expect(multer).not.toHaveBeenCalled(); + }); + + it("should configure upload with default options when enabled", () => { + configureFileUpload(app, { enabled: true }); + + expect(multer).toHaveBeenCalledWith( + expect.objectContaining({ + limits: { + fileSize: 5 * 1024 * 1024, + files: 5, + }, + }) + ); + expect(app.use).toHaveBeenCalled(); + }); + + it("should configure upload with custom options", () => { + const customOptions = { + enabled: true, + maxFileSize: 10 * 1024 * 1024, + maxFiles: 3, + allowedMimeTypes: ["image/jpeg"], + }; + + configureFileUpload(app, customOptions); + + expect(multer).toHaveBeenCalledWith( + expect.objectContaining({ + limits: { + fileSize: 10 * 1024 * 1024, + files: 3, + }, + }) + ); + }); + + it("should use fields() when fieldNames are provided", () => { + configureFileUpload(app, { + enabled: true, + fieldNames: ["avatar", "document"], + }); + + const expectedFields = [ + { name: "avatar", maxCount: 1 }, + { name: "document", maxCount: 1 }, + ]; + + const multerInstance = (multer as unknown as jest.Mock).mock.results[0] + .value; + expect(multerInstance.fields).toHaveBeenCalledWith(expectedFields); + }); + + it("should use any() when no fieldNames are provided", () => { + configureFileUpload(app, { enabled: true }); + + const multerInstance = (multer as unknown as jest.Mock).mock.results[0] + .value; + expect(multerInstance.any).toHaveBeenCalled(); + }); + + describe("fileFilter", () => { + let fileFilter: (req: any, file: any, cb: any) => void; + + beforeEach(() => { + configureFileUpload(app, { + enabled: true, + allowedMimeTypes: ["image/*", "application/pdf"], + }); + fileFilter = (multer as unknown as jest.Mock).mock.calls[0][0].fileFilter; + }); + + it("should accept files when no mime types are specified", () => { + const cb = jest.fn(); + configureFileUpload(app, { enabled: true, allowedMimeTypes: [] }); + fileFilter = (multer as unknown as jest.Mock).mock.calls[1][0].fileFilter; + + fileFilter(null, { mimetype: "anything" }, cb); + expect(cb).toHaveBeenCalledWith(null, true); + }); + + it("should accept files matching exact mime type", () => { + const cb = jest.fn(); + fileFilter(null, { mimetype: "application/pdf" }, cb); + expect(cb).toHaveBeenCalledWith(null, true); + }); + + it("should accept files matching wildcard mime type", () => { + const cb = jest.fn(); + fileFilter(null, { mimetype: "image/jpeg" }, cb); + expect(cb).toHaveBeenCalledWith(null, true); + }); + + it("should reject files with unallowed mime types", () => { + const cb = jest.fn(); + fileFilter(null, { mimetype: "text/plain" }, cb); + expect(cb).toHaveBeenCalledWith(null, false); + }); + }); +}); diff --git a/packages/middleware/package.json b/packages/middleware/package.json index e077bceac7..1b69bd6b15 100644 --- a/packages/middleware/package.json +++ b/packages/middleware/package.json @@ -27,7 +27,9 @@ "helmet": "^5.1.1", "@godaddy/terminus": "^4.12.1", "lodash.merge": "^4.6.2", - "xss": "^1.0.15" + "xss": "^1.0.15", + "multer": "^1.4.5-lts.1", + "@types/multer": "^1.4.7" }, "devDependencies": { "@types/body-parser": "^1.19.2", diff --git a/packages/middleware/src/createServer.ts b/packages/middleware/src/createServer.ts index 2c4c1f223a..0b2c4825f0 100644 --- a/packages/middleware/src/createServer.ts +++ b/packages/middleware/src/createServer.ts @@ -25,6 +25,7 @@ import { } from "./handlers"; import { createTerminusOptions } from "./terminus"; import { prepareLogger } from "./handlers/prepareLogger"; +import { configureFileUpload } from "./services/fileUpload"; const defaultCorsOptions: CreateServerOptions["cors"] = { credentials: true, @@ -50,6 +51,8 @@ async function createServer< const app = express(); + configureFileUpload(app, options.fileUpload); + app.use(express.json(options.bodyParser)); app.use( options.cookieParser diff --git a/packages/middleware/src/services/fileUpload.ts b/packages/middleware/src/services/fileUpload.ts new file mode 100644 index 0000000000..0ac5977cb1 --- /dev/null +++ b/packages/middleware/src/services/fileUpload.ts @@ -0,0 +1,51 @@ +import multer from "multer"; +import type { Express } from "express"; +import type { FileUploadOptions } from "../types"; + +const DEFAULT_OPTIONS: FileUploadOptions = { + enabled: false, + maxFileSize: 5 * 1024 * 1024, // 5MB + maxFiles: 5, + allowedMimeTypes: ["image/*", "application/pdf"], +}; + +export function configureFileUpload(app: Express, options?: FileUploadOptions) { + const config = { ...DEFAULT_OPTIONS, ...options }; + + if (!config.enabled) { + return; + } + + const storage = multer.memoryStorage(); + + const upload = multer({ + storage, + limits: { + fileSize: config.maxFileSize, + files: config.maxFiles, + }, + fileFilter: (_req, file, cb) => { + if (!config.allowedMimeTypes?.length) { + return cb(null, true); + } + + const isAllowed = config.allowedMimeTypes.some((type) => { + if (type.endsWith("/*")) { + const mainType = type.split("/")[0]; + return file.mimetype.startsWith(`${mainType}/`); + } + return type === file.mimetype; + }); + + cb(null, isAllowed); + }, + }); + + // If specific field names are provided, use fields() + if (config.fieldNames?.length) { + const fields = config.fieldNames.map((name) => ({ name, maxCount: 1 })); + app.use(upload.fields(fields)); + } else { + app.use(upload.any()); + } +} diff --git a/packages/middleware/src/types/common.ts b/packages/middleware/src/types/common.ts index 782378aaee..8f65a9ddb6 100644 --- a/packages/middleware/src/types/common.ts +++ b/packages/middleware/src/types/common.ts @@ -22,6 +22,14 @@ export type ResponseWithAlokaiLocals = Response< [key: string]: any; } >; +export type RequestWithFiles = Request & { + files?: + | { + [fieldname: string]: Express.Multer.File[]; + } + | Express.Multer.File[] + | undefined; +}; export type ExtensionEndpointHandler = ApiClientMethod & { _extensionName?: string; @@ -96,7 +104,7 @@ export interface ApiClientExtension { logger: LoggerInterface; }) => Promise | void; hooks?: ( - req: Request, + req: RequestWithFiles, res: ResponseWithAlokaiLocals, hooksContext: AlokaiContainer ) => ApiClientExtensionHooks; @@ -119,7 +127,7 @@ export interface Integration< initConfig?: TObject; errorHandler?: ( error: unknown, - req: Request, + req: RequestWithFiles, res: ResponseWithAlokaiLocals ) => void; } @@ -158,7 +166,7 @@ export type IntegrationsLoaded< > = Record>; export interface MiddlewareContext { - req: Request; + req: RequestWithFiles; res: ResponseWithAlokaiLocals; extensions: ApiClientExtension[]; customQueries: Record; diff --git a/packages/middleware/src/types/fileUpload.ts b/packages/middleware/src/types/fileUpload.ts new file mode 100644 index 0000000000..6cf8ae7e14 --- /dev/null +++ b/packages/middleware/src/types/fileUpload.ts @@ -0,0 +1,7 @@ +export interface FileUploadOptions { + enabled?: boolean; + maxFileSize?: number; // in bytes + maxFiles?: number; + allowedMimeTypes?: string[]; + fieldNames?: string[]; // specific field names to accept +} diff --git a/packages/middleware/src/types/index.ts b/packages/middleware/src/types/index.ts index 1d3037c3ce..4d492a9ed5 100644 --- a/packages/middleware/src/types/index.ts +++ b/packages/middleware/src/types/index.ts @@ -39,3 +39,4 @@ export * from "./base"; export * from "./common"; export * from "./server"; export * from "./config"; +export * from "./fileUpload"; diff --git a/packages/middleware/src/types/server.ts b/packages/middleware/src/types/server.ts index e956f0b76a..f4cfa7cc48 100644 --- a/packages/middleware/src/types/server.ts +++ b/packages/middleware/src/types/server.ts @@ -9,6 +9,7 @@ import { ApiMethodsFactory, MiddlewareContext, } from "./common"; +import { FileUploadOptions } from "./fileUpload"; export interface ClientContext { client: CLIENT; @@ -173,4 +174,10 @@ export interface CreateServerOptions { * @see https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/#define-readiness-probes */ readinessProbes?: ReadinessProbe[]; + + /** + * Configuration options for handling file uploads. + * @see FileUploadOptions + */ + fileUpload?: FileUploadOptions; } diff --git a/packages/middleware/tsconfig.json b/packages/middleware/tsconfig.json index b966bf605d..409b0459c6 100644 --- a/packages/middleware/tsconfig.json +++ b/packages/middleware/tsconfig.json @@ -5,7 +5,8 @@ "outDir": "./lib", "declarationDir": "./lib", "declaration": true, - "rootDir": "./src" + "rootDir": "./src", + "typeRoots": ["./src/types"] }, "exclude": ["node_modules"], "include": ["src/**/*.ts"] diff --git a/yarn.lock b/yarn.lock index 2ebec1361e..015c4c2cba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2370,6 +2370,13 @@ dependencies: "@types/node" "*" +"@types/multer@^1.4.7": + version "1.4.12" + resolved "https://registrynpm.storefrontcloud.io/@types/multer/-/multer-1.4.12.tgz#da67bd0c809f3a63fe097c458c0d4af1fea50ab7" + integrity sha512-pQ2hoqvXiJt2FP9WQVLPRO+AmiIm/ZYkavPlIQnx282u4ZrVdztx0pkh3jjpQt0Kz+YI0YhSG264y08UJKoUQg== + dependencies: + "@types/express" "*" + "@types/node@*": version "18.0.1" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.0.1.tgz#e91bd73239b338557a84d1f67f7b9e0f25643870" @@ -2883,6 +2890,11 @@ anymatch@~3.1.2: normalize-path "^3.0.0" picomatch "^2.0.4" +append-field@^1.0.0: + version "1.0.0" + resolved "https://registrynpm.storefrontcloud.io/append-field/-/append-field-1.0.0.tgz#1e3440e915f0b1203d23748e78edd7b9b5b43e56" + integrity sha1-HjRA6RXwsSA9I3SOeO3XubW0PlY= + "aproba@^1.0.3 || ^2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc" @@ -3813,6 +3825,13 @@ builtins@^5.0.0: dependencies: semver "^7.0.0" +busboy@^1.0.0: + version "1.6.0" + resolved "https://registrynpm.storefrontcloud.io/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893" + integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA== + dependencies: + streamsearch "^1.1.0" + byline@5.x: version "5.0.0" resolved "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz#741c5216468eadc457b03410118ad77de8c1ddb1" @@ -4321,6 +4340,16 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== +concat-stream@^1.5.2: + version "1.6.2" + resolved "https://registrynpm.storefrontcloud.io/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" + integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + concurrently@^7.6.0: version "7.6.0" resolved "https://registry.npmjs.org/concurrently/-/concurrently-7.6.0.tgz#531a6f5f30cf616f355a4afb8f8fcb2bba65a49a" @@ -8921,7 +8950,7 @@ mkdirp-infer-owner@^2.0.0: infer-owner "^1.0.4" mkdirp "^1.0.3" -mkdirp@^0.5.1: +mkdirp@^0.5.1, mkdirp@^0.5.4: version "0.5.6" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== @@ -8963,6 +8992,19 @@ ms@2.1.3, ms@^2.0.0, ms@^2.1.1, ms@^2.1.3: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== +multer@^1.4.5-lts.1: + version "1.4.5-lts.1" + resolved "https://registrynpm.storefrontcloud.io/multer/-/multer-1.4.5-lts.1.tgz#803e24ad1984f58edffbc79f56e305aec5cfd1ac" + integrity sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ== + dependencies: + append-field "^1.0.0" + busboy "^1.0.0" + concat-stream "^1.5.2" + mkdirp "^0.5.4" + object-assign "^4.1.1" + type-is "^1.6.4" + xtend "^4.0.0" + multimatch@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/multimatch/-/multimatch-5.0.0.tgz#932b800963cea7a31a033328fa1e0c3a1874dbe6" @@ -10150,7 +10192,7 @@ read-yaml-file@^1.1.0: pify "^4.0.1" strip-bom "^3.0.0" -readable-stream@^2.0.2, readable-stream@^2.3.5: +readable-stream@^2.0.2, readable-stream@^2.2.2, readable-stream@^2.3.5: version "2.3.8" resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== @@ -11041,6 +11083,11 @@ stream-transform@^2.1.3: dependencies: mixme "^0.5.1" +streamsearch@^1.1.0: + version "1.1.0" + resolved "https://registrynpm.storefrontcloud.io/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" + integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== + string-argv@0.3.2, string-argv@~0.3.1: version "0.3.2" resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.2.tgz#2b6d0ef24b656274d957d54e0a4bbf6153dc02b6" @@ -11756,7 +11803,7 @@ type-fest@^1.0.2: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-1.4.0.tgz#e9fb813fe3bf1744ec359d55d1affefa76f14be1" integrity sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA== -type-is@~1.6.18: +type-is@^1.6.4, type-is@~1.6.18: version "1.6.18" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== @@ -11847,6 +11894,11 @@ typed-array-length@^1.0.6: is-typed-array "^1.1.13" possible-typed-array-names "^1.0.0" +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registrynpm.storefrontcloud.io/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= + typescript-eslint@8.13.0: version "8.13.0" resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.13.0.tgz#c7d92cc06188176c7d0e3825e10305b9c22fb102"