From 2ea95229408e34ab5ed6faaa4f90768372825fb6 Mon Sep 17 00:00:00 2001 From: ditiqi Date: Tue, 27 May 2025 08:56:38 +0800 Subject: [PATCH 1/9] add --- apps/backend/src/index.ts | 80 +++++++++++------------ apps/backend/src/router.ts | 8 +++ apps/backend/src/trpc.ts | 16 ++--- apps/backend/tsconfig.json | 19 +++--- apps/web/app/page.tsx | 2 + apps/web/components/providers.tsx | 21 +++--- dockers/storage-minio.yml | 8 +-- packages/client/src/api/hooks/useHello.ts | 1 - 8 files changed, 79 insertions(+), 76 deletions(-) create mode 100644 apps/backend/src/router.ts diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts index 3550582..604d5b1 100644 --- a/apps/backend/src/index.ts +++ b/apps/backend/src/index.ts @@ -1,50 +1,50 @@ -import { Hono } from 'hono' -import { logger } from 'hono/logger' -import { contextStorage, getContext } from 'hono/context-storage' -import { prettyJSON } from 'hono/pretty-json' +import { Hono } from 'hono'; +import { logger } from 'hono/logger'; +import { contextStorage, getContext } from 'hono/context-storage'; +import { prettyJSON } from 'hono/pretty-json'; -import { trpcServer } from '@hono/trpc-server' -import { appRouter } from './trpc' -import Redis from 'ioredis' -import redis from './redis' -import minioClient from './minio' -import { Client } from 'minio' -import oidc from './oidc/provider' +import { trpcServer } from '@hono/trpc-server'; +import { appRouter } from './router'; +import Redis from 'ioredis'; +import redis from './redis'; +import minioClient from './minio'; +import { Client } from 'minio'; +import oidc from './oidc/provider'; type Env = { - Variables: { - redis: Redis - minio: Client - } -} + Variables: { + redis: Redis; + minio: Client; + }; +}; -const app = new Hono() +const app = new Hono(); app.use('*', async (c, next) => { - c.set('redis', redis) - c.set('minio', minioClient) - await next() -}) -app.use('*', async (c, next) => { - c.set('redis', redis); - await next(); + c.set('redis', redis); + c.set('minio', minioClient); + await next(); }); -app.use(contextStorage()) -app.use(prettyJSON()) // With options: prettyJSON({ space: 4 }) -app.use(logger()) +app.use('*', async (c, next) => { + c.set('redis', redis); + await next(); +}); +app.use(contextStorage()); +app.use(prettyJSON()); // With options: prettyJSON({ space: 4 }) +app.use(logger()); app.use( - '/trpc/*', - trpcServer({ - router: appRouter, - }) -) + '/trpc/*', + trpcServer({ + router: appRouter, + }), +); app.get('/', (c) => { - return c.text('Hello Hono!') -}) + return c.text('Hello Hono!'); +}); app.use('/oidc/*', async (c, next) => { - // @ts-ignore - await oidc.callback(c.req.raw, c.res.raw) - // return void 也可以 - return -}) -export default app + // @ts-ignore + await oidc.callback(c.req.raw, c.res.raw); + // return void 也可以 + return; +}); +export default app; diff --git a/apps/backend/src/router.ts b/apps/backend/src/router.ts new file mode 100644 index 0000000..26f009c --- /dev/null +++ b/apps/backend/src/router.ts @@ -0,0 +1,8 @@ +import { router } from './trpc'; +import { userRouter } from './user/user.trpc'; + +export const appRouter = router({ + user: userRouter, +}); + +export type AppRouter = typeof appRouter; diff --git a/apps/backend/src/trpc.ts b/apps/backend/src/trpc.ts index 9e751a2..32bd49e 100644 --- a/apps/backend/src/trpc.ts +++ b/apps/backend/src/trpc.ts @@ -1,14 +1,6 @@ -import { z } from 'zod' -import { initTRPC } from '@trpc/server' -import { userRouter } from './user/user.trpc' +import { initTRPC } from '@trpc/server'; -const t = initTRPC.create() +const t = initTRPC.create(); -export const publicProcedure = t.procedure -export const router = t.router - -export const appRouter = router({ - user: userRouter -}) - -export type AppRouter = typeof appRouter \ No newline at end of file +export const publicProcedure = t.procedure; +export const router = t.router; diff --git a/apps/backend/tsconfig.json b/apps/backend/tsconfig.json index 55a8094..53412b3 100644 --- a/apps/backend/tsconfig.json +++ b/apps/backend/tsconfig.json @@ -1,11 +1,10 @@ { - "extends": "@repo/typescript-config/hono.json", - "compilerOptions": { - "moduleResolution": "bundler", - "paths": { - "@/*": ["./*"], - "@repo/db/*": ["../../packages/db/src/*"], - }, - - } -} \ No newline at end of file + "extends": "@repo/typescript-config/hono.json", + "compilerOptions": { + "moduleResolution": "bundler", + "paths": { + "@/*": ["./*"], + "@repo/db/*": ["../../packages/db/src/*"] + } + } +} diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index b76f59b..eb60d59 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -1,5 +1,7 @@ 'use client'; +import { useTRPC } from '@repo/client'; + // import { api } from '@repo/client'; // import { Button } from '@repo/ui/components/button'; // import { useState } from 'react'; diff --git a/apps/web/components/providers.tsx b/apps/web/components/providers.tsx index 49ecbf4..4fb33d2 100644 --- a/apps/web/components/providers.tsx +++ b/apps/web/components/providers.tsx @@ -2,17 +2,20 @@ import * as React from 'react'; import { ThemeProvider as NextThemesProvider } from 'next-themes'; +import QueryProvider from '@/providers/query-provider'; export function Providers({ children }: { children: React.ReactNode }) { return ( - - {children} - + + + {children} + + ); } diff --git a/dockers/storage-minio.yml b/dockers/storage-minio.yml index 9b137b0..2ca3c74 100644 --- a/dockers/storage-minio.yml +++ b/dockers/storage-minio.yml @@ -1,11 +1,11 @@ services: minio: - image: minio/minio:${MINIO_VERSION:-RELEASE.2024-04-22T22-12-26Z} + image: minio/minio:${MINIO_VERSION:-RELEASE.2024-04-18T19-09-19Z} container_name: minio restart: always ports: - - "9000:9000" - - "9001:9001" + - '9000:9000' + - '9001:9001' networks: - nice-net environment: @@ -17,7 +17,7 @@ services: target: /data command: server /data --console-address ":9001" healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] + test: ['CMD', 'curl', '-f', 'http://localhost:9000/minio/health/live'] interval: 30s timeout: 20s retries: 3 diff --git a/packages/client/src/api/hooks/useHello.ts b/packages/client/src/api/hooks/useHello.ts index 00b5155..a2fc1a2 100644 --- a/packages/client/src/api/hooks/useHello.ts +++ b/packages/client/src/api/hooks/useHello.ts @@ -1,4 +1,3 @@ -import { api } from '../trpc'; export function useHello() { return { From 3f8d9b50236dcebcd23d8718927cce29f9f487b8 Mon Sep 17 00:00:00 2001 From: ditiqi Date: Tue, 27 May 2025 09:18:05 +0800 Subject: [PATCH 2/9] add --- dockers/search-elasticsearch.yml | 8 ++++---- dockers/storage-minio.yml | 4 +--- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/dockers/search-elasticsearch.yml b/dockers/search-elasticsearch.yml index ff0359f..b0469a1 100644 --- a/dockers/search-elasticsearch.yml +++ b/dockers/search-elasticsearch.yml @@ -4,15 +4,15 @@ services: container_name: elasticsearch restart: always ports: - - "9200:9200" - - "9300:9300" + - '9200:9200' + - '9300:9300' networks: - nice-net environment: - discovery.type=single-node - xpack.security.enabled=true - ELASTIC_PASSWORD=${ELASTIC_PASSWORD:-nice_elastic_password} - - "ES_JAVA_OPTS=-Xms512m -Xmx512m" + - 'ES_JAVA_OPTS=-Xms512m -Xmx512m' volumes: - type: volume source: elasticsearch_data @@ -29,4 +29,4 @@ volumes: driver_opts: type: none device: ${PWD}/volumes/elasticsearch - o: bind \ No newline at end of file + o: bind diff --git a/dockers/storage-minio.yml b/dockers/storage-minio.yml index 2ca3c74..1040479 100644 --- a/dockers/storage-minio.yml +++ b/dockers/storage-minio.yml @@ -12,9 +12,7 @@ services: - MINIO_ACCESS_KEY=${MINIO_ACCESS_KEY:-nice_minio_access} - MINIO_SECRET_KEY=${MINIO_SECRET_KEY:-nice_minio_secret} volumes: - - type: volume - source: storage_data - target: /data + - ./volumes/minio:/data command: server /data --console-address ":9001" healthcheck: test: ['CMD', 'curl', '-f', 'http://localhost:9000/minio/health/live'] From 56328750f0ba304aa4ab4d52fa39338726ad43c6 Mon Sep 17 00:00:00 2001 From: ditiqi Date: Tue, 27 May 2025 09:38:02 +0800 Subject: [PATCH 3/9] add --- packages/icons/package.json | 113 ++++++++++++++-------------- packages/icons/tsconfig.json | 36 ++++----- packages/icons/tsconfig.tsbuildinfo | 2 +- pnpm-lock.yaml | 66 ++-------------- 4 files changed, 81 insertions(+), 136 deletions(-) diff --git a/packages/icons/package.json b/packages/icons/package.json index b3129a8..7956201 100644 --- a/packages/icons/package.json +++ b/packages/icons/package.json @@ -1,58 +1,59 @@ { - "name": "@repo/icons", - "version": "1.7.0", - "license": "MIT", - "homepage": "https://github.com/teableio/teable", - "publishConfig": { - "access": "public" - }, - "repository": { - "type": "git", - "url": "https://github.com/teableio/teable", - "directory": "packages/icons" - }, - "main": "./dist/index.js", - "module": "./dist/index.js", - "types": "./dist/index.d.ts", - "exports": { - ".": { - "import": "./dist/index.js", - "require": "./dist/index.js", - "types": "./dist/index.d.ts" - } - }, - "files": [ - "dist" - ], - "scripts": { - "build": "tsc", - "clean": "rimraf ./dist ./build ./tsconfig.tsbuildinfo ./node_modules/.cache", - "dev": "rm -rf dist && tsc --watch", - "test": "echo \"Error: no test specified\"", - "lint": "eslint . --ext .ts,.tsx,.js,.jsx,.cjs,.mjs,.mdx --cache --cache-location ../../.cache/eslint/icons.eslintcache", - "typecheck": "tsc --project ./tsconfig.json --noEmit", - "generate": "rm -rf src/components && node ./scripts/generate.mjs" - }, - "peerDependencies": { - "react": "^19.1.0", - "react-dom": "^19.1.0" - }, - "devDependencies": { - "@svgr/core": "8.1.0", - "@svgr/plugin-jsx": "8.1.0", - "@svgr/plugin-prettier": "8.1.0", - "@svgr/plugin-svgo": "8.1.0", - "@types/fs-extra": "11.0.4", - "@types/node": "20.9.0", - "@types/react": "18.2.45", - "axios": "1.7.7", - "chalk": "5.3.0", - "dotenv": "16.4.5", - "eslint": "8.57.0", - "figma-js": "1.16.0", - "fs-extra": "11.2.0", - "lodash": "4.17.21", - "rimraf": "5.0.5", - "typescript": "5.4.3" - } + "name": "@repo/icons", + "version": "1.7.0", + "license": "MIT", + "homepage": "https://github.com/teableio/teable", + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "https://github.com/teableio/teable", + "directory": "packages/icons" + }, + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "import": "./dist/index.js", + "require": "./dist/index.js", + "types": "./dist/index.d.ts" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsc", + "clean": "rimraf ./dist ./build ./tsconfig.tsbuildinfo ./node_modules/.cache", + "dev": "rm -rf dist && tsc --watch", + "test": "echo \"Error: no test specified\"", + "lint": "eslint . --ext .ts,.tsx,.js,.jsx,.cjs,.mjs,.mdx --cache --cache-location ../../.cache/eslint/icons.eslintcache", + "typecheck": "tsc --project ./tsconfig.json --noEmit", + "generate": "rm -rf src/components && node ./scripts/generate.mjs" + }, + "peerDependencies": { + "react": "^19.1.0", + "react-dom": "^19.1.0" + }, + "devDependencies": { + "@svgr/core": "8.1.0", + "@svgr/plugin-jsx": "8.1.0", + "@svgr/plugin-prettier": "8.1.0", + "@svgr/plugin-svgo": "8.1.0", + "@types/fs-extra": "11.0.4", + "@types/node": "20.9.0", + "@types/react": "^19.1.4", + "@types/react-dom": "^19.1.5", + "axios": "1.7.7", + "chalk": "5.3.0", + "dotenv": "16.4.5", + "eslint": "8.57.0", + "figma-js": "1.16.0", + "fs-extra": "11.2.0", + "lodash": "4.17.21", + "rimraf": "5.0.5", + "typescript": "5.4.3" + } } diff --git a/packages/icons/tsconfig.json b/packages/icons/tsconfig.json index 953f496..ac24b11 100644 --- a/packages/icons/tsconfig.json +++ b/packages/icons/tsconfig.json @@ -1,20 +1,20 @@ { - "$schema": "https://json.schemastore.org/tsconfig", - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "jsx": "react", - "baseUrl": "./src", - "target": "esnext", - "outDir": "./dist", - "rootDir": "./src", - "lib": ["dom", "dom.iterable", "esnext"], - "module": "esnext", - "noEmit": false, - "incremental": true, - "composite": true, - "moduleResolution": "node", - "esModuleInterop": true - }, - "exclude": ["**/node_modules", "**/.*/", "dist", "build"], - "include": ["./src"] + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@repo/typescript-config/base.json", + "compilerOptions": { + "jsx": "react", + "baseUrl": "./src", + "target": "esnext", + "outDir": "./dist", + "rootDir": "./src", + "lib": ["dom", "dom.iterable", "esnext"], + "module": "esnext", + "noEmit": false, + "incremental": true, + "composite": true, + "moduleResolution": "node", + "esModuleInterop": true + }, + "exclude": ["**/node_modules", "**/.*/", "dist", "build"], + "include": ["./src"] } diff --git a/packages/icons/tsconfig.tsbuildinfo b/packages/icons/tsconfig.tsbuildinfo index a1861a7..8ed2261 100644 --- a/packages/icons/tsconfig.tsbuildinfo +++ b/packages/icons/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"program":{"fileNames":["../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es5.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2015.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2016.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2017.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2018.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2019.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2020.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2021.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2022.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2023.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.esnext.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.dom.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.dom.iterable.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2015.core.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2015.collection.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2015.generator.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2015.iterable.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2015.promise.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2015.proxy.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2015.reflect.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2015.symbol.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2016.array.include.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2016.intl.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2017.date.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2017.object.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2017.string.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2017.intl.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2018.intl.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2018.promise.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2018.regexp.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2019.array.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2019.object.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2019.string.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2019.symbol.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2019.intl.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2020.bigint.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2020.date.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2020.promise.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2020.string.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2020.intl.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2020.number.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2021.promise.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2021.string.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2021.weakref.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2021.intl.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2022.array.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2022.error.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2022.intl.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2022.object.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2022.sharedmemory.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2022.string.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2022.regexp.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2023.array.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2023.collection.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.esnext.collection.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.esnext.intl.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.esnext.disposable.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.esnext.promise.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.esnext.decorators.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.esnext.object.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.decorators.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.decorators.legacy.d.ts","../../node_modules/.pnpm/@types+react@18.2.45/node_modules/@types/react/global.d.ts","../../node_modules/.pnpm/csstype@3.1.3/node_modules/csstype/index.d.ts","../../node_modules/.pnpm/@types+prop-types@15.7.14/node_modules/@types/prop-types/index.d.ts","../../node_modules/.pnpm/@types+react@18.2.45/node_modules/@types/react/index.d.ts","./src/components/A.tsx","./src/components/Admin.tsx","./src/components/AlertCircle.tsx","./src/components/AlertTriangle.tsx","./src/components/Anthropic.tsx","./src/components/Apple.tsx","./src/components/Array.tsx","./src/components/ArrowDown.tsx","./src/components/ArrowLeft.tsx","./src/components/ArrowRight.tsx","./src/components/ArrowUp.tsx","./src/components/ArrowUpDown.tsx","./src/components/ArrowUpRight.tsx","./src/components/Audio.tsx","./src/components/Azure.tsx","./src/components/BarChart2.tsx","./src/components/Bell.tsx","./src/components/Boolean.tsx","./src/components/Building2.tsx","./src/components/Calendar.tsx","./src/components/Check.tsx","./src/components/CheckCircle2.tsx","./src/components/CheckSquare.tsx","./src/components/Checked.tsx","./src/components/ChevronDown.tsx","./src/components/ChevronLeft.tsx","./src/components/ChevronRight.tsx","./src/components/ChevronUp.tsx","./src/components/ChevronsLeft.tsx","./src/components/ChevronsRight.tsx","./src/components/ChevronsUpDown.tsx","./src/components/Circle.tsx","./src/components/ClipboardList.tsx","./src/components/Clock4.tsx","./src/components/Code.tsx","./src/components/Code2.tsx","./src/components/Cohere.tsx","./src/components/Component.tsx","./src/components/Condition.tsx","./src/components/Copy.tsx","./src/components/CreateRecord.tsx","./src/components/CreditCard.tsx","./src/components/Database.tsx","./src/components/DeepThinking.tsx","./src/components/Deepseek.tsx","./src/components/DivideCircle.tsx","./src/components/DivideSquare.tsx","./src/components/DollarSign.tsx","./src/components/Download.tsx","./src/components/DraggableHandle.tsx","./src/components/Edit.tsx","./src/components/Export.tsx","./src/components/Eye.tsx","./src/components/EyeOff.tsx","./src/components/File.tsx","./src/components/FileAudio.tsx","./src/components/FileCsv.tsx","./src/components/FileDocument.tsx","./src/components/FileExcel.tsx","./src/components/FileFont.tsx","./src/components/FileImage.tsx","./src/components/FileJson.tsx","./src/components/FilePack.tsx","./src/components/FilePdf.tsx","./src/components/FilePresentation.tsx","./src/components/FileQuestion.tsx","./src/components/FileScript.tsx","./src/components/FileSpreadsheet.tsx","./src/components/FileText.tsx","./src/components/FileUnknown.tsx","./src/components/FileVideo.tsx","./src/components/Filter.tsx","./src/components/Flame.tsx","./src/components/FreezeColumn.tsx","./src/components/Frown.tsx","./src/components/Gauge.tsx","./src/components/GetRecord.tsx","./src/components/Github.tsx","./src/components/GithubLogo.tsx","./src/components/GoogleLogo.tsx","./src/components/Hash.tsx","./src/components/Heart.tsx","./src/components/HelpCircle.tsx","./src/components/History.tsx","./src/components/Home.tsx","./src/components/HttpRequest.tsx","./src/components/Image.tsx","./src/components/Import.tsx","./src/components/Inbox.tsx","./src/components/Integration.tsx","./src/components/Kanban.tsx","./src/components/Key.tsx","./src/components/Layers.tsx","./src/components/LayoutGrid.tsx","./src/components/LayoutList.tsx","./src/components/LayoutTemplate.tsx","./src/components/License.tsx","./src/components/Lingyiwanwu.tsx","./src/components/Link.tsx","./src/components/ListChecks.tsx","./src/components/ListOrdered.tsx","./src/components/Loader2.tsx","./src/components/Lock.tsx","./src/components/LongText.tsx","./src/components/Mail.tsx","./src/components/MarkUnread.tsx","./src/components/Maximize2.tsx","./src/components/Menu.tsx","./src/components/MessageSquare.tsx","./src/components/Minimize2.tsx","./src/components/Mistral.tsx","./src/components/Moon.tsx","./src/components/MoreHorizontal.tsx","./src/components/Network.tsx","./src/components/Object.tsx","./src/components/Ollama.tsx","./src/components/Openai.tsx","./src/components/PackageCheck.tsx","./src/components/PaintBucket.tsx","./src/components/Pencil.tsx","./src/components/Percent.tsx","./src/components/Phone.tsx","./src/components/Play.tsx","./src/components/Plus.tsx","./src/components/PlusCircle.tsx","./src/components/Puzzle.tsx","./src/components/Qrcode.tsx","./src/components/Qwen.tsx","./src/components/Redo2.tsx","./src/components/RefreshCcw.tsx","./src/components/RotateCw.tsx","./src/components/Search.tsx","./src/components/SendMail.tsx","./src/components/Settings.tsx","./src/components/Share2.tsx","./src/components/Sheet.tsx","./src/components/ShieldCheck.tsx","./src/components/Sidebar.tsx","./src/components/SortAsc.tsx","./src/components/Square.tsx","./src/components/Star.tsx","./src/components/StretchHorizontal.tsx","./src/components/Sun.tsx","./src/components/SunMedium.tsx","./src/components/Table2.tsx","./src/components/Teable.tsx","./src/components/TeableNew.tsx","./src/components/ThumbsUp.tsx","./src/components/Trash.tsx","./src/components/Trash2.tsx","./src/components/Undo2.tsx","./src/components/UpdateRecord.tsx","./src/components/User.tsx","./src/components/UserEdit.tsx","./src/components/UserPlus.tsx","./src/components/Users.tsx","./src/components/Video.tsx","./src/components/Webhook.tsx","./src/components/X.tsx","./src/components/Xai.tsx","./src/components/Zap.tsx","./src/components/Zhipu.tsx","./src/components/ZoomIn.tsx","./src/components/ZoomOut.tsx","./src/index.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/assert.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/assert/strict.d.ts","../../node_modules/.pnpm/buffer@6.0.3/node_modules/buffer/index.d.ts","../../node_modules/.pnpm/undici-types@5.26.5/node_modules/undici-types/header.d.ts","../../node_modules/.pnpm/undici-types@5.26.5/node_modules/undici-types/readable.d.ts","../../node_modules/.pnpm/undici-types@5.26.5/node_modules/undici-types/file.d.ts","../../node_modules/.pnpm/undici-types@5.26.5/node_modules/undici-types/fetch.d.ts","../../node_modules/.pnpm/undici-types@5.26.5/node_modules/undici-types/formdata.d.ts","../../node_modules/.pnpm/undici-types@5.26.5/node_modules/undici-types/connector.d.ts","../../node_modules/.pnpm/undici-types@5.26.5/node_modules/undici-types/client.d.ts","../../node_modules/.pnpm/undici-types@5.26.5/node_modules/undici-types/errors.d.ts","../../node_modules/.pnpm/undici-types@5.26.5/node_modules/undici-types/dispatcher.d.ts","../../node_modules/.pnpm/undici-types@5.26.5/node_modules/undici-types/global-dispatcher.d.ts","../../node_modules/.pnpm/undici-types@5.26.5/node_modules/undici-types/global-origin.d.ts","../../node_modules/.pnpm/undici-types@5.26.5/node_modules/undici-types/pool-stats.d.ts","../../node_modules/.pnpm/undici-types@5.26.5/node_modules/undici-types/pool.d.ts","../../node_modules/.pnpm/undici-types@5.26.5/node_modules/undici-types/handlers.d.ts","../../node_modules/.pnpm/undici-types@5.26.5/node_modules/undici-types/balanced-pool.d.ts","../../node_modules/.pnpm/undici-types@5.26.5/node_modules/undici-types/agent.d.ts","../../node_modules/.pnpm/undici-types@5.26.5/node_modules/undici-types/mock-interceptor.d.ts","../../node_modules/.pnpm/undici-types@5.26.5/node_modules/undici-types/mock-agent.d.ts","../../node_modules/.pnpm/undici-types@5.26.5/node_modules/undici-types/mock-client.d.ts","../../node_modules/.pnpm/undici-types@5.26.5/node_modules/undici-types/mock-pool.d.ts","../../node_modules/.pnpm/undici-types@5.26.5/node_modules/undici-types/mock-errors.d.ts","../../node_modules/.pnpm/undici-types@5.26.5/node_modules/undici-types/proxy-agent.d.ts","../../node_modules/.pnpm/undici-types@5.26.5/node_modules/undici-types/api.d.ts","../../node_modules/.pnpm/undici-types@5.26.5/node_modules/undici-types/cookies.d.ts","../../node_modules/.pnpm/undici-types@5.26.5/node_modules/undici-types/patch.d.ts","../../node_modules/.pnpm/undici-types@5.26.5/node_modules/undici-types/filereader.d.ts","../../node_modules/.pnpm/undici-types@5.26.5/node_modules/undici-types/diagnostics-channel.d.ts","../../node_modules/.pnpm/undici-types@5.26.5/node_modules/undici-types/websocket.d.ts","../../node_modules/.pnpm/undici-types@5.26.5/node_modules/undici-types/content-type.d.ts","../../node_modules/.pnpm/undici-types@5.26.5/node_modules/undici-types/cache.d.ts","../../node_modules/.pnpm/undici-types@5.26.5/node_modules/undici-types/interceptors.d.ts","../../node_modules/.pnpm/undici-types@5.26.5/node_modules/undici-types/index.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/globals.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/async_hooks.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/buffer.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/child_process.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/cluster.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/console.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/constants.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/crypto.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/dgram.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/diagnostics_channel.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/dns.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/dns/promises.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/domain.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/dom-events.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/events.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/fs.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/fs/promises.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/http.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/http2.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/https.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/inspector.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/module.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/net.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/os.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/path.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/perf_hooks.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/process.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/punycode.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/querystring.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/readline.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/readline/promises.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/repl.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/stream.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/stream/promises.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/stream/consumers.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/stream/web.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/string_decoder.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/test.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/timers.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/timers/promises.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/tls.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/trace_events.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/tty.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/url.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/util.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/v8.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/vm.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/wasi.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/worker_threads.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/zlib.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/globals.global.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/index.d.ts","../../node_modules/.pnpm/@types+jsonfile@6.1.4/node_modules/@types/jsonfile/index.d.ts","../../node_modules/.pnpm/@types+jsonfile@6.1.4/node_modules/@types/jsonfile/utils.d.ts","../../node_modules/.pnpm/@types+fs-extra@11.0.4/node_modules/@types/fs-extra/index.d.ts","../../node_modules/.pnpm/@types+estree@1.0.6/node_modules/@types/estree/index.d.ts","../../node_modules/.pnpm/@types+json-schema@7.0.15/node_modules/@types/json-schema/index.d.ts","../../node_modules/.pnpm/@types+eslint@9.6.1/node_modules/@types/eslint/use-at-your-own-risk.d.ts","../../node_modules/.pnpm/@types+eslint@9.6.1/node_modules/@types/eslint/index.d.ts","../../node_modules/.pnpm/@types+eslint-scope@3.7.7/node_modules/@types/eslint-scope/index.d.ts","../../node_modules/.pnpm/@types+estree@1.0.7/node_modules/@types/estree/index.d.ts","../../node_modules/.pnpm/@types+react@19.0.0/node_modules/@types/react/index.d.ts"],"fileInfos":[{"version":"824cb491a40f7e8fdeb56f1df5edf91b23f3e3ee6b4cde84d4a99be32338faee","affectsGlobalScope":true},"45b7ab580deca34ae9729e97c13cfd999df04416a79116c3bfb483804f85ded4","3facaf05f0c5fc569c5649dd359892c98a85557e3e0c847964caeb67076f4d75","9a68c0c07ae2fa71b44384a839b7b8d81662a236d4b9ac30916718f7510b1b2d","5e1c4c362065a6b95ff952c0eab010f04dcd2c3494e813b493ecfd4fcb9fc0d8","68d73b4a11549f9c0b7d352d10e91e5dca8faa3322bfb77b661839c42b1ddec7","5efce4fc3c29ea84e8928f97adec086e3dc876365e0982cc8479a07954a3efd4","feecb1be483ed332fad555aff858affd90a48ab19ba7272ee084704eb7167569","5514e54f17d6d74ecefedc73c504eadffdeda79c7ea205cf9febead32d45c4bc","1c0cdb8dc619bc549c3e5020643e7cf7ae7940058e8c7e5aefa5871b6d86f44b","886e50ef125efb7878f744e86908884c0133e7a6d9d80013f421b0cd8fb2af94",{"version":"87d693a4920d794a73384b3c779cadcb8548ac6945aa7a925832fe2418c9527a","affectsGlobalScope":true},{"version":"76f838d5d49b65de83bc345c04aa54c62a3cfdb72a477dc0c0fce89a30596c30","affectsGlobalScope":true},{"version":"138fb588d26538783b78d1e3b2c2cc12d55840b97bf5e08bca7f7a174fbe2f17","affectsGlobalScope":true},{"version":"dc2df20b1bcdc8c2d34af4926e2c3ab15ffe1160a63e58b7e09833f616efff44","affectsGlobalScope":true},{"version":"4443e68b35f3332f753eacc66a04ac1d2053b8b035a0e0ac1d455392b5e243b3","affectsGlobalScope":true},{"version":"bc47685641087c015972a3f072480889f0d6c65515f12bd85222f49a98952ed7","affectsGlobalScope":true},{"version":"0dc1e7ceda9b8b9b455c3a2d67b0412feab00bd2f66656cd8850e8831b08b537","affectsGlobalScope":true},{"version":"ce691fb9e5c64efb9547083e4a34091bcbe5bdb41027e310ebba8f7d96a98671","affectsGlobalScope":true},{"version":"8d697a2a929a5fcb38b7a65594020fcef05ec1630804a33748829c5ff53640d0","affectsGlobalScope":true},{"version":"4ff2a353abf8a80ee399af572debb8faab2d33ad38c4b4474cff7f26e7653b8d","affectsGlobalScope":true},{"version":"93495ff27b8746f55d19fcbcdbaccc99fd95f19d057aed1bd2c0cafe1335fbf0","affectsGlobalScope":true},{"version":"6fc23bb8c3965964be8c597310a2878b53a0306edb71d4b5a4dfe760186bcc01","affectsGlobalScope":true},{"version":"ea011c76963fb15ef1cdd7ce6a6808b46322c527de2077b6cfdf23ae6f5f9ec7","affectsGlobalScope":true},{"version":"38f0219c9e23c915ef9790ab1d680440d95419ad264816fa15009a8851e79119","affectsGlobalScope":true},{"version":"bb42a7797d996412ecdc5b2787720de477103a0b2e53058569069a0e2bae6c7e","affectsGlobalScope":true},{"version":"4738f2420687fd85629c9efb470793bb753709c2379e5f85bc1815d875ceadcd","affectsGlobalScope":true},{"version":"2f11ff796926e0832f9ae148008138ad583bd181899ab7dd768a2666700b1893","affectsGlobalScope":true},{"version":"4de680d5bb41c17f7f68e0419412ca23c98d5749dcaaea1896172f06435891fc","affectsGlobalScope":true},{"version":"9fc46429fbe091ac5ad2608c657201eb68b6f1b8341bd6d670047d32ed0a88fa","affectsGlobalScope":true},{"version":"61c37c1de663cf4171e1192466e52c7a382afa58da01b1dc75058f032ddf0839","affectsGlobalScope":true},{"version":"b541a838a13f9234aba650a825393ffc2292dc0fc87681a5d81ef0c96d281e7a","affectsGlobalScope":true},{"version":"b20fe0eca9a4e405f1a5ae24a2b3290b37cf7f21eba6cbe4fc3fab979237d4f3","affectsGlobalScope":true},{"version":"811ec78f7fefcabbda4bfa93b3eb67d9ae166ef95f9bff989d964061cbf81a0c","affectsGlobalScope":true},{"version":"717937616a17072082152a2ef351cb51f98802fb4b2fdabd32399843875974ca","affectsGlobalScope":true},{"version":"d7e7d9b7b50e5f22c915b525acc5a49a7a6584cf8f62d0569e557c5cfc4b2ac2","affectsGlobalScope":true},{"version":"71c37f4c9543f31dfced6c7840e068c5a5aacb7b89111a4364b1d5276b852557","affectsGlobalScope":true},{"version":"576711e016cf4f1804676043e6a0a5414252560eb57de9faceee34d79798c850","affectsGlobalScope":true},{"version":"89c1b1281ba7b8a96efc676b11b264de7a8374c5ea1e6617f11880a13fc56dc6","affectsGlobalScope":true},{"version":"49ed889be54031e1044af0ad2c603d627b8bda8b50c1a68435fe85583901d072","affectsGlobalScope":true},{"version":"e93d098658ce4f0c8a0779e6cab91d0259efb88a318137f686ad76f8410ca270","affectsGlobalScope":true},{"version":"063600664504610fe3e99b717a1223f8b1900087fab0b4cad1496a114744f8df","affectsGlobalScope":true},{"version":"934019d7e3c81950f9a8426d093458b65d5aff2c7c1511233c0fd5b941e608ab","affectsGlobalScope":true},{"version":"bf14a426dbbf1022d11bd08d6b8e709a2e9d246f0c6c1032f3b2edb9a902adbe","affectsGlobalScope":true},{"version":"5e07ed3809d48205d5b985642a59f2eba47c402374a7cf8006b686f79efadcbd","affectsGlobalScope":true},{"version":"2b72d528b2e2fe3c57889ca7baef5e13a56c957b946906d03767c642f386bbc3","affectsGlobalScope":true},{"version":"8073890e29d2f46fdbc19b8d6d2eb9ea58db9a2052f8640af20baff9afbc8640","affectsGlobalScope":true},{"version":"368af93f74c9c932edd84c58883e736c9e3d53cec1fe24c0b0ff451f529ceab1","affectsGlobalScope":true},{"version":"af3dd424cf267428f30ccfc376f47a2c0114546b55c44d8c0f1d57d841e28d74","affectsGlobalScope":true},{"version":"995c005ab91a498455ea8dfb63aa9f83fa2ea793c3d8aa344be4a1678d06d399","affectsGlobalScope":true},{"version":"51e547984877a62227042850456de71a5c45e7fe86b7c975c6e68896c86fa23b","affectsGlobalScope":true},{"version":"956d27abdea9652e8368ce029bb1e0b9174e9678a273529f426df4b3d90abd60","affectsGlobalScope":true},{"version":"4fa6ed14e98aa80b91f61b9805c653ee82af3502dc21c9da5268d3857772ca05","affectsGlobalScope":true},{"version":"e6633e05da3ff36e6da2ec170d0d03ccf33de50ca4dc6f5aeecb572cedd162fb","affectsGlobalScope":true},{"version":"d8670852241d4c6e03f2b89d67497a4bbefe29ecaa5a444e2c11a9b05e6fccc6","affectsGlobalScope":true},{"version":"8444af78980e3b20b49324f4a16ba35024fef3ee069a0eb67616ea6ca821c47a","affectsGlobalScope":true},{"version":"caccc56c72713969e1cfe5c3d44e5bab151544d9d2b373d7dbe5a1e4166652be","affectsGlobalScope":true},{"version":"3287d9d085fbd618c3971944b65b4be57859f5415f495b33a6adc994edd2f004","affectsGlobalScope":true},{"version":"50d53ccd31f6667aff66e3d62adf948879a3a16f05d89882d1188084ee415bbc","affectsGlobalScope":true},{"version":"08a58483392df5fcc1db57d782e87734f77ae9eab42516028acbfe46f29a3ef7","affectsGlobalScope":true},{"version":"436aaf437562f276ec2ddbee2f2cdedac7664c1e4c1d2c36839ddd582eeb3d0a","affectsGlobalScope":true},{"version":"b1cb28af0c891c8c96b2d6b7be76bd394fddcfdb4709a20ba05a7c1605eea0f9","affectsGlobalScope":true},{"version":"13f6e6380c78e15e140243dc4be2fa546c287c6d61f4729bc2dd7cf449605471","affectsGlobalScope":true},{"version":"15b98a533864d324e5f57cd3cfc0579b231df58c1c0f6063ea0fcb13c3c74ff9","affectsGlobalScope":true},{"version":"ac77cb3e8c6d3565793eb90a8373ee8033146315a3dbead3bde8db5eaf5e5ec6","affectsGlobalScope":true},{"version":"d4b1d2c51d058fc21ec2629fff7a76249dec2e36e12960ea056e3ef89174080f","affectsGlobalScope":true},{"version":"2fef54945a13095fdb9b84f705f2b5994597640c46afeb2ce78352fab4cb3279","affectsGlobalScope":true},{"version":"33358442698bb565130f52ba79bfd3d4d484ac85fe33f3cb1759c54d18201393","affectsGlobalScope":true},{"version":"782dec38049b92d4e85c1585fbea5474a219c6984a35b004963b00beb1aab538","affectsGlobalScope":true},{"version":"0bd5e7096c7bc02bf70b2cc017fc45ef489cb19bd2f32a71af39ff5787f1b56a","affectsGlobalScope":true},"8a8eb4ebffd85e589a1cc7c178e291626c359543403d58c9cd22b81fab5b1fb9","65ff5a0aefd7817a03c1ad04fee85c9cdd3ec415cc3c9efec85d8008d4d5e4ee",{"version":"369b91cb44fdb8a8fa15de2bd6c28a7abfe6cc16d483ea42291cc7b1efff88d4","affectsGlobalScope":true},{"version":"cf4f51c95006178f73b2a5390547f644727f7872beed0a38a9c49b7c62985733","signature":"793fcd8cb05ab3c547c38bf7b6061761aff248308b92711a1685e74b35d4b732"},{"version":"4198f4e6a0b98bf894cef46b9277fc99d98846efaa6bb46042b158523a9c75fa","signature":"3a1028add64854981e9676dcd934cf2413b96d1d3b57ea57d3c4cf2802ccee28"},{"version":"c4cca2c6f64a68927a7793c042f45d7b7dc940b63f4db27a24e6dd4cbcec24b6","signature":"94d7cb03427947d10364eb7489e90dbea08c8f2f3275630684aa10b8861e18d5"},{"version":"82140439b309b322545d9c2f93ebd30a325fac54974d849cbcabf50397eac3af","signature":"123e08996a5090621a564223839369bb8010a16e29b7aca732fdf668b9a82fac"},{"version":"9f01d515a2f1d698cd07485752c835544ff2d91336786ffe5151f7e15b5743ec","signature":"b75036cd4b2379f417629435a8b784c8a8a1d28e85e7001ba73154ce019e050e"},{"version":"2d03159b60a0ce25e9ba7470524217994f61d41e7ebd9ac452a09ee959077d68","signature":"9c49d7c5c30106706ec1fac0123f1d18aa5c8194922909ed943279907bf14a9d"},{"version":"1d04daa4b3851242a7efdd6f5438e77418e81de8279ca916933602b0bd068f5d","signature":"43d1d89e26c3dc60b5d5cd3b40e06989ee0be4f1407e07c6421aa79bc520939c"},{"version":"7c8302d89b8108bd9678cef74f4d43a1517702190dbc4fd4eefb6dc215f5c4c3","signature":"a4d5ed124d19f2f56e0a2413fff0e4f419519183e67fc673d28eab26b50e54c4"},{"version":"c76d27e4b0c8138ea612659d168d470d485ac388c3a45a3169613ca92dd517d7","signature":"a11cd407295c4a99f8c46412f5c74ebc7fc9a83a484c8342bfe11d04209df163"},{"version":"6e41b72852e6c487867c3426eca3d1d73a3624415250e7e5a718c918c073c3ac","signature":"b83c3c81a7a698eb2ef486528cb737c9b0245872fabb264094d6c6392dcf00dc"},{"version":"2e6b5fc5bf6ee457b396902edd991dbe5589ef18ff1e9befbaf1c728a6485aaf","signature":"51a2ac99aa78f610bb7487253cce9255fbc3fe17bca503ff68694e095bd9242a"},{"version":"40c24d9fdf47b4c7bf1bdd3029622c14699132be435964ce4a602dfdc06325f3","signature":"66c214793eec943afc8f4cd4699c4a5df02b2bf26d8b19ceb39080b4589fcb7d"},{"version":"dae195b83d941eebe0b4c29afae94cbfb4b840ac892d17a667bc6406994fb9a2","signature":"934264faa0190f05e8970a56ac76bbb0915bff9cd57fdbc92f3941be0f7f94fb"},{"version":"0c458f6d4c624b7bcd133d3e88c7f62a885ba114b0bd626c0c3938aa2dd2ebd6","signature":"b4beb523efee58f5d427757ef9de64ca4c0cafc4729563c232000e793ea84cef"},{"version":"eddeafe73bbcaa417ca11f31c3ce818342bf4af33e306b02e4f1e185b3884c93","signature":"bc941abd620d7dd60554d604f8a49db223056982bcdac6ab2eeeb523d36174ed"},{"version":"c1752693c90f6f8a515e11e4416a07b7a7240ea1302afa1de4b88153b786751c","signature":"46b9e43e3d94e674c0b62d25b1901a4d6fe82f26b07bd440206fe693b0700f11"},{"version":"99167e16aa3d4a65f58cba30f442f8088881134bcd83d5bdbb12d53ccca58edf","signature":"78a5b5966bb9405f4b08add09ade7e45e963bcd90b2d36dc418ad77af12b2ad1"},{"version":"83c4f4532bef827a63a6fcfda4b1819bb0deba171c557f4b779db05bb4c91b25","signature":"1a8f56ca54a48c02aedf38962b41e42e7f7cc97b806430abc15d2d0977c939a9"},{"version":"854620d087536658326a493f1c7cabd1943f2152f76e839369d4b9f68e89c262","signature":"edd4b2e6370cfeadc243ca326e2f3f9c9f3b7379af4d90f27e729203a04725db"},{"version":"c9fda8d722750ef70166e3567f059f7769a5a09d63d0d11047754297cb992329","signature":"e13ad2c34c0b7dedc416dd17e9778cd95efb5b7c2a167811a1b4f73dcdf326d6"},{"version":"4f5bf66b9e615381972ce0b5624c269e79f6bd3c1417a8a989ece2078d77f376","signature":"32d3d962cc2c9f6542d3ffa95097c387f1a7c953be1b7ca173fa4b11b5d21366"},{"version":"abca6c463859540e10c19803f9fca52de5ebc7bcd1fbb27ca2032475865a5292","signature":"bef1c591dc32a189513925e5003f85fcb8b52759a08f4bfa88775c24e9c0e52f"},{"version":"26326cc9da100321183a053e16b4553114d64bcfbac2bda4285eaf21e4ebd3c4","signature":"710be0b8e35935d92ab6c812eb9989833b36501e9e8fd77a2db1fdfcd3a99aa3"},{"version":"5765b616f0a24a84553e3689eb1a7dfb73f30cd0b6bcfb9ea7a792f3f6226c25","signature":"a02847feb44d6ad3023c85a7a69612ba9b0fbdd4598a2f132a32d05fd76506ae"},{"version":"813c3ac9a4791be23c4965f079e0896b4550ca2e63087cadec93af2ee6d43ed7","signature":"f5d337314e3514573f2b9a1e7e7f37cc815c4bd3738c763f4fe837faf1f4ed60"},{"version":"73cf86f8f87f74106bf44589d8a0e0ca99d3badde3c3f493a82fcf3b8a96cb67","signature":"a74bf8723c60e5907ecb95e3d7f4fb8f0c1790e8cd0dd32a5aaf301208065c06"},{"version":"466441949f83e44e7bd4254ee2e1e1a2b5af85ad3700e99f604b6120a4aebb14","signature":"2d494cfb904e682cdcf97c2645cfe63196eec4e6fab413cf0ef8bf5033a6f411"},{"version":"a7686d1f59cc651be217cc1420027e31baa82f0ae6b4991f36e9be1c8a7fe3c8","signature":"fdcb0f02c86e8dc1a2461e937ec5f24eaf7ee16ce51559c1d70a31c559747721"},{"version":"cc48bde0fa1c7b0ccfda6f6caa299ac2158de91bc6f6ee66cbaec24b19b4ee91","signature":"9986f78a6ee05fe3964655cde3b03695f361d084118317c8ce729baa934403d0"},{"version":"e0b6a4ac73de30899c21274962911f21a25c3a65ea0bd8a83ade05263c6c25c2","signature":"ad35d6dfe3ac904572c903aadb1fe27da68c63bf40bddf2146592893766faca3"},{"version":"dab26d3810fc078fab5989fd5b2a5fb127eb47442de5a6b5328fd147241c99a3","signature":"fd9e9bb5fd07ae1c471da8ea9c098d412ef6996fcd28c170237eabad21f03f56"},{"version":"c3f2994c68bfc76489593c0e6adea7f133fa092e1f08aa7dc1206ee39116e81d","signature":"75e5ca8ecb4e002ec013dc81b8c73ed0b6ac987c7f7c4e9df6a6cd2310bd4f2b"},{"version":"f4965adc62d05502dd5bb15920869c66fcf99a369807ae0b1484c888f6a79dbe","signature":"c753ae414649909c7c2db58790dcf657020b7226198135783ade6229fe273934"},{"version":"21d8f5f4f8439150dda71c17cf9af555498f13b66702bd3c53e88d29f9885747","signature":"1c93a19347a3c94c8f0b3b0de6ff9f3b6e13b52efa447989f6647946b4a7bbc2"},{"version":"eca42e7d96bbff08bc1f8b71c28549923b2712d2c991982a6fc70b6d3a954f66","signature":"7f2b8bf87329450ce59f6ad184ad01ac0e44d8f9cff451a3437d21a3e24c0432"},{"version":"32a881eb6e28fce7804733674392782c67f9b206669eee319fe728ecfa52be96","signature":"0b95d5838bca4ae0fc61382adad4ec2a203b767c36310a21b0f4544018014dfe"},{"version":"4fef640e44b64f31a1c2a43d6d036f4eca98d158963780d16366781834e014dc","signature":"8f399a318e33ddca38b6e298365c1d94ef84766bfd0be64c46485ecdb10e6eff"},{"version":"83867b0415a8e33d9e6655b639c8e5c04b03820884236126a838dc5f84f4b1f1","signature":"a696a531ca8a420bf6b2109ab85cb7f9e21c35165520b6eca574a7aa55421824"},{"version":"7e53b00c99fadfd2d4fa2b274290fc9249fa2ba485b90fafed62d5a43ddb25b5","signature":"639b10aab0920aaeac04252259a90a1daa30491a00d1d0ff99252cfaf203d926"},{"version":"c05eedff90a89df64ac326eb49782d1e94646c302e57d680ad66044192e83a0c","signature":"359e0afba160b4b4cf34eca6dcaad88af74edfb679f9465c7ed981cdeca423e4"},{"version":"7794b6d37b2248e892ea311d3b193feed5276c91e66d184483a9f3dcc8ea4d5d","signature":"ad90047aecdd8f5a29c27173124413f18029001bda342c978a7a9d197319017d"},{"version":"9b15584bb6e57def1f3c0999a6549a48a3bb631e72fa4ad8cd724ac0432816b6","signature":"01f69376a9226cd91b7f71cea3a80102edcb4df910a1dee4fbf6b222835e6f46"},{"version":"5e3c1d8f644583c697522170302f405e6d723696150f43c6f21efe5c66b245a1","signature":"b05760029b5a44d79f6b8d2556bf43f7d32595eff16d047cab842a835a716575"},{"version":"c289cc2c1d319f6357fa628ad3a2598a78ec8b57c5bc53610522d2203ab5c0de","signature":"f9ad5e7a4736b8fd64b113deb5f4262601c69678c9e818a1fe0a9f3a64b4d608"},{"version":"0f8b8bad6dd336917f9fab60af2eb8a42a1c458d596ee481a7ff67b5facba3a1","signature":"6617d0f61439a9291f5eed3b421546ac18ec8319852d7d39a0e341065c82b253"},{"version":"2402b79db79ee990c214278bc9b4e2dc11cf6c3d588e2a511fe02c1c0a4d78d8","signature":"96c9a0279cff69a0f7550de24bb1b9144cb382a806e86b330e7fc0a4bc36b173"},{"version":"ce2eec1cd315fe3213fabcf42273111c9081e3400c82d2d1ceedca2489ec82f7","signature":"0e7bc94df74e24c969dc5f1def7c246c6aac0e572bb5868c850d7586f5f479be"},{"version":"69dc16ad613f64fdaacbae76778ae04354939bd41957b46c6add7f45d6dff821","signature":"721e567698d06c7146e4631c0405cd34b81f7b582e2d2c2131fc74e97691538e"},{"version":"388a08114a0633b0048452a1b8e6735f1e3c654e2a7850531623c1098f8b6749","signature":"6b308b571131497fb7995259f72574e9e8c0ef7a0c6ddbccdc844c9998660d6e"},{"version":"78ce89281a09343d498fa99b68160634f922f10880d7876664b178801a2effd2","signature":"7112c11636756b0c855d16335745e4eb614a2bce3f6203e5d79e9b8c996d2f91"},{"version":"666fc4e4f195fe2f2839085bfe24f0f36982e25cae1c662fa69cfd2595d7e494","signature":"5262133846a6cfb74863cbc7bbd314f07b4ea79e467185449e804739cb7d256c"},{"version":"f59434e187255537f03917cd69a63c78b1e17d6b630a8d32a4679c741573e74e","signature":"7d9dd4ef2708f56cdd16596a6ed0a3ca954ab48199df1e92c0fcd030aa085202"},{"version":"d031dea1fa43cf0b4481b65568a3f2a9da8164bf4ebefb65729266ea0c2c10a0","signature":"e287e4158a7d3a00950683544f51d353e751f1299022a3562a88fbfa02eabdc3"},{"version":"9b06b1000d65fdf8079e04efdf14763736923150b64392e37329effcf29c5179","signature":"3037ac6f43fd4df29f409d2996b7c5239d0cc7b3051d7a2c7acc434493965d86"},{"version":"d975e39bc7deb2203920cfb1ec682ed59088a8c5fd7a7c62c612b09497c0a6b0","signature":"f1defa902697f9c1e33a9e8454c088715fe68c9b42a51668c0f88958651fc449"},{"version":"b05d4875ac4d48eeca25f33a55a5f5bc2c6f03ffaca6c6b1b36f78a12fbfa3c1","signature":"713c757b802cc7af29331cb4c358e07f1b6a7fd02fd2d435b12ac15b34c7a1f4"},{"version":"b5d415018197f4521735a26fb1c4b97200d0d7e6684c01efa18cac349d9a7b85","signature":"2e9c7577e8b2141301d7549a58c9b65fb33d898f76531abbc6f3394945a5ea3c"},{"version":"386b411232f1e81b3a3e6b687ad84bf5be580fc73f6e50b48002c28b4a202455","signature":"2ddff34d957d352f4993ca2d99eacab9870c3850c989ebfc1658f7caa46c6a3f"},{"version":"afd4ea549642c205223eb4b752e7d3b2365a7532b09c7bd83aa96218d29fa2f8","signature":"b67d62b0cd170a03e7e9a5e84a1110121ae5c1d5d5f8a5cffca2a9fc5641be9c"},{"version":"ac01efdef177f41868748b1308d2af2b4c953bcc5be05a29c92f8f3de3deb1b6","signature":"98d2e012e7ab63864e03f539002fb11ec95eae37920c207fa8c54aab15e535ac"},{"version":"5ee3d2e03be6e91b204047b9eb57876d4c1417b8fa483bdd2e3aa217ac74bbac","signature":"4a5c6c9f8f5d8bfa487cb395d01f8704fa46e597b5424fde099302d0f2b8e9d3"},{"version":"9b69efe39faa7f56978ee3b1bf3d53dd2adecfa4a258f4e63ad9e17101f5a564","signature":"ddffd169cac3718b180ee1a4a2772ffe135e4d1e06db075af683e3d2ca15a0ce"},{"version":"e30e3f7c9f339dfe14b85f6e905124c9c5d793e428e7208865624af884a9fa50","signature":"f75a3a3cf5bf7d7ef7068afe39aac37c787cae2043e90dfd94c967b6d818ab0f"},{"version":"c961cecd5b6d82bb51eb5f7ff2c7beb450ad17b94cd4ff69ee7b4ac0573d7daf","signature":"682006b8c1753c1e6fd5454df86d94fd27d60f8612dfce35b7af784b8d11539b"},{"version":"de16b95e4ca50846524d98985d5df100c20fb228a3f5f47ca864b259de670fbb","signature":"e1920744eb0253b6394040c7263824fd1f77a96befeeba64c2d476902e80cd76"},{"version":"765f39a5b08758e0ff15b8ba43eccde198b2ea82f955bb628c5c89f61879a874","signature":"11ff297e22645ded852b70582c2af82cb5a3832a36dfade226ab2913c03a482f"},{"version":"a38133cd47cf99e84e7c27b41c1b605d4343f87be7ce3f88f24dc50e09cd126f","signature":"820f329e737a14f25dca447a7e4fb368ae9c7b0ede325179200e9d59caabbe53"},{"version":"33e048db9486686623f9e20fcd972c7bc4335615740565a47d3c38edd27be9ee","signature":"39f85d0bb8bf86c0d1b768e99e1c7913f6ef410169e7a2d5e06ebc3b3805e744"},{"version":"86b41c29bd17b17cb90a3006b5f935caf44544970ca99ed641b6faf202fea2e1","signature":"0f6b48ab2c6cc68d5f552b5bf66a73b8e721c467363f7f58ebf0c0656b5a376e"},{"version":"07f4948296d43a091443d82999f515e2ff1fabd34b4af4d1c9ebbed9974bc53c","signature":"82ce077e2253638efed5ebfa0d731f8d1bf26591c73916c1f00a58a3b316a62d"},{"version":"be46080ba56eba3aeebffba944882c83cd298c6146914769981d6d09fcade791","signature":"5273f347acebe1b8f7bc9cd51fc7c0aa48c40bcfaf09192908fbbd1591d45822"},{"version":"752de27faeb21841eb6658235576f4147e805a9e8cf3eca865cc072c0a14b275","signature":"9fd77e9a5ad66ff7e28c11dee037ba892801f5422fa3b668c214511fde2b0072"},{"version":"442a39678c13e630c97bcef29d0e929b114e0eef34410f8cfe337c074273eedf","signature":"5456a03a3e65b2d044707aa39add5717e4ee748d8deb54e155eaf0586c7b7493"},{"version":"c252703d6bac58616b6624d6c74b78f2d1ff7535079f484f5d625c3b3e55dabb","signature":"c90324103e3e28d9a64c9f62be638fa061f64f5b449cfd7dba9a3568aa7a95a7"},{"version":"7cdf38597e76fcae8ea6c15dc968ed945e32525d9c8160b3b4739de4be81429b","signature":"2d66f0aa1b5eab5d1b733ae8281d8ca402b3a245d4fdb0af8bd3e611fb5d3bba"},{"version":"f44a964daae9ba8589ec8f6028bf5dc39fbc8804f8608e77266510e52b8dbaec","signature":"9a6d8d5d597201f597c8f619c4ef8110976c6455effa3ebb731cb17415054e39"},{"version":"c702046b51d77b83eb243fa9a87ac9c7c6f90a291aa0db8aea0b8a15f2a8ef5b","signature":"e488e4b1d1a84ad891f5dbdeab607a49e4620e02dd0d7414076b6d5794687648"},{"version":"9a8f751c5b1bd6e3b33a57127f2ba9e4258a16509af58a3dc0170ad20d833eef","signature":"9f161d98f452039d8624a94566fbf1a2f78f5f47bf2ebd730ea0bfc47d8caac7"},{"version":"75ac57972004a2645098997b5c1064f09a22ecc36865d4a692b8c3d9d890972e","signature":"8bb98bd88a3a0bd5c8f6d64ba30ec86d4dad17c92db1be659c3861b15414d59b"},{"version":"92cc8535098230c6a7457c11b501c5f5d3929014fd8743f59a5cb69384a00615","signature":"2e7e7d479c1c0ad2ebb82051a12df781fd6bec1a7a2cb430b93ef091eb181eb5"},{"version":"0ffe6a5eff9bfcf4ef50e4e28b8e257fa9d4210b2bcf7a48a2acfda818ee5ff4","signature":"6dbd43019e109f5a6adae6449bd22e61611f13d6cb8796048cdab9a55769c56a"},{"version":"464d374396eb3d730a5a3103e681d353aaa3efbc9860fb540465d3f6228d7630","signature":"36d0d03b1fc91419b4bec9486c6b81595699a7326ebe12382afdc61c700d7f0c"},{"version":"680e89bcaa7510c2e85dd5df3e2ac824d27e7ac6403f8d33d5b91dc5e97e8d64","signature":"0d0a1bf4882fd9ba6fcdf9d5358de2c45f7f0dbb5bf5b8da157c4793a2490373"},{"version":"41b33ce18c1fa63ad75a7212d0a0b56983b349750832cdbfdfbec310a86c8ae6","signature":"8f06b738fa0dae9e731ddaf5ff164bdd2c3fde6a38119e0100d0c02b6606e680"},{"version":"2d20394dfe778b0e6d810f304c4df13baef91e2956860d7fb81a93db10d97b18","signature":"0e63101e2d1d1522efd91dcb4e1ccdb63beee7d3caaec1a9203093095734f15e"},{"version":"072d0aa0a079bf983dbccdd8789f45cd8ba70d08d99ee485d2588d110d782325","signature":"50a31c87a0fa058eaf90a46705f48620b75b266346b494415a14d684c7da7395"},{"version":"14ddd77fd4a96cfd30e5ddc55873172bb9bd4078b356009faf8402811dc2572d","signature":"a079ff0c33ef13cc061ad7f6695ff8edfa435be8195f7706b3c3bbc91c77571c"},{"version":"6bbfcac2a5586666ac04d43df9c51e533411b6a5b113318de6fad6dd6b06f0b2","signature":"0775ae94319dcfc523a98bbcf519d14c27ee040a86c90acead1967052c9e8188"},{"version":"ff7030e7b2e108c5453f8a17ab04d75ac102e8241055f3c0fb4cf2f29d1e6703","signature":"f08771637b9894592f30d98068207367708fb74e7c88b3dbf90665c488120603"},{"version":"8e28842fdd19730994f81b983bdfc33d74dee9a85c195503e43618427df94bd3","signature":"717610f93b0ecc2f68261ee95a96b27b813487a0b0357cb928afd48b84883060"},{"version":"a3be6aff738f2ddb337a46dff8f86878382fe36723ce20a17a00ad0f35738a9b","signature":"325ff002aea559ff0c19a237548173e7a9a8eb3b5d72624610697ebbc3bb3e27"},{"version":"e98e56954850971bcc102a176df7eb10e211017fcebad938fe296cf3903b93d7","signature":"3e226ae598aa2c71d90333cd1e6b5c4f849a3fd2fdbfc8cbadceb028440affbe"},{"version":"df590b28093a13a2d4b19b5d84b63a99fc0b567979751eedcf656aef841eb849","signature":"efdfba9f01c46aeddcdaaeae75aaa68449f49c5229a661b90f77ec3870ac8cfa"},{"version":"b6ed09ae6c977e036985f3d51fedde538ac7e38f6d264fa299117a9b2ed4db09","signature":"382886c12f25e3b79521b34f2b9822d59bc971d8d2de1b6d169bb5fdcc4af529"},{"version":"d877905842f7e79d11aad26b49069cdd62eeda97c1ad95ef886fa095e7ed1f0d","signature":"f5c7b7280638c8d7190e7ad278982241f9f47bebdb0702917c2f8af85fc45f87"},{"version":"a92f000347755b04ea1dce635066f4eefde06b4c9e678aea0147a2544428844c","signature":"35602e4f9c44b823602b6980c36129c9a903b386945f9a83f810ca3df64d82d8"},{"version":"f6a34bc18da75bfe8866ea3f230f6f2a20c5ec5421be026074d0b2e6b6019b1f","signature":"25419c21e707946cce26b908165c9f0ec14ec82272f5bcb79a25e7b9123c8ea8"},{"version":"e5d7414b7a8a1032539bde73343d576dadfb95f1fd07aaa845c26897133d3508","signature":"fd55365a57c9e9e30674ca7fc63be408779f20c245487e5f507e25964098c99f"},{"version":"f390033c14e37c3e37cf57b0bc0496f4d6e8060a1c6e5f600c4eb4b6cff9597a","signature":"7b1ec174ad64cb1725f1ca0aed2ac4069baab8f4590914810d0e02e107895e8d"},{"version":"d6c9f998812a14a678407d5d205d537ddd2c35d45163914ca94851e06be20633","signature":"68527786790ae231e0cf15e5e50794a6359b5584d45a9180819fd9c9bb329232"},{"version":"9e764382af650282da112e54de7165ad56c69231f503471950050a2c11eab7d6","signature":"00b6b04c9e25c792e9071db683864bcef4dfcccb444f2884b70a823362640a90"},{"version":"733687cd08b716b09991fca7fba72433954057f8797d06588db588a2abf8e683","signature":"bb2531f43c70fc6b4e6d0a76286f8b54121a56ee2e2aeee9f4c0acc0fd914233"},{"version":"07bcfcd73a10121f4840f931823b38bd2fb222d51be3db2062c6ecfc1bba6eae","signature":"84099ad6270bc810ac365406ef17c6b178fdd03cb446b317c85a3e204d078de4"},{"version":"13c893f5a10ec0af106a7476a3365c36293960458c4d8c50899781ece83bfd71","signature":"f0a1c856b099c0fb1ca60a920e12f321b47c1b12a175b9a9fd39276f25e1ff58"},{"version":"95a3fb507b571610f875b4d75f4c7f6c67ba2a452b95f68bc153dc85d55e64cd","signature":"0615aca4b224f024df7602a3b0042237c979679cee159e6085b05cf8fe1cd891"},{"version":"8f87507021f2c8162fce74375f40f02a982babd3f2151c0827df2a75b03371bd","signature":"e31488663836629a8b6b87d12cc82fd3b28034d288cfcc0b3cb2365b762b7b78"},{"version":"73a0cc23944a7bce6856757d68b3d079edd7c2de0812c9504c2819f30accae5a","signature":"3f4cd7c373aeae3f6b010eb1ba3aed5d06728c22768d20dc9fc7dab2d7e458d2"},{"version":"ace1f0a770cb5872187fbf5ec739483cb30f92e2f9bc09c8b67c83351b11c017","signature":"93b58dcd300dd8f08dec4a956ba56b414e319310b59ca186422e5739f88bce16"},{"version":"d2146769683d32edc9888e69b24291505dba96a81fb17929e01f6bc26d69e87a","signature":"f7a834ddbcd50a45c3176ee9704cd7500c39b202fd37e32ea039738b17d2285c"},{"version":"4c03040570d648054a5180e827eea3f334b38235087480c022e0602f908c4c66","signature":"4d7cf21c5c36c3b6c22f45da69f31fe576b5964e068bbd7f3f01d1a9aaeedb1c"},{"version":"a95c01bb1ff97361ca1ca7f18ceedce6cadf82420dc8d3d49115aea0705d3f98","signature":"42cca77d4d88ff6f81913476791ecf776e607d00899c1a531b2f7408504913f2"},{"version":"0fd03e91ce37a1610dac81118b33eae77a46f3df74c5882b0ee18782e9770d64","signature":"dd27d147a99658eeec0762624f80967eccb59ceb09be593e150352967fff6b66"},{"version":"7e746959c17a31f42c72dcf5eacd45db438a6117fd6dd6aad54539d725c73a00","signature":"c29e1fe71d87b11a12857431d2ab9da6cf61ba18b804b82739b7ab4535d50dbe"},{"version":"9ddf74e5ed103accc0ce8a8038385bd3701f3c29d248e22e270da27d9e6133a5","signature":"952fd6fae615028b530123e4e17238f480397fb08c27e1ed20a2211d71dc6007"},{"version":"092a4fa604d9d45e9653a1151af6d16eb8da30ecbe47cff1c0c7bdee1fa46da2","signature":"3ba43b1d12972096d780293c1205f8480ed1cb39749c75de21388aa60fe47b2c"},{"version":"ecf9a3c748e6bd44216fc68170f6482e9544048639ed6d489393109293530cc8","signature":"67d749a631dddd2a3d4c97c35034970addb65cbbd807e81a3ed93d0c64ed29a7"},{"version":"08667b17363f80e9bec2b5edbe690731d02f41652a79049fcc109d59dd216170","signature":"417d6bf795c5d8930497030e76ec63642d7cc840da788510630ca724034dad72"},{"version":"16f2b46c705bf4ea8452edc4297b68d62a818600d843fd8081a05eeb7c615a8a","signature":"f192c60be78e238732a3aa09759991834633fbde6e80c1b66180dee828d4c9f9"},{"version":"009afbeaeff2b93a22dac88a665f5dd2fd18ebf3e72a06ca43d6c4a1384c0fda","signature":"5bd7046df9bda573bd5d05da99e337ef2a13cd6ef4727845741d0509959bf42c"},{"version":"7a8bfefd403987cc39d460e158e5bafa35e42a5c50c97052a44abf7af5366e5b","signature":"473a62d4306739eda703ed1ac319cb20968bc48d9ab2d02eef59329a9742e245"},{"version":"b52d4fdfeec42f6e1ecbeccd45deec1dccad8677ff66c5add27cada21b120f88","signature":"e39b7f56257b23109adf6eec10b216521c475929d637307f1c19d27fc6167338"},{"version":"c45c4e8ebca4c7e633a600c063d305f7c7b515008d284d01f61ed0615e8a1cf0","signature":"206d565048d8b3c6c7b0b659388f2603fc58de6ec28047c18382a591d59118d0"},{"version":"77d0ec594521372161f4bd7264803fae0be3c2a22145dc96cf435b3f4be6d620","signature":"cb52c1893f37489268bf1a50cb4dc2bc2eb0c2de12e3e0fd49cd5ac7280d3ca6"},{"version":"55ab3cf9d7fbd6e8c910b2262dfe77134c3d1ef1c24ed98c0bbed495ea283acb","signature":"b64ef296a6ab1187ec9dee4cdc4c40bd9459ca4412593fad52a184f4740870f6"},{"version":"d1dbc6515ca6e2ea85468c6f6711c17050265e7bae94744306041ec9b7aacc56","signature":"0ec2aadde09b3cefa474330235c0660ad5710cf832e42b938b55e9479c30e897"},{"version":"a24cff4e66408d6bb2e50363907a5248bf7ee67e52c58e803337a9b3560a410d","signature":"9e070ab8efd5525f966ad4dbca20d23ebb3ef32b82cd739425377a5bec5269fd"},{"version":"d5cf10ef3c71d81627c62cbdf8aad53cf39ef5a517341a67e20943123245bb43","signature":"4b89fed7b0f222bf4d142bd7c427214acd73ab5c379eb84581f98fa55987b385"},{"version":"fa99c463dfce1d33d5e0d2cae60a1120ff829abcffc5d84c21ae72f74712e42a","signature":"c9b9bfeeb5e58c8abc50beecffdcdb7a7922b39e8bd87d5a061742e453d18532"},{"version":"d459da40fa48467d4c5868b9ac8c67400a4e0c64095a2723ba2912e27d57420f","signature":"0ebc0b98c4a597eeec9d827f23de5b19a26d7c84ea87548624122579f6cf1c47"},{"version":"cb00947a4c6a6c0cf6f527990ff6f41ae8a3aca174a3fb5ef0aa1f41dcf120f4","signature":"7adf811d88b945bea2c165dd5f055f46c7527751888d75665d091c30ed695681"},{"version":"7c3468e92903bad5a6d24a332f82eeebb58f8a2bc86e61034a4f2aeccd62dd31","signature":"356fcb7318e4396e39f976d74c71250972de308442790851c6fc7d545515a348"},{"version":"c9e1ba040566ec15c16f6162f13d782678812dab014bdfa9b1966e45e47ac8c7","signature":"84feddc140f4eda7a499a6b13e4c49305e64f8e45338216e962d9810e55dbe05"},{"version":"f1b9c13b36daae6a58135aee3463e78bf3db33c7a6c222984a3334931585be9d","signature":"4428d9f02513d3b7722178ee8229cdb668353b8272864aca39046308d1e27f43"},{"version":"30d4ccf51c85873d4ad39e0d17fc0b8bb19f16356f19ba4b00aa93f2d0f6d4c2","signature":"0404680361d5785a1251b412f92ca0350f3fda89850fbdb2198370f69fe0d67e"},{"version":"ed9292185810774b2812fd32859afb7fa9fd7c6eca74be42cfa59d153da82459","signature":"e768845b2bb0f808722ff4abc78409a8f41aaceba7d4009ae218ea308addafff"},{"version":"7fafda40ea450f6fb029f9512c4eecf5cf07105e2d0cbd62064f1d462723a9f8","signature":"61787a6c4fccab8bb87463f89f979bfe169a20c22cb8d1b7c183b01cdc494d0a"},{"version":"856c2dc8f49d6db5f4821fc78a55fae58cfc874d04984262169a5a54d289b3a2","signature":"a8b188e3cbf9f386996b5e932ca4303a708f475b268ac2895a4a28a72e8c0972"},{"version":"9318bf9ad613bde387c2f4927c35fb2f9979c062bc1ab910a71c5dfb630f41b7","signature":"138cf2eb0363e1485da84bd5090eb0df0397572fe7a40449243ca0bf4c85cb34"},{"version":"5a3ec101cf2a0d0705d1650820c769d3d3247fe4d54a5a0964e6aa49fde4f9f4","signature":"31eeb7cbaaeb3136057785264ad7bc1ccd3666a962cfcac60d33e81d0274df44"},{"version":"449ecb3de0100a7906bdb8eb70272b17cdd606b15bbcb87e568f92068079e81e","signature":"b50ed306e9e4094110121b3fc653b01aa9f2b3b846813d76e3592976e075505f"},{"version":"fb4a51af9996918ad6a02a0272ebccb243bd18229ff0dd2865da3115251661cf","signature":"08a773556c6e31be739c73400aa908b8e4dc72bfa0cf2ebbf52485fff11d5619"},{"version":"7a1a0587da8a65b93b9ad579e2955c08d7ebd08413f61c9e8622ab2717aee1d5","signature":"409a71b09ec3e97d170cf3c6eacbe3b79731e0c0fb3e97e37ef33e5653e1d67e"},{"version":"b92c67e149140d3b5bc3637cb1f2a3b98990d21f16284ef103da00af88d91c05","signature":"20c8bd3a2917306f5266d4b9218922f68b94da6b1310c666b92ccc21ee21d807"},{"version":"b6be846a43560355d8d65720317bbe5b55d9fa4c9dd66379772a32a3f8748a64","signature":"0ca14f6ca40c5295f4541c999ab52dde2e514140e9f73e1be7fe231b557a7e53"},{"version":"ff9f2b01cd9e0adc3f03d2bf85f99e42c967fbfa3c13ce8a8618e7bd57e9b6d5","signature":"9aad5f476ad86f0a594f86290501573b2f588e256f8c38e5d24c174d716d47c2"},{"version":"096b311734ceaad25e2d5ed708c385017c61d794e55a23ade7d0502b48b7cb45","signature":"8986dae0c5316b3c99dc16bcd3d414057bf6cd3b395387f1d3cd8ed3e5ff9904"},{"version":"5614b140a360e5df88bef2a88445346261e4793e4757f51ed23ed5687ba38352","signature":"6312d71ed3239cdedc216e324a5f021883bc550c471e7b55f9db8c22aad63ff0"},{"version":"431ed9a9d14fb820c1bd19e968fbd0473e8eaf964f8b0168528080aaa5093a0f","signature":"be81fcf84654bd58aa776d2838d4d12018c70eb33d371d27e3798f51729b8b80"},{"version":"c9665be4bc1fee3d7cf381d9d8076b316b640420d99754b7e10b17ae7e7bb624","signature":"bc388ebbe4f5cff420b9b33000cc06f80ce92638e08f9ed1f27788c04bd8765e"},{"version":"ef448584e4e6579fcbafd7d56576907c47ed10e686e30ec4e5b5affa2c2bb62e","signature":"07fda70730090df550194f1b746bf7662ab6bc822d5b83ee39ffceb79f79143c"},{"version":"00c6f2ec25a3c2784f23c0fe6729d7eb687905b72d99e26e141817c591c5f059","signature":"abcaeeef722134fd8644f3aa58bd7bfbf3c4e53c86089d061554224b497ba7fc"},{"version":"36290629d28f73205ccd755785698518219bc6960f72409163d0cb4ddf43af46","signature":"e3e748f6cdced3d84241ea085cd2a33fed3786ad4b505de9eb7293678a5c5c15"},{"version":"6ddf069ecfa76b693949deedb93b328e276c51d2421a0a6dbb1e57b652902cd8","signature":"acb10d756ba7237876fc2c2deb1e531f35b3bac783a056a4145a190e49669257"},{"version":"0710ca00a482df2161d9b2aa3dd70a349d9c53a1542181cb41151754124a37b9","signature":"119816941f291d63803d89b44964567c30cee52a7cd7b9d3a58f4d08e8630d44"},{"version":"4999b27ec584c208f97f9c031f2aa7a35884eedf904dae7f52531ce420cc509d","signature":"6c7da223318941e4720a4d1ede7310787ac5c147a8c1ec6afb3e2ebca86fbf6a"},{"version":"4dcf0abaad211bf1ab8a9dc4549c33d8eab0080488a2c7cbaac045fa815234b5","signature":"2f92081d99d84204dc5c9f5294dae6c35f619732c649d222d358fd2897726177"},{"version":"343de9b06a28ab2cba9b52da7f8d011ee67597460c180c47f822b5fbdcc14e6e","signature":"17d4dddcdc8af645977e7b36f8fccf8e3970587cb75e0976736d902653cbdaeb"},{"version":"141c5a3c5d9ef8e796b4b667e5c740f8eac6e82be3ec588b2a8a21f6a50803cd","signature":"36ef8c9aaee7490e6b794f67fdf0f019e87aea73fa835a8d21b77b2d32eb21b0"},{"version":"3a603f297b4ee0ba65f83a1b8f6d0a6040ae5e8584afce7c5efff8c721b00630","signature":"934f7fd1bd96f7e49c82e1242153d543d4891f8b60f21e777a2ff749f6145033"},{"version":"1e0a8ed975b24497bcacea1a7ce3a6c77c09cca6564af83e6bc87359bd1fdc8e","signature":"119f476d9200e478cbe0d4718f3365adc1a08d39373aaca9a6fa746e88650ad1"},{"version":"49b83e76a776f7a266d60eeebda510356cb1fc77720db7512fc77c4122302cd9","signature":"3cafff51d2eb884a5fb175836c6559300d480c829cf7917d66adf3019ff2971f"},{"version":"3ba6477962523e9839c713f5999f336b3f46091a0f0be3996c7f869f0651e012","signature":"f2d2ac8bb32d70d376df55d5170e5e78e5b18b022b10086e06e87f8e276a324b"},{"version":"7b6d554f6d34c5d691ef5933affdd9cfb1af120f0546b0ba5309665b50ad68d6","signature":"fec1019418e2f5ef8d5dd3efd90911d3472e29c16d529f6591e9c61f0e3394f3"},{"version":"e2c48da2a267db97620cb1dc37361eb6108d13b191bc9845edb70e72457cc268","signature":"92596f93df0a40c067f669af97c0ae4a712e66ec0798e6345b3eb0ee7fe1a4c9"},"cf0e707b9af46eef58ac20a75be391f546c66c814b75871f1323bfef68804f71","09df3b4f1c937f02e7fee2836d4c4d7a63e66db70fd4d4e97126f4542cc21d9d","7394959e5a741b185456e1ef5d64599c36c60a323207450991e7a42e08911419","4967529644e391115ca5592184d4b63980569adf60ee685f968fd59ab1557188","5929864ce17fba74232584d90cb721a89b7ad277220627cc97054ba15a98ea8f","7180c03fd3cb6e22f911ce9ba0f8a7008b1a6ddbe88ccf16a9c8140ef9ac1686","25c8056edf4314820382a5fdb4bb7816999acdcb929c8f75e3f39473b87e85bc","54cb85a47d760da1c13c00add10d26b5118280d44d58e6908d8e89abbd9d7725","3e4825171442666d31c845aeb47fcd34b62e14041bb353ae2b874285d78482aa","c6fd2c5a395f2432786c9cb8deb870b9b0e8ff7e22c029954fabdd692bff6195","a967bfe3ad4e62243eb604bf956101e4c740f5921277c60debaf325c1320bf88","e9775e97ac4877aebf963a0289c81abe76d1ec9a2a7778dbe637e5151f25c5f3","471e1da5a78350bc55ef8cef24eb3aca6174143c281b8b214ca2beda51f5e04a","cadc8aced301244057c4e7e73fbcae534b0f5b12a37b150d80e5a45aa4bebcbd","385aab901643aa54e1c36f5ef3107913b10d1b5bb8cbcd933d4263b80a0d7f20","9670d44354bab9d9982eca21945686b5c24a3f893db73c0dae0fd74217a4c219","db3435f3525cd785bf21ec6769bf8da7e8a776be1a99e2e7efb5f244a2ef5fee","c3b170c45fc031db31f782e612adf7314b167e60439d304b49e704010e7bafe5","40383ebef22b943d503c6ce2cb2e060282936b952a01bea5f9f493d5fb487cc7","4893a895ea92c85345017a04ed427cbd6a1710453338df26881a6019432febdd","3a84b7cb891141824bd00ef8a50b6a44596aded4075da937f180c90e362fe5f6","13f6f39e12b1518c6650bbb220c8985999020fe0f21d818e28f512b7771d00f9","9b5369969f6e7175740bf51223112ff209f94ba43ecd3bb09eefff9fd675624a","4fe9e626e7164748e8769bbf74b538e09607f07ed17c2f20af8d680ee49fc1da","24515859bc0b836719105bb6cc3d68255042a9f02a6022b3187948b204946bd2","33203609eba548914dc83ddf6cadbc0bcb6e8ef89f6d648ca0908ae887f9fcc5","0db18c6e78ea846316c012478888f33c11ffadab9efd1cc8bcc12daded7a60b6","89167d696a849fce5ca508032aabfe901c0868f833a8625d5a9c6e861ef935d2","e53a3c2a9f624d90f24bf4588aacd223e7bec1b9d0d479b68d2f4a9e6011147f","339dc5265ee5ed92e536a93a04c4ebbc2128f45eeec6ed29f379e0085283542c","9f0a92164925aa37d4a5d9dd3e0134cff8177208dba55fd2310cd74beea40ee2","8bfdb79bf1a9d435ec48d9372dc93291161f152c0865b81fc0b2694aedb4578d","2e85db9e6fd73cfa3d7f28e0ab6b55417ea18931423bd47b409a96e4a169e8e6","c46e079fe54c76f95c67fb89081b3e399da2c7d109e7dca8e4b58d83e332e605","d32275be3546f252e3ad33976caf8c5e842c09cb87d468cb40d5f4cf092d1acc","4a0c3504813a3289f7fb1115db13967c8e004aa8e4f8a9021b95285502221bd1",{"version":"4d719cfab49ae4045d15cb6bed0f38ad3d7d6eb7f277d2603502a0f862ca3182","affectsGlobalScope":true},"cce1f5f86974c1e916ec4a8cab6eec9aa8e31e8148845bf07fbaa8e1d97b1a2c",{"version":"5a856afb15f9dc9983faa391dde989826995a33983c1cccb173e9606688e9709","affectsGlobalScope":true},"546ab07e19116d935ad982e76a223275b53bff7771dab94f433b7ab04652936e","7b43160a49cf2c6082da0465876c4a0b164e160b81187caeb0a6ca7a281e85ba",{"version":"aefb5a4a209f756b580eb53ea771cca8aad411603926f307a5e5b8ec6b16dcf6","affectsGlobalScope":true},"a40826e8476694e90da94aa008283a7de50d1dafd37beada623863f1901cb7fb","f5a8b7ec4b798c88679194a8ebc25dcb6f5368e6e5811fcda9fe12b0d445b8db","b86e1a45b29437f3a99bad4147cb9fe2357617e8008c0484568e5bb5138d6e13","b5b719a47968cd61a6f83f437236bb6fe22a39223b6620da81ef89f5d7a78fb7","42c431e7965b641106b5e25ab3283aa4865ca7bb9909610a2abfa6226e4348be","0b7e732af0a9599be28c091d6bd1cb22c856ec0d415d4749c087c3881ca07a56","b7fe70be794e13d1b7940e318b8770cd1fb3eced7707805318a2e3aaac2c3e9e",{"version":"2c71199d1fc83bf17636ad5bf63a945633406b7b94887612bba4ef027c662b3e","affectsGlobalScope":true},{"version":"8d6138a264ddc6f94f16e99d4e117a2d6eb31b217891cf091b6437a2f114d561","affectsGlobalScope":true},"3b4c85eea12187de9929a76792b98406e8778ce575caca8c574f06da82622c54","f788131a39c81e0c9b9e463645dd7132b5bc1beb609b0e31e5c1ceaea378b4df","0c236069ce7bded4f6774946e928e4b3601894d294054af47a553f7abcafe2c1","21894466693f64957b9bd4c80fa3ec7fdfd4efa9d1861e070aca23f10220c9b2","396a8939b5e177542bdf9b5262b4eee85d29851b2d57681fa9d7eae30e225830","21773f5ac69ddf5a05636ba1f50b5239f4f2d27e4420db147fc2f76a5ae598ac",{"version":"6ec93c745c5e3e25e278fa35451bf18ef857f733de7e57c15e7920ac463baa2a","affectsGlobalScope":true},"a5fe4cc622c3bf8e09ababde5f4096ceac53163eefcd95e9cd53f062ff9bb67a","30c2ec6abf6aaa60eb4f32fb1235531506b7961c6d1bdc7430711aec8fd85295","0f05c06ff6196958d76b865ae17245b52d8fe01773626ac3c43214a2458ea7b7",{"version":"308b84e1943ef30015469770e931eb21b795348893b2a6562ca54ea8f0b3c41c","affectsGlobalScope":true},{"version":"d48009cbe8a30a504031cc82e1286f78fed33b7a42abf7602c23b5547b382563","affectsGlobalScope":true},"7aaeb5e62f90e1b2be0fc4844df78cdb1be15c22b427bc6c39d57308785b8f10","3ba30205a029ebc0c91d7b1ab4da73f6277d730ca1fc6692d5a9144c6772c76b","d8dba11dc34d50cb4202de5effa9a1b296d7a2f4a029eec871f894bddfb6430d","8b71dd18e7e63b6f991b511a201fad7c3bf8d1e0dd98acb5e3d844f335a73634","01d8e1419c84affad359cc240b2b551fb9812b450b4d3d456b64cda8102d4f60","458b216959c231df388a5de9dcbcafd4b4ca563bc3784d706d0455467d7d4942","269929a24b2816343a178008ac9ae9248304d92a8ba8e233055e0ed6dbe6ef71","93452d394fdd1dc551ec62f5042366f011a00d342d36d50793b3529bfc9bd633","f8c87b19eae111f8720b0345ab301af8d81add39621b63614dfc2d15fd6f140a","831c22d257717bf2cbb03afe9c4bcffc5ccb8a2074344d4238bf16d3a857bb12",{"version":"24ba151e213906027e2b1f5223d33575a3612b0234a0e2b56119520bbe0e594b","affectsGlobalScope":true},{"version":"cbf046714f3a3ba2544957e1973ac94aa819fa8aa668846fa8de47eb1c41b0b2","affectsGlobalScope":true},"aa34c3aa493d1c699601027c441b9664547c3024f9dbab1639df7701d63d18fa","eae74e3d50820f37c72c0679fed959cd1e63c98f6a146a55b8c4361582fa6a52","7c651f8dce91a927ab62925e73f190763574c46098f2b11fb8ddc1b147a6709a","7440ab60f4cb031812940cc38166b8bb6fbf2540cfe599f87c41c08011f0c1df",{"version":"aed89e3c18f4c659ee8153a76560dffda23e2d801e1e60d7a67abd84bc555f8d","affectsGlobalScope":true},{"version":"0ed13c80faeb2b7160bffb4926ff299c468e67a37a645b3ae0917ba0db633c1b","affectsGlobalScope":true},"e393915d3dc385e69c0e2390739c87b2d296a610662eb0b1cb85224e55992250","2f940651c2f30e6b29f8743fae3f40b7b1c03615184f837132b56ea75edad08b","5749c327c3f789f658072f8340786966c8b05ea124a56c1d8d60e04649495a4d",{"version":"c9d62b2a51b2ff166314d8be84f6881a7fcbccd37612442cf1c70d27d5352f50","affectsGlobalScope":true},"e7dbf5716d76846c7522e910896c5747b6df1abd538fee8f5291bdc843461795",{"version":"ab9b9a36e5284fd8d3bf2f7d5fcbc60052f25f27e4d20954782099282c60d23e","affectsGlobalScope":true},"b510d0a18e3db42ac9765d26711083ec1e8b4e21caaca6dc4d25ae6e8623f447","211440ce81e87b3491cdf07155881344b0a61566df6e749acff0be7e8b9d1a07","5d9a0b6e6be8dbb259f64037bce02f34692e8c1519f5cd5d467d7fa4490dced4","880da0e0f3ebca42f9bd1bc2d3e5e7df33f2619d85f18ee0ed4bd16d1800bc32","785b9d575b49124ce01b46f5b9402157c7611e6532effa562ac6aebec0074dfc","f3d8c757e148ad968f0d98697987db363070abada5f503da3c06aefd9d4248c1","a4a39b5714adfcadd3bbea6698ca2e942606d833bde62ad5fb6ec55f5e438ff8","bbc1d029093135d7d9bfa4b38cbf8761db505026cc458b5e9c8b74f4000e5e75","1f68ab0e055994eb337b67aa87d2a15e0200951e9664959b3866ee6f6b11a0fe"],"root":[[74,238]],"options":{"composite":true,"esModuleInterop":true,"jsx":2,"module":99,"outDir":"./dist","rootDir":"./src","target":99},"fileIdsList":[[318,329,332],[318,329,330,331],[318,332],[318],[289,318,325,326,327],[289,317,318,325],[239,318],[275,318],[276,281,309,318],[277,288,289,296,306,317,318],[277,278,288,296,318],[279,318],[280,281,289,297,318],[281,306,314,318],[282,284,288,296,318],[283,318],[284,285,318],[288,318],[286,288,318],[275,288,318],[288,289,290,306,317,318],[288,289,290,303,306,309,318],[273,318,322],[284,288,291,296,306,317,318],[288,289,291,292,296,306,314,317,318],[291,293,306,314,317,318],[239,240,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324],[288,294,318],[295,317,318,322],[284,288,296,306,318],[297,318],[298,318],[275,299,318],[300,316,318,322],[301,318],[302,318],[288,303,304,318],[303,305,318,320],[276,288,306,307,308,309,318],[276,306,308,318],[306,307,318],[309,318],[310,318],[275,306,318],[288,312,313,318],[312,313,318],[281,296,306,314,318],[315,318],[296,316,318],[276,291,302,317,318],[281,318],[306,318,319],[295,318,320],[318,321],[276,281,288,290,299,306,317,318,320,322],[306,318,323],[70,71,72,318],[250,254,317,318],[250,306,317,318],[245,318],[247,250,314,317,318],[296,314,318],[318,325],[245,318,325],[247,250,296,317,318],[242,243,246,249,276,288,306,317,318],[242,248,318],[246,250,276,309,317,318,325],[276,318,325],[266,276,318,325],[244,245,318,325],[250,318],[244,245,246,247,248,249,250,251,252,254,255,256,257,258,259,260,261,262,263,264,265,267,268,269,270,271,272,318],[250,257,258,318],[248,250,258,259,318],[249,318],[242,245,250,318],[250,254,258,259,318],[254,318],[248,250,253,317,318],[242,247,248,250,254,257,318],[276,306,318],[245,250,266,276,318,322,325],[73,318],[74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,318],[318,332,334],[318,330,331,334],[335]],"referencedMap":[[333,1],[332,2],[331,3],[329,4],[328,5],[330,4],[326,6],[327,4],[239,7],[240,7],[275,8],[276,9],[277,10],[278,11],[279,12],[280,13],[281,14],[282,15],[283,16],[284,17],[285,17],[287,18],[286,19],[288,20],[289,21],[290,22],[274,23],[324,4],[291,24],[292,25],[293,26],[325,27],[294,28],[295,29],[296,30],[297,31],[298,32],[299,33],[300,34],[301,35],[302,36],[303,37],[304,37],[305,38],[306,39],[308,40],[307,41],[309,42],[310,43],[311,44],[312,45],[313,46],[314,47],[315,48],[316,49],[317,50],[318,51],[319,52],[320,53],[321,54],[322,55],[323,56],[72,4],[70,4],[73,57],[241,4],[71,4],[68,4],[69,4],[12,4],[13,4],[15,4],[14,4],[2,4],[16,4],[17,4],[18,4],[19,4],[20,4],[21,4],[22,4],[23,4],[3,4],[24,4],[4,4],[25,4],[29,4],[26,4],[27,4],[28,4],[30,4],[31,4],[32,4],[5,4],[33,4],[34,4],[35,4],[36,4],[6,4],[40,4],[37,4],[38,4],[39,4],[41,4],[7,4],[42,4],[47,4],[48,4],[43,4],[44,4],[45,4],[46,4],[8,4],[52,4],[49,4],[50,4],[51,4],[53,4],[9,4],[54,4],[55,4],[56,4],[59,4],[57,4],[58,4],[60,4],[61,4],[10,4],[1,4],[62,4],[11,4],[66,4],[64,4],[63,4],[67,4],[65,4],[257,58],[264,59],[256,58],[271,60],[248,61],[247,62],[270,63],[265,64],[268,65],[250,66],[249,67],[245,68],[244,69],[267,70],[246,71],[251,72],[252,4],[255,72],[242,4],[273,73],[272,72],[259,74],[260,75],[262,76],[258,77],[261,78],[266,63],[253,79],[254,80],[263,81],[243,82],[269,83],[74,84],[75,84],[76,84],[77,84],[78,84],[79,84],[80,84],[81,84],[82,84],[83,84],[84,84],[85,84],[86,84],[87,84],[88,84],[89,84],[90,84],[91,84],[92,84],[93,84],[94,84],[95,84],[96,84],[97,84],[98,84],[99,84],[100,84],[101,84],[102,84],[103,84],[104,84],[105,84],[106,84],[107,84],[108,84],[109,84],[110,84],[111,84],[112,84],[113,84],[114,84],[115,84],[116,84],[117,84],[118,84],[119,84],[120,84],[121,84],[122,84],[123,84],[124,84],[125,84],[126,84],[127,84],[128,84],[129,84],[130,84],[131,84],[132,84],[133,84],[134,84],[135,84],[136,84],[137,84],[138,84],[139,84],[140,84],[141,84],[142,84],[143,84],[144,84],[145,84],[146,84],[147,84],[148,84],[149,84],[150,84],[151,84],[152,84],[153,84],[154,84],[155,84],[156,84],[157,84],[158,84],[159,84],[160,84],[161,84],[162,84],[163,84],[164,84],[165,84],[166,84],[167,84],[168,84],[169,84],[170,84],[171,84],[172,84],[173,84],[174,84],[175,84],[176,84],[177,84],[178,84],[179,84],[180,84],[181,84],[182,84],[183,84],[184,84],[185,84],[186,84],[187,84],[188,84],[189,84],[190,84],[191,84],[192,84],[193,84],[194,84],[195,84],[196,84],[197,84],[198,84],[199,84],[200,84],[201,84],[202,84],[203,84],[204,84],[205,84],[206,84],[207,84],[208,84],[209,84],[210,84],[211,84],[212,84],[213,84],[214,84],[215,84],[216,84],[217,84],[218,84],[219,84],[220,84],[221,84],[222,84],[223,84],[224,84],[225,84],[226,84],[227,84],[228,84],[229,84],[230,84],[231,84],[232,84],[233,84],[234,84],[235,84],[236,84],[237,84],[238,85]],"exportedModulesMap":[[333,86],[332,87],[331,3],[329,4],[328,5],[330,4],[326,6],[327,4],[239,7],[240,7],[275,8],[276,9],[277,10],[278,11],[279,12],[280,13],[281,14],[282,15],[283,16],[284,17],[285,17],[287,18],[286,19],[288,20],[289,21],[290,22],[274,23],[324,4],[291,24],[292,25],[293,26],[325,27],[294,28],[295,29],[296,30],[297,31],[298,32],[299,33],[300,34],[301,35],[302,36],[303,37],[304,37],[305,38],[306,39],[308,40],[307,41],[309,42],[310,43],[311,44],[312,45],[313,46],[314,47],[315,48],[316,49],[317,50],[318,51],[319,52],[320,53],[321,54],[322,55],[323,56],[72,4],[70,4],[73,57],[241,4],[71,4],[68,4],[69,4],[12,4],[13,4],[15,4],[14,4],[2,4],[16,4],[17,4],[18,4],[19,4],[20,4],[21,4],[22,4],[23,4],[3,4],[24,4],[4,4],[25,4],[29,4],[26,4],[27,4],[28,4],[30,4],[31,4],[32,4],[5,4],[33,4],[34,4],[35,4],[36,4],[6,4],[40,4],[37,4],[38,4],[39,4],[41,4],[7,4],[42,4],[47,4],[48,4],[43,4],[44,4],[45,4],[46,4],[8,4],[52,4],[49,4],[50,4],[51,4],[53,4],[9,4],[54,4],[55,4],[56,4],[59,4],[57,4],[58,4],[60,4],[61,4],[10,4],[1,4],[62,4],[11,4],[66,4],[64,4],[63,4],[67,4],[65,4],[257,58],[264,59],[256,58],[271,60],[248,61],[247,62],[270,63],[265,64],[268,65],[250,66],[249,67],[245,68],[244,69],[267,70],[246,71],[251,72],[252,4],[255,72],[242,4],[273,73],[272,72],[259,74],[260,75],[262,76],[258,77],[261,78],[266,63],[253,79],[254,80],[263,81],[243,82],[269,83],[74,88],[75,88],[76,88],[77,88],[78,88],[79,88],[80,88],[81,88],[82,88],[83,88],[84,88],[85,88],[86,88],[87,88],[88,88],[89,88],[90,88],[91,88],[92,88],[93,88],[94,88],[95,88],[96,88],[97,88],[98,88],[99,88],[100,88],[101,88],[102,88],[103,88],[104,88],[105,88],[106,88],[107,88],[108,88],[109,88],[110,88],[111,88],[112,88],[113,88],[114,88],[115,88],[116,88],[117,88],[118,88],[119,88],[120,88],[121,88],[122,88],[123,88],[124,88],[125,88],[126,88],[127,88],[128,88],[129,88],[130,88],[131,88],[132,88],[133,88],[134,88],[135,88],[136,88],[137,88],[138,88],[139,88],[140,88],[141,88],[142,88],[143,88],[144,88],[145,88],[146,88],[147,88],[148,88],[149,88],[150,88],[151,88],[152,88],[153,88],[154,88],[155,88],[156,88],[157,88],[158,88],[159,88],[160,88],[161,88],[162,88],[163,88],[164,88],[165,88],[166,88],[167,88],[168,88],[169,88],[170,88],[171,88],[172,88],[173,88],[174,88],[175,88],[176,88],[177,88],[178,88],[179,88],[180,88],[181,88],[182,88],[183,88],[184,88],[185,88],[186,88],[187,88],[188,88],[189,88],[190,88],[191,88],[192,88],[193,88],[194,88],[195,88],[196,88],[197,88],[198,88],[199,88],[200,88],[201,88],[202,88],[203,88],[204,88],[205,88],[206,88],[207,88],[208,88],[209,88],[210,88],[211,88],[212,88],[213,88],[214,88],[215,88],[216,88],[217,88],[218,88],[219,88],[220,88],[221,88],[222,88],[223,88],[224,88],[225,88],[226,88],[227,88],[228,88],[229,88],[230,88],[231,88],[232,88],[233,88],[234,88],[235,88],[236,88],[237,88],[238,85]],"semanticDiagnosticsPerFile":[333,332,331,329,328,330,326,327,239,240,275,276,277,278,279,280,281,282,283,284,285,287,286,288,289,290,274,324,291,292,293,325,294,295,296,297,298,299,300,301,302,303,304,305,306,308,307,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,72,70,[73,[{"file":"../../node_modules/.pnpm/@types+react@18.2.45/node_modules/@types/react/index.d.ts","start":381,"length":19,"messageText":"Cannot find module 'scheduler/tracing' or its corresponding type declarations.","category":1,"code":2307}]],241,71,68,69,12,13,15,14,2,16,17,18,19,20,21,22,23,3,24,4,25,29,26,27,28,30,31,32,5,33,34,35,36,6,40,37,38,39,41,7,42,47,48,43,44,45,46,8,52,49,50,51,53,9,54,55,56,59,57,58,60,61,10,1,62,11,66,64,63,67,65,257,264,256,271,248,247,270,265,268,250,249,245,244,267,246,251,252,255,242,273,272,259,260,262,258,261,266,253,254,263,243,269,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238],"latestChangedDtsFile":"./dist/index.d.ts"},"version":"5.4.3"} \ No newline at end of file +{"program":{"fileNames":["../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es5.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2015.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2016.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2017.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2018.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2019.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2020.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2021.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2022.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2023.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.esnext.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.dom.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.dom.iterable.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2015.core.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2015.collection.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2015.generator.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2015.iterable.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2015.promise.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2015.proxy.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2015.reflect.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2015.symbol.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2016.array.include.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2016.intl.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2017.date.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2017.object.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2017.string.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2017.intl.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2018.intl.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2018.promise.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2018.regexp.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2019.array.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2019.object.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2019.string.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2019.symbol.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2019.intl.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2020.bigint.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2020.date.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2020.promise.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2020.string.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2020.intl.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2020.number.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2021.promise.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2021.string.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2021.weakref.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2021.intl.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2022.array.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2022.error.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2022.intl.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2022.object.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2022.sharedmemory.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2022.string.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2022.regexp.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2023.array.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.es2023.collection.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.esnext.collection.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.esnext.intl.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.esnext.disposable.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.esnext.promise.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.esnext.decorators.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.esnext.object.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.decorators.d.ts","../../node_modules/.pnpm/typescript@5.4.3/node_modules/typescript/lib/lib.decorators.legacy.d.ts","../../node_modules/.pnpm/@types+react@19.1.5/node_modules/@types/react/global.d.ts","../../node_modules/.pnpm/csstype@3.1.3/node_modules/csstype/index.d.ts","../../node_modules/.pnpm/@types+react@19.1.5/node_modules/@types/react/index.d.ts","./src/components/A.tsx","./src/components/Admin.tsx","./src/components/AlertCircle.tsx","./src/components/AlertTriangle.tsx","./src/components/Anthropic.tsx","./src/components/Apple.tsx","./src/components/Array.tsx","./src/components/ArrowDown.tsx","./src/components/ArrowLeft.tsx","./src/components/ArrowRight.tsx","./src/components/ArrowUp.tsx","./src/components/ArrowUpDown.tsx","./src/components/ArrowUpRight.tsx","./src/components/Audio.tsx","./src/components/Azure.tsx","./src/components/BarChart2.tsx","./src/components/Bell.tsx","./src/components/Boolean.tsx","./src/components/Building2.tsx","./src/components/Calendar.tsx","./src/components/Check.tsx","./src/components/CheckCircle2.tsx","./src/components/CheckSquare.tsx","./src/components/Checked.tsx","./src/components/ChevronDown.tsx","./src/components/ChevronLeft.tsx","./src/components/ChevronRight.tsx","./src/components/ChevronUp.tsx","./src/components/ChevronsLeft.tsx","./src/components/ChevronsRight.tsx","./src/components/ChevronsUpDown.tsx","./src/components/Circle.tsx","./src/components/ClipboardList.tsx","./src/components/Clock4.tsx","./src/components/Code.tsx","./src/components/Code2.tsx","./src/components/Cohere.tsx","./src/components/Component.tsx","./src/components/Condition.tsx","./src/components/Copy.tsx","./src/components/CreateRecord.tsx","./src/components/CreditCard.tsx","./src/components/Database.tsx","./src/components/DeepThinking.tsx","./src/components/Deepseek.tsx","./src/components/DivideCircle.tsx","./src/components/DivideSquare.tsx","./src/components/DollarSign.tsx","./src/components/Download.tsx","./src/components/DraggableHandle.tsx","./src/components/Edit.tsx","./src/components/Export.tsx","./src/components/Eye.tsx","./src/components/EyeOff.tsx","./src/components/File.tsx","./src/components/FileAudio.tsx","./src/components/FileCsv.tsx","./src/components/FileDocument.tsx","./src/components/FileExcel.tsx","./src/components/FileFont.tsx","./src/components/FileImage.tsx","./src/components/FileJson.tsx","./src/components/FilePack.tsx","./src/components/FilePdf.tsx","./src/components/FilePresentation.tsx","./src/components/FileQuestion.tsx","./src/components/FileScript.tsx","./src/components/FileSpreadsheet.tsx","./src/components/FileText.tsx","./src/components/FileUnknown.tsx","./src/components/FileVideo.tsx","./src/components/Filter.tsx","./src/components/Flame.tsx","./src/components/FreezeColumn.tsx","./src/components/Frown.tsx","./src/components/Gauge.tsx","./src/components/GetRecord.tsx","./src/components/Github.tsx","./src/components/GithubLogo.tsx","./src/components/GoogleLogo.tsx","./src/components/Hash.tsx","./src/components/Heart.tsx","./src/components/HelpCircle.tsx","./src/components/History.tsx","./src/components/Home.tsx","./src/components/HttpRequest.tsx","./src/components/Image.tsx","./src/components/Import.tsx","./src/components/Inbox.tsx","./src/components/Integration.tsx","./src/components/Kanban.tsx","./src/components/Key.tsx","./src/components/Layers.tsx","./src/components/LayoutGrid.tsx","./src/components/LayoutList.tsx","./src/components/LayoutTemplate.tsx","./src/components/License.tsx","./src/components/Lingyiwanwu.tsx","./src/components/Link.tsx","./src/components/ListChecks.tsx","./src/components/ListOrdered.tsx","./src/components/Loader2.tsx","./src/components/Lock.tsx","./src/components/LongText.tsx","./src/components/Mail.tsx","./src/components/MarkUnread.tsx","./src/components/Maximize2.tsx","./src/components/Menu.tsx","./src/components/MessageSquare.tsx","./src/components/Minimize2.tsx","./src/components/Mistral.tsx","./src/components/Moon.tsx","./src/components/MoreHorizontal.tsx","./src/components/Network.tsx","./src/components/Object.tsx","./src/components/Ollama.tsx","./src/components/Openai.tsx","./src/components/PackageCheck.tsx","./src/components/PaintBucket.tsx","./src/components/Pencil.tsx","./src/components/Percent.tsx","./src/components/Phone.tsx","./src/components/Play.tsx","./src/components/Plus.tsx","./src/components/PlusCircle.tsx","./src/components/Puzzle.tsx","./src/components/Qrcode.tsx","./src/components/Qwen.tsx","./src/components/Redo2.tsx","./src/components/RefreshCcw.tsx","./src/components/RotateCw.tsx","./src/components/Search.tsx","./src/components/SendMail.tsx","./src/components/Settings.tsx","./src/components/Share2.tsx","./src/components/Sheet.tsx","./src/components/ShieldCheck.tsx","./src/components/Sidebar.tsx","./src/components/SortAsc.tsx","./src/components/Square.tsx","./src/components/Star.tsx","./src/components/StretchHorizontal.tsx","./src/components/Sun.tsx","./src/components/SunMedium.tsx","./src/components/Table2.tsx","./src/components/Teable.tsx","./src/components/TeableNew.tsx","./src/components/ThumbsUp.tsx","./src/components/Trash.tsx","./src/components/Trash2.tsx","./src/components/Undo2.tsx","./src/components/UpdateRecord.tsx","./src/components/User.tsx","./src/components/UserEdit.tsx","./src/components/UserPlus.tsx","./src/components/Users.tsx","./src/components/Video.tsx","./src/components/Webhook.tsx","./src/components/X.tsx","./src/components/Xai.tsx","./src/components/Zap.tsx","./src/components/Zhipu.tsx","./src/components/ZoomIn.tsx","./src/components/ZoomOut.tsx","./src/index.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/assert.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/assert/strict.d.ts","../../node_modules/.pnpm/buffer@5.7.1/node_modules/buffer/index.d.ts","../../node_modules/.pnpm/undici-types@5.26.5/node_modules/undici-types/header.d.ts","../../node_modules/.pnpm/undici-types@5.26.5/node_modules/undici-types/readable.d.ts","../../node_modules/.pnpm/undici-types@5.26.5/node_modules/undici-types/file.d.ts","../../node_modules/.pnpm/undici-types@5.26.5/node_modules/undici-types/fetch.d.ts","../../node_modules/.pnpm/undici-types@5.26.5/node_modules/undici-types/formdata.d.ts","../../node_modules/.pnpm/undici-types@5.26.5/node_modules/undici-types/connector.d.ts","../../node_modules/.pnpm/undici-types@5.26.5/node_modules/undici-types/client.d.ts","../../node_modules/.pnpm/undici-types@5.26.5/node_modules/undici-types/errors.d.ts","../../node_modules/.pnpm/undici-types@5.26.5/node_modules/undici-types/dispatcher.d.ts","../../node_modules/.pnpm/undici-types@5.26.5/node_modules/undici-types/global-dispatcher.d.ts","../../node_modules/.pnpm/undici-types@5.26.5/node_modules/undici-types/global-origin.d.ts","../../node_modules/.pnpm/undici-types@5.26.5/node_modules/undici-types/pool-stats.d.ts","../../node_modules/.pnpm/undici-types@5.26.5/node_modules/undici-types/pool.d.ts","../../node_modules/.pnpm/undici-types@5.26.5/node_modules/undici-types/handlers.d.ts","../../node_modules/.pnpm/undici-types@5.26.5/node_modules/undici-types/balanced-pool.d.ts","../../node_modules/.pnpm/undici-types@5.26.5/node_modules/undici-types/agent.d.ts","../../node_modules/.pnpm/undici-types@5.26.5/node_modules/undici-types/mock-interceptor.d.ts","../../node_modules/.pnpm/undici-types@5.26.5/node_modules/undici-types/mock-agent.d.ts","../../node_modules/.pnpm/undici-types@5.26.5/node_modules/undici-types/mock-client.d.ts","../../node_modules/.pnpm/undici-types@5.26.5/node_modules/undici-types/mock-pool.d.ts","../../node_modules/.pnpm/undici-types@5.26.5/node_modules/undici-types/mock-errors.d.ts","../../node_modules/.pnpm/undici-types@5.26.5/node_modules/undici-types/proxy-agent.d.ts","../../node_modules/.pnpm/undici-types@5.26.5/node_modules/undici-types/api.d.ts","../../node_modules/.pnpm/undici-types@5.26.5/node_modules/undici-types/cookies.d.ts","../../node_modules/.pnpm/undici-types@5.26.5/node_modules/undici-types/patch.d.ts","../../node_modules/.pnpm/undici-types@5.26.5/node_modules/undici-types/filereader.d.ts","../../node_modules/.pnpm/undici-types@5.26.5/node_modules/undici-types/diagnostics-channel.d.ts","../../node_modules/.pnpm/undici-types@5.26.5/node_modules/undici-types/websocket.d.ts","../../node_modules/.pnpm/undici-types@5.26.5/node_modules/undici-types/content-type.d.ts","../../node_modules/.pnpm/undici-types@5.26.5/node_modules/undici-types/cache.d.ts","../../node_modules/.pnpm/undici-types@5.26.5/node_modules/undici-types/interceptors.d.ts","../../node_modules/.pnpm/undici-types@5.26.5/node_modules/undici-types/index.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/globals.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/async_hooks.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/buffer.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/child_process.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/cluster.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/console.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/constants.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/crypto.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/dgram.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/diagnostics_channel.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/dns.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/dns/promises.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/domain.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/dom-events.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/events.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/fs.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/fs/promises.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/http.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/http2.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/https.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/inspector.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/module.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/net.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/os.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/path.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/perf_hooks.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/process.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/punycode.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/querystring.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/readline.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/readline/promises.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/repl.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/stream.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/stream/promises.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/stream/consumers.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/stream/web.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/string_decoder.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/test.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/timers.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/timers/promises.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/tls.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/trace_events.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/tty.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/url.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/util.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/v8.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/vm.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/wasi.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/worker_threads.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/zlib.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/globals.global.d.ts","../../node_modules/.pnpm/@types+node@20.9.0/node_modules/@types/node/index.d.ts","../../node_modules/.pnpm/@types+jsonfile@6.1.4/node_modules/@types/jsonfile/index.d.ts","../../node_modules/.pnpm/@types+jsonfile@6.1.4/node_modules/@types/jsonfile/utils.d.ts","../../node_modules/.pnpm/@types+fs-extra@11.0.4/node_modules/@types/fs-extra/index.d.ts","../../node_modules/.pnpm/@types+react-dom@19.1.5_@types+react@19.1.5/node_modules/@types/react-dom/index.d.ts","../../node_modules/.pnpm/@types+react@19.0.0/node_modules/@types/react/index.d.ts"],"fileInfos":[{"version":"824cb491a40f7e8fdeb56f1df5edf91b23f3e3ee6b4cde84d4a99be32338faee","affectsGlobalScope":true},"45b7ab580deca34ae9729e97c13cfd999df04416a79116c3bfb483804f85ded4","3facaf05f0c5fc569c5649dd359892c98a85557e3e0c847964caeb67076f4d75","9a68c0c07ae2fa71b44384a839b7b8d81662a236d4b9ac30916718f7510b1b2d","5e1c4c362065a6b95ff952c0eab010f04dcd2c3494e813b493ecfd4fcb9fc0d8","68d73b4a11549f9c0b7d352d10e91e5dca8faa3322bfb77b661839c42b1ddec7","5efce4fc3c29ea84e8928f97adec086e3dc876365e0982cc8479a07954a3efd4","feecb1be483ed332fad555aff858affd90a48ab19ba7272ee084704eb7167569","5514e54f17d6d74ecefedc73c504eadffdeda79c7ea205cf9febead32d45c4bc","1c0cdb8dc619bc549c3e5020643e7cf7ae7940058e8c7e5aefa5871b6d86f44b","886e50ef125efb7878f744e86908884c0133e7a6d9d80013f421b0cd8fb2af94",{"version":"87d693a4920d794a73384b3c779cadcb8548ac6945aa7a925832fe2418c9527a","affectsGlobalScope":true},{"version":"76f838d5d49b65de83bc345c04aa54c62a3cfdb72a477dc0c0fce89a30596c30","affectsGlobalScope":true},{"version":"138fb588d26538783b78d1e3b2c2cc12d55840b97bf5e08bca7f7a174fbe2f17","affectsGlobalScope":true},{"version":"dc2df20b1bcdc8c2d34af4926e2c3ab15ffe1160a63e58b7e09833f616efff44","affectsGlobalScope":true},{"version":"4443e68b35f3332f753eacc66a04ac1d2053b8b035a0e0ac1d455392b5e243b3","affectsGlobalScope":true},{"version":"bc47685641087c015972a3f072480889f0d6c65515f12bd85222f49a98952ed7","affectsGlobalScope":true},{"version":"0dc1e7ceda9b8b9b455c3a2d67b0412feab00bd2f66656cd8850e8831b08b537","affectsGlobalScope":true},{"version":"ce691fb9e5c64efb9547083e4a34091bcbe5bdb41027e310ebba8f7d96a98671","affectsGlobalScope":true},{"version":"8d697a2a929a5fcb38b7a65594020fcef05ec1630804a33748829c5ff53640d0","affectsGlobalScope":true},{"version":"4ff2a353abf8a80ee399af572debb8faab2d33ad38c4b4474cff7f26e7653b8d","affectsGlobalScope":true},{"version":"93495ff27b8746f55d19fcbcdbaccc99fd95f19d057aed1bd2c0cafe1335fbf0","affectsGlobalScope":true},{"version":"6fc23bb8c3965964be8c597310a2878b53a0306edb71d4b5a4dfe760186bcc01","affectsGlobalScope":true},{"version":"ea011c76963fb15ef1cdd7ce6a6808b46322c527de2077b6cfdf23ae6f5f9ec7","affectsGlobalScope":true},{"version":"38f0219c9e23c915ef9790ab1d680440d95419ad264816fa15009a8851e79119","affectsGlobalScope":true},{"version":"bb42a7797d996412ecdc5b2787720de477103a0b2e53058569069a0e2bae6c7e","affectsGlobalScope":true},{"version":"4738f2420687fd85629c9efb470793bb753709c2379e5f85bc1815d875ceadcd","affectsGlobalScope":true},{"version":"2f11ff796926e0832f9ae148008138ad583bd181899ab7dd768a2666700b1893","affectsGlobalScope":true},{"version":"4de680d5bb41c17f7f68e0419412ca23c98d5749dcaaea1896172f06435891fc","affectsGlobalScope":true},{"version":"9fc46429fbe091ac5ad2608c657201eb68b6f1b8341bd6d670047d32ed0a88fa","affectsGlobalScope":true},{"version":"61c37c1de663cf4171e1192466e52c7a382afa58da01b1dc75058f032ddf0839","affectsGlobalScope":true},{"version":"b541a838a13f9234aba650a825393ffc2292dc0fc87681a5d81ef0c96d281e7a","affectsGlobalScope":true},{"version":"b20fe0eca9a4e405f1a5ae24a2b3290b37cf7f21eba6cbe4fc3fab979237d4f3","affectsGlobalScope":true},{"version":"811ec78f7fefcabbda4bfa93b3eb67d9ae166ef95f9bff989d964061cbf81a0c","affectsGlobalScope":true},{"version":"717937616a17072082152a2ef351cb51f98802fb4b2fdabd32399843875974ca","affectsGlobalScope":true},{"version":"d7e7d9b7b50e5f22c915b525acc5a49a7a6584cf8f62d0569e557c5cfc4b2ac2","affectsGlobalScope":true},{"version":"71c37f4c9543f31dfced6c7840e068c5a5aacb7b89111a4364b1d5276b852557","affectsGlobalScope":true},{"version":"576711e016cf4f1804676043e6a0a5414252560eb57de9faceee34d79798c850","affectsGlobalScope":true},{"version":"89c1b1281ba7b8a96efc676b11b264de7a8374c5ea1e6617f11880a13fc56dc6","affectsGlobalScope":true},{"version":"49ed889be54031e1044af0ad2c603d627b8bda8b50c1a68435fe85583901d072","affectsGlobalScope":true},{"version":"e93d098658ce4f0c8a0779e6cab91d0259efb88a318137f686ad76f8410ca270","affectsGlobalScope":true},{"version":"063600664504610fe3e99b717a1223f8b1900087fab0b4cad1496a114744f8df","affectsGlobalScope":true},{"version":"934019d7e3c81950f9a8426d093458b65d5aff2c7c1511233c0fd5b941e608ab","affectsGlobalScope":true},{"version":"bf14a426dbbf1022d11bd08d6b8e709a2e9d246f0c6c1032f3b2edb9a902adbe","affectsGlobalScope":true},{"version":"5e07ed3809d48205d5b985642a59f2eba47c402374a7cf8006b686f79efadcbd","affectsGlobalScope":true},{"version":"2b72d528b2e2fe3c57889ca7baef5e13a56c957b946906d03767c642f386bbc3","affectsGlobalScope":true},{"version":"8073890e29d2f46fdbc19b8d6d2eb9ea58db9a2052f8640af20baff9afbc8640","affectsGlobalScope":true},{"version":"368af93f74c9c932edd84c58883e736c9e3d53cec1fe24c0b0ff451f529ceab1","affectsGlobalScope":true},{"version":"af3dd424cf267428f30ccfc376f47a2c0114546b55c44d8c0f1d57d841e28d74","affectsGlobalScope":true},{"version":"995c005ab91a498455ea8dfb63aa9f83fa2ea793c3d8aa344be4a1678d06d399","affectsGlobalScope":true},{"version":"51e547984877a62227042850456de71a5c45e7fe86b7c975c6e68896c86fa23b","affectsGlobalScope":true},{"version":"956d27abdea9652e8368ce029bb1e0b9174e9678a273529f426df4b3d90abd60","affectsGlobalScope":true},{"version":"4fa6ed14e98aa80b91f61b9805c653ee82af3502dc21c9da5268d3857772ca05","affectsGlobalScope":true},{"version":"e6633e05da3ff36e6da2ec170d0d03ccf33de50ca4dc6f5aeecb572cedd162fb","affectsGlobalScope":true},{"version":"d8670852241d4c6e03f2b89d67497a4bbefe29ecaa5a444e2c11a9b05e6fccc6","affectsGlobalScope":true},{"version":"8444af78980e3b20b49324f4a16ba35024fef3ee069a0eb67616ea6ca821c47a","affectsGlobalScope":true},{"version":"caccc56c72713969e1cfe5c3d44e5bab151544d9d2b373d7dbe5a1e4166652be","affectsGlobalScope":true},{"version":"3287d9d085fbd618c3971944b65b4be57859f5415f495b33a6adc994edd2f004","affectsGlobalScope":true},{"version":"50d53ccd31f6667aff66e3d62adf948879a3a16f05d89882d1188084ee415bbc","affectsGlobalScope":true},{"version":"08a58483392df5fcc1db57d782e87734f77ae9eab42516028acbfe46f29a3ef7","affectsGlobalScope":true},{"version":"436aaf437562f276ec2ddbee2f2cdedac7664c1e4c1d2c36839ddd582eeb3d0a","affectsGlobalScope":true},{"version":"b1cb28af0c891c8c96b2d6b7be76bd394fddcfdb4709a20ba05a7c1605eea0f9","affectsGlobalScope":true},{"version":"13f6e6380c78e15e140243dc4be2fa546c287c6d61f4729bc2dd7cf449605471","affectsGlobalScope":true},{"version":"15b98a533864d324e5f57cd3cfc0579b231df58c1c0f6063ea0fcb13c3c74ff9","affectsGlobalScope":true},{"version":"ac77cb3e8c6d3565793eb90a8373ee8033146315a3dbead3bde8db5eaf5e5ec6","affectsGlobalScope":true},{"version":"d4b1d2c51d058fc21ec2629fff7a76249dec2e36e12960ea056e3ef89174080f","affectsGlobalScope":true},{"version":"2fef54945a13095fdb9b84f705f2b5994597640c46afeb2ce78352fab4cb3279","affectsGlobalScope":true},{"version":"33358442698bb565130f52ba79bfd3d4d484ac85fe33f3cb1759c54d18201393","affectsGlobalScope":true},{"version":"782dec38049b92d4e85c1585fbea5474a219c6984a35b004963b00beb1aab538","affectsGlobalScope":true},{"version":"170d4db14678c68178ee8a3d5a990d5afb759ecb6ec44dbd885c50f6da6204f6","affectsGlobalScope":true},"8a8eb4ebffd85e589a1cc7c178e291626c359543403d58c9cd22b81fab5b1fb9","63a3a080e64f95754b32cfbf6d1d06b2703ee53e3a46962739e88fdd98703261",{"version":"cf4f51c95006178f73b2a5390547f644727f7872beed0a38a9c49b7c62985733","signature":"793fcd8cb05ab3c547c38bf7b6061761aff248308b92711a1685e74b35d4b732"},{"version":"4198f4e6a0b98bf894cef46b9277fc99d98846efaa6bb46042b158523a9c75fa","signature":"3a1028add64854981e9676dcd934cf2413b96d1d3b57ea57d3c4cf2802ccee28"},{"version":"c4cca2c6f64a68927a7793c042f45d7b7dc940b63f4db27a24e6dd4cbcec24b6","signature":"94d7cb03427947d10364eb7489e90dbea08c8f2f3275630684aa10b8861e18d5"},{"version":"82140439b309b322545d9c2f93ebd30a325fac54974d849cbcabf50397eac3af","signature":"123e08996a5090621a564223839369bb8010a16e29b7aca732fdf668b9a82fac"},{"version":"9f01d515a2f1d698cd07485752c835544ff2d91336786ffe5151f7e15b5743ec","signature":"b75036cd4b2379f417629435a8b784c8a8a1d28e85e7001ba73154ce019e050e"},{"version":"2d03159b60a0ce25e9ba7470524217994f61d41e7ebd9ac452a09ee959077d68","signature":"9c49d7c5c30106706ec1fac0123f1d18aa5c8194922909ed943279907bf14a9d"},{"version":"1d04daa4b3851242a7efdd6f5438e77418e81de8279ca916933602b0bd068f5d","signature":"43d1d89e26c3dc60b5d5cd3b40e06989ee0be4f1407e07c6421aa79bc520939c"},{"version":"7c8302d89b8108bd9678cef74f4d43a1517702190dbc4fd4eefb6dc215f5c4c3","signature":"a4d5ed124d19f2f56e0a2413fff0e4f419519183e67fc673d28eab26b50e54c4"},{"version":"c76d27e4b0c8138ea612659d168d470d485ac388c3a45a3169613ca92dd517d7","signature":"a11cd407295c4a99f8c46412f5c74ebc7fc9a83a484c8342bfe11d04209df163"},{"version":"6e41b72852e6c487867c3426eca3d1d73a3624415250e7e5a718c918c073c3ac","signature":"b83c3c81a7a698eb2ef486528cb737c9b0245872fabb264094d6c6392dcf00dc"},{"version":"2e6b5fc5bf6ee457b396902edd991dbe5589ef18ff1e9befbaf1c728a6485aaf","signature":"51a2ac99aa78f610bb7487253cce9255fbc3fe17bca503ff68694e095bd9242a"},{"version":"40c24d9fdf47b4c7bf1bdd3029622c14699132be435964ce4a602dfdc06325f3","signature":"66c214793eec943afc8f4cd4699c4a5df02b2bf26d8b19ceb39080b4589fcb7d"},{"version":"dae195b83d941eebe0b4c29afae94cbfb4b840ac892d17a667bc6406994fb9a2","signature":"934264faa0190f05e8970a56ac76bbb0915bff9cd57fdbc92f3941be0f7f94fb"},{"version":"0c458f6d4c624b7bcd133d3e88c7f62a885ba114b0bd626c0c3938aa2dd2ebd6","signature":"b4beb523efee58f5d427757ef9de64ca4c0cafc4729563c232000e793ea84cef"},{"version":"eddeafe73bbcaa417ca11f31c3ce818342bf4af33e306b02e4f1e185b3884c93","signature":"bc941abd620d7dd60554d604f8a49db223056982bcdac6ab2eeeb523d36174ed"},{"version":"c1752693c90f6f8a515e11e4416a07b7a7240ea1302afa1de4b88153b786751c","signature":"46b9e43e3d94e674c0b62d25b1901a4d6fe82f26b07bd440206fe693b0700f11"},{"version":"99167e16aa3d4a65f58cba30f442f8088881134bcd83d5bdbb12d53ccca58edf","signature":"78a5b5966bb9405f4b08add09ade7e45e963bcd90b2d36dc418ad77af12b2ad1"},{"version":"83c4f4532bef827a63a6fcfda4b1819bb0deba171c557f4b779db05bb4c91b25","signature":"1a8f56ca54a48c02aedf38962b41e42e7f7cc97b806430abc15d2d0977c939a9"},{"version":"854620d087536658326a493f1c7cabd1943f2152f76e839369d4b9f68e89c262","signature":"edd4b2e6370cfeadc243ca326e2f3f9c9f3b7379af4d90f27e729203a04725db"},{"version":"c9fda8d722750ef70166e3567f059f7769a5a09d63d0d11047754297cb992329","signature":"e13ad2c34c0b7dedc416dd17e9778cd95efb5b7c2a167811a1b4f73dcdf326d6"},{"version":"4f5bf66b9e615381972ce0b5624c269e79f6bd3c1417a8a989ece2078d77f376","signature":"32d3d962cc2c9f6542d3ffa95097c387f1a7c953be1b7ca173fa4b11b5d21366"},{"version":"abca6c463859540e10c19803f9fca52de5ebc7bcd1fbb27ca2032475865a5292","signature":"bef1c591dc32a189513925e5003f85fcb8b52759a08f4bfa88775c24e9c0e52f"},{"version":"26326cc9da100321183a053e16b4553114d64bcfbac2bda4285eaf21e4ebd3c4","signature":"710be0b8e35935d92ab6c812eb9989833b36501e9e8fd77a2db1fdfcd3a99aa3"},{"version":"5765b616f0a24a84553e3689eb1a7dfb73f30cd0b6bcfb9ea7a792f3f6226c25","signature":"a02847feb44d6ad3023c85a7a69612ba9b0fbdd4598a2f132a32d05fd76506ae"},{"version":"813c3ac9a4791be23c4965f079e0896b4550ca2e63087cadec93af2ee6d43ed7","signature":"f5d337314e3514573f2b9a1e7e7f37cc815c4bd3738c763f4fe837faf1f4ed60"},{"version":"73cf86f8f87f74106bf44589d8a0e0ca99d3badde3c3f493a82fcf3b8a96cb67","signature":"a74bf8723c60e5907ecb95e3d7f4fb8f0c1790e8cd0dd32a5aaf301208065c06"},{"version":"466441949f83e44e7bd4254ee2e1e1a2b5af85ad3700e99f604b6120a4aebb14","signature":"2d494cfb904e682cdcf97c2645cfe63196eec4e6fab413cf0ef8bf5033a6f411"},{"version":"a7686d1f59cc651be217cc1420027e31baa82f0ae6b4991f36e9be1c8a7fe3c8","signature":"fdcb0f02c86e8dc1a2461e937ec5f24eaf7ee16ce51559c1d70a31c559747721"},{"version":"cc48bde0fa1c7b0ccfda6f6caa299ac2158de91bc6f6ee66cbaec24b19b4ee91","signature":"9986f78a6ee05fe3964655cde3b03695f361d084118317c8ce729baa934403d0"},{"version":"e0b6a4ac73de30899c21274962911f21a25c3a65ea0bd8a83ade05263c6c25c2","signature":"ad35d6dfe3ac904572c903aadb1fe27da68c63bf40bddf2146592893766faca3"},{"version":"dab26d3810fc078fab5989fd5b2a5fb127eb47442de5a6b5328fd147241c99a3","signature":"fd9e9bb5fd07ae1c471da8ea9c098d412ef6996fcd28c170237eabad21f03f56"},{"version":"c3f2994c68bfc76489593c0e6adea7f133fa092e1f08aa7dc1206ee39116e81d","signature":"75e5ca8ecb4e002ec013dc81b8c73ed0b6ac987c7f7c4e9df6a6cd2310bd4f2b"},{"version":"f4965adc62d05502dd5bb15920869c66fcf99a369807ae0b1484c888f6a79dbe","signature":"c753ae414649909c7c2db58790dcf657020b7226198135783ade6229fe273934"},{"version":"21d8f5f4f8439150dda71c17cf9af555498f13b66702bd3c53e88d29f9885747","signature":"1c93a19347a3c94c8f0b3b0de6ff9f3b6e13b52efa447989f6647946b4a7bbc2"},{"version":"eca42e7d96bbff08bc1f8b71c28549923b2712d2c991982a6fc70b6d3a954f66","signature":"7f2b8bf87329450ce59f6ad184ad01ac0e44d8f9cff451a3437d21a3e24c0432"},{"version":"32a881eb6e28fce7804733674392782c67f9b206669eee319fe728ecfa52be96","signature":"0b95d5838bca4ae0fc61382adad4ec2a203b767c36310a21b0f4544018014dfe"},{"version":"4fef640e44b64f31a1c2a43d6d036f4eca98d158963780d16366781834e014dc","signature":"8f399a318e33ddca38b6e298365c1d94ef84766bfd0be64c46485ecdb10e6eff"},{"version":"83867b0415a8e33d9e6655b639c8e5c04b03820884236126a838dc5f84f4b1f1","signature":"a696a531ca8a420bf6b2109ab85cb7f9e21c35165520b6eca574a7aa55421824"},{"version":"7e53b00c99fadfd2d4fa2b274290fc9249fa2ba485b90fafed62d5a43ddb25b5","signature":"639b10aab0920aaeac04252259a90a1daa30491a00d1d0ff99252cfaf203d926"},{"version":"c05eedff90a89df64ac326eb49782d1e94646c302e57d680ad66044192e83a0c","signature":"359e0afba160b4b4cf34eca6dcaad88af74edfb679f9465c7ed981cdeca423e4"},{"version":"7794b6d37b2248e892ea311d3b193feed5276c91e66d184483a9f3dcc8ea4d5d","signature":"ad90047aecdd8f5a29c27173124413f18029001bda342c978a7a9d197319017d"},{"version":"9b15584bb6e57def1f3c0999a6549a48a3bb631e72fa4ad8cd724ac0432816b6","signature":"01f69376a9226cd91b7f71cea3a80102edcb4df910a1dee4fbf6b222835e6f46"},{"version":"5e3c1d8f644583c697522170302f405e6d723696150f43c6f21efe5c66b245a1","signature":"b05760029b5a44d79f6b8d2556bf43f7d32595eff16d047cab842a835a716575"},{"version":"c289cc2c1d319f6357fa628ad3a2598a78ec8b57c5bc53610522d2203ab5c0de","signature":"f9ad5e7a4736b8fd64b113deb5f4262601c69678c9e818a1fe0a9f3a64b4d608"},{"version":"0f8b8bad6dd336917f9fab60af2eb8a42a1c458d596ee481a7ff67b5facba3a1","signature":"6617d0f61439a9291f5eed3b421546ac18ec8319852d7d39a0e341065c82b253"},{"version":"2402b79db79ee990c214278bc9b4e2dc11cf6c3d588e2a511fe02c1c0a4d78d8","signature":"96c9a0279cff69a0f7550de24bb1b9144cb382a806e86b330e7fc0a4bc36b173"},{"version":"ce2eec1cd315fe3213fabcf42273111c9081e3400c82d2d1ceedca2489ec82f7","signature":"0e7bc94df74e24c969dc5f1def7c246c6aac0e572bb5868c850d7586f5f479be"},{"version":"69dc16ad613f64fdaacbae76778ae04354939bd41957b46c6add7f45d6dff821","signature":"721e567698d06c7146e4631c0405cd34b81f7b582e2d2c2131fc74e97691538e"},{"version":"388a08114a0633b0048452a1b8e6735f1e3c654e2a7850531623c1098f8b6749","signature":"6b308b571131497fb7995259f72574e9e8c0ef7a0c6ddbccdc844c9998660d6e"},{"version":"78ce89281a09343d498fa99b68160634f922f10880d7876664b178801a2effd2","signature":"7112c11636756b0c855d16335745e4eb614a2bce3f6203e5d79e9b8c996d2f91"},{"version":"666fc4e4f195fe2f2839085bfe24f0f36982e25cae1c662fa69cfd2595d7e494","signature":"5262133846a6cfb74863cbc7bbd314f07b4ea79e467185449e804739cb7d256c"},{"version":"f59434e187255537f03917cd69a63c78b1e17d6b630a8d32a4679c741573e74e","signature":"7d9dd4ef2708f56cdd16596a6ed0a3ca954ab48199df1e92c0fcd030aa085202"},{"version":"d031dea1fa43cf0b4481b65568a3f2a9da8164bf4ebefb65729266ea0c2c10a0","signature":"e287e4158a7d3a00950683544f51d353e751f1299022a3562a88fbfa02eabdc3"},{"version":"9b06b1000d65fdf8079e04efdf14763736923150b64392e37329effcf29c5179","signature":"3037ac6f43fd4df29f409d2996b7c5239d0cc7b3051d7a2c7acc434493965d86"},{"version":"d975e39bc7deb2203920cfb1ec682ed59088a8c5fd7a7c62c612b09497c0a6b0","signature":"f1defa902697f9c1e33a9e8454c088715fe68c9b42a51668c0f88958651fc449"},{"version":"b05d4875ac4d48eeca25f33a55a5f5bc2c6f03ffaca6c6b1b36f78a12fbfa3c1","signature":"713c757b802cc7af29331cb4c358e07f1b6a7fd02fd2d435b12ac15b34c7a1f4"},{"version":"b5d415018197f4521735a26fb1c4b97200d0d7e6684c01efa18cac349d9a7b85","signature":"2e9c7577e8b2141301d7549a58c9b65fb33d898f76531abbc6f3394945a5ea3c"},{"version":"386b411232f1e81b3a3e6b687ad84bf5be580fc73f6e50b48002c28b4a202455","signature":"2ddff34d957d352f4993ca2d99eacab9870c3850c989ebfc1658f7caa46c6a3f"},{"version":"afd4ea549642c205223eb4b752e7d3b2365a7532b09c7bd83aa96218d29fa2f8","signature":"b67d62b0cd170a03e7e9a5e84a1110121ae5c1d5d5f8a5cffca2a9fc5641be9c"},{"version":"ac01efdef177f41868748b1308d2af2b4c953bcc5be05a29c92f8f3de3deb1b6","signature":"98d2e012e7ab63864e03f539002fb11ec95eae37920c207fa8c54aab15e535ac"},{"version":"5ee3d2e03be6e91b204047b9eb57876d4c1417b8fa483bdd2e3aa217ac74bbac","signature":"4a5c6c9f8f5d8bfa487cb395d01f8704fa46e597b5424fde099302d0f2b8e9d3"},{"version":"9b69efe39faa7f56978ee3b1bf3d53dd2adecfa4a258f4e63ad9e17101f5a564","signature":"ddffd169cac3718b180ee1a4a2772ffe135e4d1e06db075af683e3d2ca15a0ce"},{"version":"e30e3f7c9f339dfe14b85f6e905124c9c5d793e428e7208865624af884a9fa50","signature":"f75a3a3cf5bf7d7ef7068afe39aac37c787cae2043e90dfd94c967b6d818ab0f"},{"version":"c961cecd5b6d82bb51eb5f7ff2c7beb450ad17b94cd4ff69ee7b4ac0573d7daf","signature":"682006b8c1753c1e6fd5454df86d94fd27d60f8612dfce35b7af784b8d11539b"},{"version":"de16b95e4ca50846524d98985d5df100c20fb228a3f5f47ca864b259de670fbb","signature":"e1920744eb0253b6394040c7263824fd1f77a96befeeba64c2d476902e80cd76"},{"version":"765f39a5b08758e0ff15b8ba43eccde198b2ea82f955bb628c5c89f61879a874","signature":"11ff297e22645ded852b70582c2af82cb5a3832a36dfade226ab2913c03a482f"},{"version":"a38133cd47cf99e84e7c27b41c1b605d4343f87be7ce3f88f24dc50e09cd126f","signature":"820f329e737a14f25dca447a7e4fb368ae9c7b0ede325179200e9d59caabbe53"},{"version":"33e048db9486686623f9e20fcd972c7bc4335615740565a47d3c38edd27be9ee","signature":"39f85d0bb8bf86c0d1b768e99e1c7913f6ef410169e7a2d5e06ebc3b3805e744"},{"version":"86b41c29bd17b17cb90a3006b5f935caf44544970ca99ed641b6faf202fea2e1","signature":"0f6b48ab2c6cc68d5f552b5bf66a73b8e721c467363f7f58ebf0c0656b5a376e"},{"version":"07f4948296d43a091443d82999f515e2ff1fabd34b4af4d1c9ebbed9974bc53c","signature":"82ce077e2253638efed5ebfa0d731f8d1bf26591c73916c1f00a58a3b316a62d"},{"version":"be46080ba56eba3aeebffba944882c83cd298c6146914769981d6d09fcade791","signature":"5273f347acebe1b8f7bc9cd51fc7c0aa48c40bcfaf09192908fbbd1591d45822"},{"version":"752de27faeb21841eb6658235576f4147e805a9e8cf3eca865cc072c0a14b275","signature":"9fd77e9a5ad66ff7e28c11dee037ba892801f5422fa3b668c214511fde2b0072"},{"version":"442a39678c13e630c97bcef29d0e929b114e0eef34410f8cfe337c074273eedf","signature":"5456a03a3e65b2d044707aa39add5717e4ee748d8deb54e155eaf0586c7b7493"},{"version":"c252703d6bac58616b6624d6c74b78f2d1ff7535079f484f5d625c3b3e55dabb","signature":"c90324103e3e28d9a64c9f62be638fa061f64f5b449cfd7dba9a3568aa7a95a7"},{"version":"7cdf38597e76fcae8ea6c15dc968ed945e32525d9c8160b3b4739de4be81429b","signature":"2d66f0aa1b5eab5d1b733ae8281d8ca402b3a245d4fdb0af8bd3e611fb5d3bba"},{"version":"f44a964daae9ba8589ec8f6028bf5dc39fbc8804f8608e77266510e52b8dbaec","signature":"9a6d8d5d597201f597c8f619c4ef8110976c6455effa3ebb731cb17415054e39"},{"version":"c702046b51d77b83eb243fa9a87ac9c7c6f90a291aa0db8aea0b8a15f2a8ef5b","signature":"e488e4b1d1a84ad891f5dbdeab607a49e4620e02dd0d7414076b6d5794687648"},{"version":"9a8f751c5b1bd6e3b33a57127f2ba9e4258a16509af58a3dc0170ad20d833eef","signature":"9f161d98f452039d8624a94566fbf1a2f78f5f47bf2ebd730ea0bfc47d8caac7"},{"version":"75ac57972004a2645098997b5c1064f09a22ecc36865d4a692b8c3d9d890972e","signature":"8bb98bd88a3a0bd5c8f6d64ba30ec86d4dad17c92db1be659c3861b15414d59b"},{"version":"92cc8535098230c6a7457c11b501c5f5d3929014fd8743f59a5cb69384a00615","signature":"2e7e7d479c1c0ad2ebb82051a12df781fd6bec1a7a2cb430b93ef091eb181eb5"},{"version":"0ffe6a5eff9bfcf4ef50e4e28b8e257fa9d4210b2bcf7a48a2acfda818ee5ff4","signature":"6dbd43019e109f5a6adae6449bd22e61611f13d6cb8796048cdab9a55769c56a"},{"version":"464d374396eb3d730a5a3103e681d353aaa3efbc9860fb540465d3f6228d7630","signature":"36d0d03b1fc91419b4bec9486c6b81595699a7326ebe12382afdc61c700d7f0c"},{"version":"680e89bcaa7510c2e85dd5df3e2ac824d27e7ac6403f8d33d5b91dc5e97e8d64","signature":"0d0a1bf4882fd9ba6fcdf9d5358de2c45f7f0dbb5bf5b8da157c4793a2490373"},{"version":"41b33ce18c1fa63ad75a7212d0a0b56983b349750832cdbfdfbec310a86c8ae6","signature":"8f06b738fa0dae9e731ddaf5ff164bdd2c3fde6a38119e0100d0c02b6606e680"},{"version":"2d20394dfe778b0e6d810f304c4df13baef91e2956860d7fb81a93db10d97b18","signature":"0e63101e2d1d1522efd91dcb4e1ccdb63beee7d3caaec1a9203093095734f15e"},{"version":"072d0aa0a079bf983dbccdd8789f45cd8ba70d08d99ee485d2588d110d782325","signature":"50a31c87a0fa058eaf90a46705f48620b75b266346b494415a14d684c7da7395"},{"version":"14ddd77fd4a96cfd30e5ddc55873172bb9bd4078b356009faf8402811dc2572d","signature":"a079ff0c33ef13cc061ad7f6695ff8edfa435be8195f7706b3c3bbc91c77571c"},{"version":"6bbfcac2a5586666ac04d43df9c51e533411b6a5b113318de6fad6dd6b06f0b2","signature":"0775ae94319dcfc523a98bbcf519d14c27ee040a86c90acead1967052c9e8188"},{"version":"ff7030e7b2e108c5453f8a17ab04d75ac102e8241055f3c0fb4cf2f29d1e6703","signature":"f08771637b9894592f30d98068207367708fb74e7c88b3dbf90665c488120603"},{"version":"8e28842fdd19730994f81b983bdfc33d74dee9a85c195503e43618427df94bd3","signature":"717610f93b0ecc2f68261ee95a96b27b813487a0b0357cb928afd48b84883060"},{"version":"a3be6aff738f2ddb337a46dff8f86878382fe36723ce20a17a00ad0f35738a9b","signature":"325ff002aea559ff0c19a237548173e7a9a8eb3b5d72624610697ebbc3bb3e27"},{"version":"e98e56954850971bcc102a176df7eb10e211017fcebad938fe296cf3903b93d7","signature":"3e226ae598aa2c71d90333cd1e6b5c4f849a3fd2fdbfc8cbadceb028440affbe"},{"version":"df590b28093a13a2d4b19b5d84b63a99fc0b567979751eedcf656aef841eb849","signature":"efdfba9f01c46aeddcdaaeae75aaa68449f49c5229a661b90f77ec3870ac8cfa"},{"version":"b6ed09ae6c977e036985f3d51fedde538ac7e38f6d264fa299117a9b2ed4db09","signature":"382886c12f25e3b79521b34f2b9822d59bc971d8d2de1b6d169bb5fdcc4af529"},{"version":"d877905842f7e79d11aad26b49069cdd62eeda97c1ad95ef886fa095e7ed1f0d","signature":"f5c7b7280638c8d7190e7ad278982241f9f47bebdb0702917c2f8af85fc45f87"},{"version":"a92f000347755b04ea1dce635066f4eefde06b4c9e678aea0147a2544428844c","signature":"35602e4f9c44b823602b6980c36129c9a903b386945f9a83f810ca3df64d82d8"},{"version":"f6a34bc18da75bfe8866ea3f230f6f2a20c5ec5421be026074d0b2e6b6019b1f","signature":"25419c21e707946cce26b908165c9f0ec14ec82272f5bcb79a25e7b9123c8ea8"},{"version":"e5d7414b7a8a1032539bde73343d576dadfb95f1fd07aaa845c26897133d3508","signature":"fd55365a57c9e9e30674ca7fc63be408779f20c245487e5f507e25964098c99f"},{"version":"f390033c14e37c3e37cf57b0bc0496f4d6e8060a1c6e5f600c4eb4b6cff9597a","signature":"7b1ec174ad64cb1725f1ca0aed2ac4069baab8f4590914810d0e02e107895e8d"},{"version":"d6c9f998812a14a678407d5d205d537ddd2c35d45163914ca94851e06be20633","signature":"68527786790ae231e0cf15e5e50794a6359b5584d45a9180819fd9c9bb329232"},{"version":"9e764382af650282da112e54de7165ad56c69231f503471950050a2c11eab7d6","signature":"00b6b04c9e25c792e9071db683864bcef4dfcccb444f2884b70a823362640a90"},{"version":"733687cd08b716b09991fca7fba72433954057f8797d06588db588a2abf8e683","signature":"bb2531f43c70fc6b4e6d0a76286f8b54121a56ee2e2aeee9f4c0acc0fd914233"},{"version":"07bcfcd73a10121f4840f931823b38bd2fb222d51be3db2062c6ecfc1bba6eae","signature":"84099ad6270bc810ac365406ef17c6b178fdd03cb446b317c85a3e204d078de4"},{"version":"13c893f5a10ec0af106a7476a3365c36293960458c4d8c50899781ece83bfd71","signature":"f0a1c856b099c0fb1ca60a920e12f321b47c1b12a175b9a9fd39276f25e1ff58"},{"version":"95a3fb507b571610f875b4d75f4c7f6c67ba2a452b95f68bc153dc85d55e64cd","signature":"0615aca4b224f024df7602a3b0042237c979679cee159e6085b05cf8fe1cd891"},{"version":"8f87507021f2c8162fce74375f40f02a982babd3f2151c0827df2a75b03371bd","signature":"e31488663836629a8b6b87d12cc82fd3b28034d288cfcc0b3cb2365b762b7b78"},{"version":"73a0cc23944a7bce6856757d68b3d079edd7c2de0812c9504c2819f30accae5a","signature":"3f4cd7c373aeae3f6b010eb1ba3aed5d06728c22768d20dc9fc7dab2d7e458d2"},{"version":"ace1f0a770cb5872187fbf5ec739483cb30f92e2f9bc09c8b67c83351b11c017","signature":"93b58dcd300dd8f08dec4a956ba56b414e319310b59ca186422e5739f88bce16"},{"version":"d2146769683d32edc9888e69b24291505dba96a81fb17929e01f6bc26d69e87a","signature":"f7a834ddbcd50a45c3176ee9704cd7500c39b202fd37e32ea039738b17d2285c"},{"version":"4c03040570d648054a5180e827eea3f334b38235087480c022e0602f908c4c66","signature":"4d7cf21c5c36c3b6c22f45da69f31fe576b5964e068bbd7f3f01d1a9aaeedb1c"},{"version":"a95c01bb1ff97361ca1ca7f18ceedce6cadf82420dc8d3d49115aea0705d3f98","signature":"42cca77d4d88ff6f81913476791ecf776e607d00899c1a531b2f7408504913f2"},{"version":"0fd03e91ce37a1610dac81118b33eae77a46f3df74c5882b0ee18782e9770d64","signature":"dd27d147a99658eeec0762624f80967eccb59ceb09be593e150352967fff6b66"},{"version":"7e746959c17a31f42c72dcf5eacd45db438a6117fd6dd6aad54539d725c73a00","signature":"c29e1fe71d87b11a12857431d2ab9da6cf61ba18b804b82739b7ab4535d50dbe"},{"version":"9ddf74e5ed103accc0ce8a8038385bd3701f3c29d248e22e270da27d9e6133a5","signature":"952fd6fae615028b530123e4e17238f480397fb08c27e1ed20a2211d71dc6007"},{"version":"092a4fa604d9d45e9653a1151af6d16eb8da30ecbe47cff1c0c7bdee1fa46da2","signature":"3ba43b1d12972096d780293c1205f8480ed1cb39749c75de21388aa60fe47b2c"},{"version":"ecf9a3c748e6bd44216fc68170f6482e9544048639ed6d489393109293530cc8","signature":"67d749a631dddd2a3d4c97c35034970addb65cbbd807e81a3ed93d0c64ed29a7"},{"version":"08667b17363f80e9bec2b5edbe690731d02f41652a79049fcc109d59dd216170","signature":"417d6bf795c5d8930497030e76ec63642d7cc840da788510630ca724034dad72"},{"version":"16f2b46c705bf4ea8452edc4297b68d62a818600d843fd8081a05eeb7c615a8a","signature":"f192c60be78e238732a3aa09759991834633fbde6e80c1b66180dee828d4c9f9"},{"version":"009afbeaeff2b93a22dac88a665f5dd2fd18ebf3e72a06ca43d6c4a1384c0fda","signature":"5bd7046df9bda573bd5d05da99e337ef2a13cd6ef4727845741d0509959bf42c"},{"version":"7a8bfefd403987cc39d460e158e5bafa35e42a5c50c97052a44abf7af5366e5b","signature":"473a62d4306739eda703ed1ac319cb20968bc48d9ab2d02eef59329a9742e245"},{"version":"b52d4fdfeec42f6e1ecbeccd45deec1dccad8677ff66c5add27cada21b120f88","signature":"e39b7f56257b23109adf6eec10b216521c475929d637307f1c19d27fc6167338"},{"version":"c45c4e8ebca4c7e633a600c063d305f7c7b515008d284d01f61ed0615e8a1cf0","signature":"206d565048d8b3c6c7b0b659388f2603fc58de6ec28047c18382a591d59118d0"},{"version":"77d0ec594521372161f4bd7264803fae0be3c2a22145dc96cf435b3f4be6d620","signature":"cb52c1893f37489268bf1a50cb4dc2bc2eb0c2de12e3e0fd49cd5ac7280d3ca6"},{"version":"55ab3cf9d7fbd6e8c910b2262dfe77134c3d1ef1c24ed98c0bbed495ea283acb","signature":"b64ef296a6ab1187ec9dee4cdc4c40bd9459ca4412593fad52a184f4740870f6"},{"version":"d1dbc6515ca6e2ea85468c6f6711c17050265e7bae94744306041ec9b7aacc56","signature":"0ec2aadde09b3cefa474330235c0660ad5710cf832e42b938b55e9479c30e897"},{"version":"a24cff4e66408d6bb2e50363907a5248bf7ee67e52c58e803337a9b3560a410d","signature":"9e070ab8efd5525f966ad4dbca20d23ebb3ef32b82cd739425377a5bec5269fd"},{"version":"d5cf10ef3c71d81627c62cbdf8aad53cf39ef5a517341a67e20943123245bb43","signature":"4b89fed7b0f222bf4d142bd7c427214acd73ab5c379eb84581f98fa55987b385"},{"version":"fa99c463dfce1d33d5e0d2cae60a1120ff829abcffc5d84c21ae72f74712e42a","signature":"c9b9bfeeb5e58c8abc50beecffdcdb7a7922b39e8bd87d5a061742e453d18532"},{"version":"d459da40fa48467d4c5868b9ac8c67400a4e0c64095a2723ba2912e27d57420f","signature":"0ebc0b98c4a597eeec9d827f23de5b19a26d7c84ea87548624122579f6cf1c47"},{"version":"cb00947a4c6a6c0cf6f527990ff6f41ae8a3aca174a3fb5ef0aa1f41dcf120f4","signature":"7adf811d88b945bea2c165dd5f055f46c7527751888d75665d091c30ed695681"},{"version":"7c3468e92903bad5a6d24a332f82eeebb58f8a2bc86e61034a4f2aeccd62dd31","signature":"356fcb7318e4396e39f976d74c71250972de308442790851c6fc7d545515a348"},{"version":"c9e1ba040566ec15c16f6162f13d782678812dab014bdfa9b1966e45e47ac8c7","signature":"84feddc140f4eda7a499a6b13e4c49305e64f8e45338216e962d9810e55dbe05"},{"version":"f1b9c13b36daae6a58135aee3463e78bf3db33c7a6c222984a3334931585be9d","signature":"4428d9f02513d3b7722178ee8229cdb668353b8272864aca39046308d1e27f43"},{"version":"30d4ccf51c85873d4ad39e0d17fc0b8bb19f16356f19ba4b00aa93f2d0f6d4c2","signature":"0404680361d5785a1251b412f92ca0350f3fda89850fbdb2198370f69fe0d67e"},{"version":"ed9292185810774b2812fd32859afb7fa9fd7c6eca74be42cfa59d153da82459","signature":"e768845b2bb0f808722ff4abc78409a8f41aaceba7d4009ae218ea308addafff"},{"version":"7fafda40ea450f6fb029f9512c4eecf5cf07105e2d0cbd62064f1d462723a9f8","signature":"61787a6c4fccab8bb87463f89f979bfe169a20c22cb8d1b7c183b01cdc494d0a"},{"version":"856c2dc8f49d6db5f4821fc78a55fae58cfc874d04984262169a5a54d289b3a2","signature":"a8b188e3cbf9f386996b5e932ca4303a708f475b268ac2895a4a28a72e8c0972"},{"version":"9318bf9ad613bde387c2f4927c35fb2f9979c062bc1ab910a71c5dfb630f41b7","signature":"138cf2eb0363e1485da84bd5090eb0df0397572fe7a40449243ca0bf4c85cb34"},{"version":"5a3ec101cf2a0d0705d1650820c769d3d3247fe4d54a5a0964e6aa49fde4f9f4","signature":"31eeb7cbaaeb3136057785264ad7bc1ccd3666a962cfcac60d33e81d0274df44"},{"version":"449ecb3de0100a7906bdb8eb70272b17cdd606b15bbcb87e568f92068079e81e","signature":"b50ed306e9e4094110121b3fc653b01aa9f2b3b846813d76e3592976e075505f"},{"version":"fb4a51af9996918ad6a02a0272ebccb243bd18229ff0dd2865da3115251661cf","signature":"08a773556c6e31be739c73400aa908b8e4dc72bfa0cf2ebbf52485fff11d5619"},{"version":"7a1a0587da8a65b93b9ad579e2955c08d7ebd08413f61c9e8622ab2717aee1d5","signature":"409a71b09ec3e97d170cf3c6eacbe3b79731e0c0fb3e97e37ef33e5653e1d67e"},{"version":"b92c67e149140d3b5bc3637cb1f2a3b98990d21f16284ef103da00af88d91c05","signature":"20c8bd3a2917306f5266d4b9218922f68b94da6b1310c666b92ccc21ee21d807"},{"version":"b6be846a43560355d8d65720317bbe5b55d9fa4c9dd66379772a32a3f8748a64","signature":"0ca14f6ca40c5295f4541c999ab52dde2e514140e9f73e1be7fe231b557a7e53"},{"version":"ff9f2b01cd9e0adc3f03d2bf85f99e42c967fbfa3c13ce8a8618e7bd57e9b6d5","signature":"9aad5f476ad86f0a594f86290501573b2f588e256f8c38e5d24c174d716d47c2"},{"version":"096b311734ceaad25e2d5ed708c385017c61d794e55a23ade7d0502b48b7cb45","signature":"8986dae0c5316b3c99dc16bcd3d414057bf6cd3b395387f1d3cd8ed3e5ff9904"},{"version":"5614b140a360e5df88bef2a88445346261e4793e4757f51ed23ed5687ba38352","signature":"6312d71ed3239cdedc216e324a5f021883bc550c471e7b55f9db8c22aad63ff0"},{"version":"431ed9a9d14fb820c1bd19e968fbd0473e8eaf964f8b0168528080aaa5093a0f","signature":"be81fcf84654bd58aa776d2838d4d12018c70eb33d371d27e3798f51729b8b80"},{"version":"c9665be4bc1fee3d7cf381d9d8076b316b640420d99754b7e10b17ae7e7bb624","signature":"bc388ebbe4f5cff420b9b33000cc06f80ce92638e08f9ed1f27788c04bd8765e"},{"version":"ef448584e4e6579fcbafd7d56576907c47ed10e686e30ec4e5b5affa2c2bb62e","signature":"07fda70730090df550194f1b746bf7662ab6bc822d5b83ee39ffceb79f79143c"},{"version":"00c6f2ec25a3c2784f23c0fe6729d7eb687905b72d99e26e141817c591c5f059","signature":"abcaeeef722134fd8644f3aa58bd7bfbf3c4e53c86089d061554224b497ba7fc"},{"version":"36290629d28f73205ccd755785698518219bc6960f72409163d0cb4ddf43af46","signature":"e3e748f6cdced3d84241ea085cd2a33fed3786ad4b505de9eb7293678a5c5c15"},{"version":"6ddf069ecfa76b693949deedb93b328e276c51d2421a0a6dbb1e57b652902cd8","signature":"acb10d756ba7237876fc2c2deb1e531f35b3bac783a056a4145a190e49669257"},{"version":"0710ca00a482df2161d9b2aa3dd70a349d9c53a1542181cb41151754124a37b9","signature":"119816941f291d63803d89b44964567c30cee52a7cd7b9d3a58f4d08e8630d44"},{"version":"4999b27ec584c208f97f9c031f2aa7a35884eedf904dae7f52531ce420cc509d","signature":"6c7da223318941e4720a4d1ede7310787ac5c147a8c1ec6afb3e2ebca86fbf6a"},{"version":"4dcf0abaad211bf1ab8a9dc4549c33d8eab0080488a2c7cbaac045fa815234b5","signature":"2f92081d99d84204dc5c9f5294dae6c35f619732c649d222d358fd2897726177"},{"version":"343de9b06a28ab2cba9b52da7f8d011ee67597460c180c47f822b5fbdcc14e6e","signature":"17d4dddcdc8af645977e7b36f8fccf8e3970587cb75e0976736d902653cbdaeb"},{"version":"141c5a3c5d9ef8e796b4b667e5c740f8eac6e82be3ec588b2a8a21f6a50803cd","signature":"36ef8c9aaee7490e6b794f67fdf0f019e87aea73fa835a8d21b77b2d32eb21b0"},{"version":"3a603f297b4ee0ba65f83a1b8f6d0a6040ae5e8584afce7c5efff8c721b00630","signature":"934f7fd1bd96f7e49c82e1242153d543d4891f8b60f21e777a2ff749f6145033"},{"version":"1e0a8ed975b24497bcacea1a7ce3a6c77c09cca6564af83e6bc87359bd1fdc8e","signature":"119f476d9200e478cbe0d4718f3365adc1a08d39373aaca9a6fa746e88650ad1"},{"version":"49b83e76a776f7a266d60eeebda510356cb1fc77720db7512fc77c4122302cd9","signature":"3cafff51d2eb884a5fb175836c6559300d480c829cf7917d66adf3019ff2971f"},{"version":"3ba6477962523e9839c713f5999f336b3f46091a0f0be3996c7f869f0651e012","signature":"f2d2ac8bb32d70d376df55d5170e5e78e5b18b022b10086e06e87f8e276a324b"},{"version":"7b6d554f6d34c5d691ef5933affdd9cfb1af120f0546b0ba5309665b50ad68d6","signature":"fec1019418e2f5ef8d5dd3efd90911d3472e29c16d529f6591e9c61f0e3394f3"},{"version":"e2c48da2a267db97620cb1dc37361eb6108d13b191bc9845edb70e72457cc268","signature":"92596f93df0a40c067f669af97c0ae4a712e66ec0798e6345b3eb0ee7fe1a4c9"},"cf0e707b9af46eef58ac20a75be391f546c66c814b75871f1323bfef68804f71","09df3b4f1c937f02e7fee2836d4c4d7a63e66db70fd4d4e97126f4542cc21d9d","7394959e5a741b185456e1ef5d64599c36c60a323207450991e7a42e08911419","8e9c23ba78aabc2e0a27033f18737a6df754067731e69dc5f52823957d60a4b6","5929864ce17fba74232584d90cb721a89b7ad277220627cc97054ba15a98ea8f","7180c03fd3cb6e22f911ce9ba0f8a7008b1a6ddbe88ccf16a9c8140ef9ac1686","25c8056edf4314820382a5fdb4bb7816999acdcb929c8f75e3f39473b87e85bc","54cb85a47d760da1c13c00add10d26b5118280d44d58e6908d8e89abbd9d7725","3e4825171442666d31c845aeb47fcd34b62e14041bb353ae2b874285d78482aa","c6fd2c5a395f2432786c9cb8deb870b9b0e8ff7e22c029954fabdd692bff6195","a967bfe3ad4e62243eb604bf956101e4c740f5921277c60debaf325c1320bf88","e9775e97ac4877aebf963a0289c81abe76d1ec9a2a7778dbe637e5151f25c5f3","471e1da5a78350bc55ef8cef24eb3aca6174143c281b8b214ca2beda51f5e04a","cadc8aced301244057c4e7e73fbcae534b0f5b12a37b150d80e5a45aa4bebcbd","385aab901643aa54e1c36f5ef3107913b10d1b5bb8cbcd933d4263b80a0d7f20","9670d44354bab9d9982eca21945686b5c24a3f893db73c0dae0fd74217a4c219","db3435f3525cd785bf21ec6769bf8da7e8a776be1a99e2e7efb5f244a2ef5fee","c3b170c45fc031db31f782e612adf7314b167e60439d304b49e704010e7bafe5","40383ebef22b943d503c6ce2cb2e060282936b952a01bea5f9f493d5fb487cc7","4893a895ea92c85345017a04ed427cbd6a1710453338df26881a6019432febdd","3a84b7cb891141824bd00ef8a50b6a44596aded4075da937f180c90e362fe5f6","13f6f39e12b1518c6650bbb220c8985999020fe0f21d818e28f512b7771d00f9","9b5369969f6e7175740bf51223112ff209f94ba43ecd3bb09eefff9fd675624a","4fe9e626e7164748e8769bbf74b538e09607f07ed17c2f20af8d680ee49fc1da","24515859bc0b836719105bb6cc3d68255042a9f02a6022b3187948b204946bd2","33203609eba548914dc83ddf6cadbc0bcb6e8ef89f6d648ca0908ae887f9fcc5","0db18c6e78ea846316c012478888f33c11ffadab9efd1cc8bcc12daded7a60b6","89167d696a849fce5ca508032aabfe901c0868f833a8625d5a9c6e861ef935d2","e53a3c2a9f624d90f24bf4588aacd223e7bec1b9d0d479b68d2f4a9e6011147f","339dc5265ee5ed92e536a93a04c4ebbc2128f45eeec6ed29f379e0085283542c","9f0a92164925aa37d4a5d9dd3e0134cff8177208dba55fd2310cd74beea40ee2","8bfdb79bf1a9d435ec48d9372dc93291161f152c0865b81fc0b2694aedb4578d","2e85db9e6fd73cfa3d7f28e0ab6b55417ea18931423bd47b409a96e4a169e8e6","c46e079fe54c76f95c67fb89081b3e399da2c7d109e7dca8e4b58d83e332e605","d32275be3546f252e3ad33976caf8c5e842c09cb87d468cb40d5f4cf092d1acc","4a0c3504813a3289f7fb1115db13967c8e004aa8e4f8a9021b95285502221bd1",{"version":"4d719cfab49ae4045d15cb6bed0f38ad3d7d6eb7f277d2603502a0f862ca3182","affectsGlobalScope":true},"cce1f5f86974c1e916ec4a8cab6eec9aa8e31e8148845bf07fbaa8e1d97b1a2c",{"version":"5a856afb15f9dc9983faa391dde989826995a33983c1cccb173e9606688e9709","affectsGlobalScope":true},"546ab07e19116d935ad982e76a223275b53bff7771dab94f433b7ab04652936e","7b43160a49cf2c6082da0465876c4a0b164e160b81187caeb0a6ca7a281e85ba",{"version":"aefb5a4a209f756b580eb53ea771cca8aad411603926f307a5e5b8ec6b16dcf6","affectsGlobalScope":true},"a40826e8476694e90da94aa008283a7de50d1dafd37beada623863f1901cb7fb","f5a8b7ec4b798c88679194a8ebc25dcb6f5368e6e5811fcda9fe12b0d445b8db","b86e1a45b29437f3a99bad4147cb9fe2357617e8008c0484568e5bb5138d6e13","b5b719a47968cd61a6f83f437236bb6fe22a39223b6620da81ef89f5d7a78fb7","42c431e7965b641106b5e25ab3283aa4865ca7bb9909610a2abfa6226e4348be","0b7e732af0a9599be28c091d6bd1cb22c856ec0d415d4749c087c3881ca07a56","b7fe70be794e13d1b7940e318b8770cd1fb3eced7707805318a2e3aaac2c3e9e",{"version":"2c71199d1fc83bf17636ad5bf63a945633406b7b94887612bba4ef027c662b3e","affectsGlobalScope":true},{"version":"8d6138a264ddc6f94f16e99d4e117a2d6eb31b217891cf091b6437a2f114d561","affectsGlobalScope":true},"3b4c85eea12187de9929a76792b98406e8778ce575caca8c574f06da82622c54","f788131a39c81e0c9b9e463645dd7132b5bc1beb609b0e31e5c1ceaea378b4df","0c236069ce7bded4f6774946e928e4b3601894d294054af47a553f7abcafe2c1","21894466693f64957b9bd4c80fa3ec7fdfd4efa9d1861e070aca23f10220c9b2","396a8939b5e177542bdf9b5262b4eee85d29851b2d57681fa9d7eae30e225830","21773f5ac69ddf5a05636ba1f50b5239f4f2d27e4420db147fc2f76a5ae598ac",{"version":"6ec93c745c5e3e25e278fa35451bf18ef857f733de7e57c15e7920ac463baa2a","affectsGlobalScope":true},"a5fe4cc622c3bf8e09ababde5f4096ceac53163eefcd95e9cd53f062ff9bb67a","30c2ec6abf6aaa60eb4f32fb1235531506b7961c6d1bdc7430711aec8fd85295","0f05c06ff6196958d76b865ae17245b52d8fe01773626ac3c43214a2458ea7b7",{"version":"308b84e1943ef30015469770e931eb21b795348893b2a6562ca54ea8f0b3c41c","affectsGlobalScope":true},{"version":"d48009cbe8a30a504031cc82e1286f78fed33b7a42abf7602c23b5547b382563","affectsGlobalScope":true},"7aaeb5e62f90e1b2be0fc4844df78cdb1be15c22b427bc6c39d57308785b8f10","3ba30205a029ebc0c91d7b1ab4da73f6277d730ca1fc6692d5a9144c6772c76b","d8dba11dc34d50cb4202de5effa9a1b296d7a2f4a029eec871f894bddfb6430d","8b71dd18e7e63b6f991b511a201fad7c3bf8d1e0dd98acb5e3d844f335a73634","01d8e1419c84affad359cc240b2b551fb9812b450b4d3d456b64cda8102d4f60","458b216959c231df388a5de9dcbcafd4b4ca563bc3784d706d0455467d7d4942","269929a24b2816343a178008ac9ae9248304d92a8ba8e233055e0ed6dbe6ef71","93452d394fdd1dc551ec62f5042366f011a00d342d36d50793b3529bfc9bd633","f8c87b19eae111f8720b0345ab301af8d81add39621b63614dfc2d15fd6f140a","831c22d257717bf2cbb03afe9c4bcffc5ccb8a2074344d4238bf16d3a857bb12",{"version":"24ba151e213906027e2b1f5223d33575a3612b0234a0e2b56119520bbe0e594b","affectsGlobalScope":true},{"version":"cbf046714f3a3ba2544957e1973ac94aa819fa8aa668846fa8de47eb1c41b0b2","affectsGlobalScope":true},"aa34c3aa493d1c699601027c441b9664547c3024f9dbab1639df7701d63d18fa","eae74e3d50820f37c72c0679fed959cd1e63c98f6a146a55b8c4361582fa6a52","7c651f8dce91a927ab62925e73f190763574c46098f2b11fb8ddc1b147a6709a","7440ab60f4cb031812940cc38166b8bb6fbf2540cfe599f87c41c08011f0c1df",{"version":"aed89e3c18f4c659ee8153a76560dffda23e2d801e1e60d7a67abd84bc555f8d","affectsGlobalScope":true},{"version":"0ed13c80faeb2b7160bffb4926ff299c468e67a37a645b3ae0917ba0db633c1b","affectsGlobalScope":true},"e393915d3dc385e69c0e2390739c87b2d296a610662eb0b1cb85224e55992250","2f940651c2f30e6b29f8743fae3f40b7b1c03615184f837132b56ea75edad08b","5749c327c3f789f658072f8340786966c8b05ea124a56c1d8d60e04649495a4d",{"version":"c9d62b2a51b2ff166314d8be84f6881a7fcbccd37612442cf1c70d27d5352f50","affectsGlobalScope":true},"e7dbf5716d76846c7522e910896c5747b6df1abd538fee8f5291bdc843461795",{"version":"ab9b9a36e5284fd8d3bf2f7d5fcbc60052f25f27e4d20954782099282c60d23e","affectsGlobalScope":true},"b510d0a18e3db42ac9765d26711083ec1e8b4e21caaca6dc4d25ae6e8623f447","211440ce81e87b3491cdf07155881344b0a61566df6e749acff0be7e8b9d1a07","5d9a0b6e6be8dbb259f64037bce02f34692e8c1519f5cd5d467d7fa4490dced4","880da0e0f3ebca42f9bd1bc2d3e5e7df33f2619d85f18ee0ed4bd16d1800bc32","a0acca63c9e39580f32a10945df231815f0fe554c074da96ba6564010ffbd2d8"],"root":[[73,237]],"options":{"composite":true,"declaration":true,"declarationMap":true,"esModuleInterop":true,"jsx":2,"module":99,"noImplicitAny":false,"noUncheckedIndexedAccess":true,"noUnusedLocals":false,"noUnusedParameters":false,"outDir":"./dist","removeComments":true,"rootDir":"./src","skipLibCheck":true,"strict":true,"strictNullChecks":true,"target":99},"fileIdsList":[[288,317,324,325,326],[288,316,317,324],[317],[238,317],[274,317],[275,280,308,317],[276,287,288,295,305,316,317],[276,277,287,295,317],[278,317],[279,280,288,296,317],[280,305,313,317],[281,283,287,295,317],[282,317],[283,284,317],[287,317],[285,287,317],[274,287,317],[287,288,289,305,316,317],[287,288,289,302,305,308,317],[272,317,321],[283,287,290,295,305,316,317],[287,288,290,291,295,305,313,316,317],[290,292,305,313,316,317],[238,239,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323],[287,293,317],[294,316,317,321],[283,287,295,305,317],[296,317],[297,317],[274,298,317],[299,315,317,321],[300,317],[301,317],[287,302,303,317],[302,304,317,319],[275,287,305,306,307,308,317],[275,305,307,317],[305,306,317],[308,317],[309,317],[274,305,317],[287,311,312,317],[311,312,317],[280,295,305,313,317],[314,317],[295,315,317],[275,290,301,316,317],[280,317],[305,317,318],[294,317,319],[317,320],[275,280,287,289,298,305,316,317,319,321],[305,317,322],[72,317],[70,71,317],[249,253,316,317],[249,305,316,317],[244,317],[246,249,313,316,317],[295,313,317],[317,324],[244,317,324],[246,249,295,316,317],[241,242,245,248,275,287,305,316,317],[241,247,317],[245,249,275,308,316,317,324],[275,317,324],[265,275,317,324],[243,244,317,324],[249,317],[243,244,245,246,247,248,249,250,251,253,254,255,256,257,258,259,260,261,262,263,264,266,267,268,269,270,271,317],[249,256,257,317],[247,249,257,258,317],[248,317],[241,244,249,317],[249,253,257,258,317],[253,317],[247,249,252,316,317],[241,246,247,249,253,256,317],[275,305,317],[244,249,265,275,317,321,324],[73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,317],[329]],"referencedMap":[[327,1],[325,2],[326,3],[238,4],[239,4],[274,5],[275,6],[276,7],[277,8],[278,9],[279,10],[280,11],[281,12],[282,13],[283,14],[284,14],[286,15],[285,16],[287,17],[288,18],[289,19],[273,20],[323,3],[290,21],[291,22],[292,23],[324,24],[293,25],[294,26],[295,27],[296,28],[297,29],[298,30],[299,31],[300,32],[301,33],[302,34],[303,34],[304,35],[305,36],[307,37],[306,38],[308,39],[309,40],[310,41],[311,42],[312,43],[313,44],[314,45],[315,46],[316,47],[317,48],[318,49],[319,50],[320,51],[321,52],[322,53],[328,54],[70,3],[72,55],[240,3],[71,3],[68,3],[69,3],[12,3],[13,3],[15,3],[14,3],[2,3],[16,3],[17,3],[18,3],[19,3],[20,3],[21,3],[22,3],[23,3],[3,3],[24,3],[4,3],[25,3],[29,3],[26,3],[27,3],[28,3],[30,3],[31,3],[32,3],[5,3],[33,3],[34,3],[35,3],[36,3],[6,3],[40,3],[37,3],[38,3],[39,3],[41,3],[7,3],[42,3],[47,3],[48,3],[43,3],[44,3],[45,3],[46,3],[8,3],[52,3],[49,3],[50,3],[51,3],[53,3],[9,3],[54,3],[55,3],[56,3],[59,3],[57,3],[58,3],[60,3],[61,3],[10,3],[1,3],[62,3],[11,3],[66,3],[64,3],[63,3],[67,3],[65,3],[256,56],[263,57],[255,56],[270,58],[247,59],[246,60],[269,61],[264,62],[267,63],[249,64],[248,65],[244,66],[243,67],[266,68],[245,69],[250,70],[251,3],[254,70],[241,3],[272,71],[271,70],[258,72],[259,73],[261,74],[257,75],[260,76],[265,61],[252,77],[253,78],[262,79],[242,80],[268,81],[73,54],[74,54],[75,54],[76,54],[77,54],[78,54],[79,54],[80,54],[81,54],[82,54],[83,54],[84,54],[85,54],[86,54],[87,54],[88,54],[89,54],[90,54],[91,54],[92,54],[93,54],[94,54],[95,54],[96,54],[97,54],[98,54],[99,54],[100,54],[101,54],[102,54],[103,54],[104,54],[105,54],[106,54],[107,54],[108,54],[109,54],[110,54],[111,54],[112,54],[113,54],[114,54],[115,54],[116,54],[117,54],[118,54],[119,54],[120,54],[121,54],[122,54],[123,54],[124,54],[125,54],[126,54],[127,54],[128,54],[129,54],[130,54],[131,54],[132,54],[133,54],[134,54],[135,54],[136,54],[137,54],[138,54],[139,54],[140,54],[141,54],[142,54],[143,54],[144,54],[145,54],[146,54],[147,54],[148,54],[149,54],[150,54],[151,54],[152,54],[153,54],[154,54],[155,54],[156,54],[157,54],[158,54],[159,54],[160,54],[161,54],[162,54],[163,54],[164,54],[165,54],[166,54],[167,54],[168,54],[169,54],[170,54],[171,54],[172,54],[173,54],[174,54],[175,54],[176,54],[177,54],[178,54],[179,54],[180,54],[181,54],[182,54],[183,54],[184,54],[185,54],[186,54],[187,54],[188,54],[189,54],[190,54],[191,54],[192,54],[193,54],[194,54],[195,54],[196,54],[197,54],[198,54],[199,54],[200,54],[201,54],[202,54],[203,54],[204,54],[205,54],[206,54],[207,54],[208,54],[209,54],[210,54],[211,54],[212,54],[213,54],[214,54],[215,54],[216,54],[217,54],[218,54],[219,54],[220,54],[221,54],[222,54],[223,54],[224,54],[225,54],[226,54],[227,54],[228,54],[229,54],[230,54],[231,54],[232,54],[233,54],[234,54],[235,54],[236,54],[237,82]],"exportedModulesMap":[[327,1],[325,2],[326,3],[238,4],[239,4],[274,5],[275,6],[276,7],[277,8],[278,9],[279,10],[280,11],[281,12],[282,13],[283,14],[284,14],[286,15],[285,16],[287,17],[288,18],[289,19],[273,20],[323,3],[290,21],[291,22],[292,23],[324,24],[293,25],[294,26],[295,27],[296,28],[297,29],[298,30],[299,31],[300,32],[301,33],[302,34],[303,34],[304,35],[305,36],[307,37],[306,38],[308,39],[309,40],[310,41],[311,42],[312,43],[313,44],[314,45],[315,46],[316,47],[317,48],[318,49],[319,50],[320,51],[321,52],[322,53],[328,54],[70,3],[72,55],[240,3],[71,3],[68,3],[69,3],[12,3],[13,3],[15,3],[14,3],[2,3],[16,3],[17,3],[18,3],[19,3],[20,3],[21,3],[22,3],[23,3],[3,3],[24,3],[4,3],[25,3],[29,3],[26,3],[27,3],[28,3],[30,3],[31,3],[32,3],[5,3],[33,3],[34,3],[35,3],[36,3],[6,3],[40,3],[37,3],[38,3],[39,3],[41,3],[7,3],[42,3],[47,3],[48,3],[43,3],[44,3],[45,3],[46,3],[8,3],[52,3],[49,3],[50,3],[51,3],[53,3],[9,3],[54,3],[55,3],[56,3],[59,3],[57,3],[58,3],[60,3],[61,3],[10,3],[1,3],[62,3],[11,3],[66,3],[64,3],[63,3],[67,3],[65,3],[256,56],[263,57],[255,56],[270,58],[247,59],[246,60],[269,61],[264,62],[267,63],[249,64],[248,65],[244,66],[243,67],[266,68],[245,69],[250,70],[251,3],[254,70],[241,3],[272,71],[271,70],[258,72],[259,73],[261,74],[257,75],[260,76],[265,61],[252,77],[253,78],[262,79],[242,80],[268,81],[73,83],[74,83],[75,83],[76,83],[77,83],[78,83],[79,83],[80,83],[81,83],[82,83],[83,83],[84,83],[85,83],[86,83],[87,83],[88,83],[89,83],[90,83],[91,83],[92,83],[93,83],[94,83],[95,83],[96,83],[97,83],[98,83],[99,83],[100,83],[101,83],[102,83],[103,83],[104,83],[105,83],[106,83],[107,83],[108,83],[109,83],[110,83],[111,83],[112,83],[113,83],[114,83],[115,83],[116,83],[117,83],[118,83],[119,83],[120,83],[121,83],[122,83],[123,83],[124,83],[125,83],[126,83],[127,83],[128,83],[129,83],[130,83],[131,83],[132,83],[133,83],[134,83],[135,83],[136,83],[137,83],[138,83],[139,83],[140,83],[141,83],[142,83],[143,83],[144,83],[145,83],[146,83],[147,83],[148,83],[149,83],[150,83],[151,83],[152,83],[153,83],[154,83],[155,83],[156,83],[157,83],[158,83],[159,83],[160,83],[161,83],[162,83],[163,83],[164,83],[165,83],[166,83],[167,83],[168,83],[169,83],[170,83],[171,83],[172,83],[173,83],[174,83],[175,83],[176,83],[177,83],[178,83],[179,83],[180,83],[181,83],[182,83],[183,83],[184,83],[185,83],[186,83],[187,83],[188,83],[189,83],[190,83],[191,83],[192,83],[193,83],[194,83],[195,83],[196,83],[197,83],[198,83],[199,83],[200,83],[201,83],[202,83],[203,83],[204,83],[205,83],[206,83],[207,83],[208,83],[209,83],[210,83],[211,83],[212,83],[213,83],[214,83],[215,83],[216,83],[217,83],[218,83],[219,83],[220,83],[221,83],[222,83],[223,83],[224,83],[225,83],[226,83],[227,83],[228,83],[229,83],[230,83],[231,83],[232,83],[233,83],[234,83],[235,83],[236,83],[237,82]],"semanticDiagnosticsPerFile":[327,325,326,238,239,274,275,276,277,278,279,280,281,282,283,284,286,285,287,288,289,273,323,290,291,292,324,293,294,295,296,297,298,299,300,301,302,303,304,305,307,306,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,328,70,72,240,71,68,69,12,13,15,14,2,16,17,18,19,20,21,22,23,3,24,4,25,29,26,27,28,30,31,32,5,33,34,35,36,6,40,37,38,39,41,7,42,47,48,43,44,45,46,8,52,49,50,51,53,9,54,55,56,59,57,58,60,61,10,1,62,11,66,64,63,67,65,256,263,255,270,247,246,269,264,267,249,248,244,243,266,245,250,251,254,241,272,271,258,259,261,257,260,265,252,253,262,242,268,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237],"latestChangedDtsFile":"./dist/index.d.ts"},"version":"5.4.3"} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6e658a9..f53bc75 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -302,8 +302,11 @@ importers: specifier: 20.9.0 version: 20.9.0 '@types/react': - specifier: 18.2.45 - version: 18.2.45 + specifier: ^19.1.4 + version: 19.1.5 + '@types/react-dom': + specifier: ^19.1.5 + version: 19.1.5(@types/react@19.1.5) axios: specifier: 1.7.7 version: 1.7.7 @@ -950,85 +953,72 @@ packages: resolution: {integrity: sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew==} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-arm@1.1.0': resolution: {integrity: sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA==} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-ppc64@1.1.0': resolution: {integrity: sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ==} cpu: [ppc64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-s390x@1.1.0': resolution: {integrity: sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA==} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-x64@1.1.0': resolution: {integrity: sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q==} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.1.0': resolution: {integrity: sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w==} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.1.0': resolution: {integrity: sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A==} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-linux-arm64@0.34.2': resolution: {integrity: sha512-D8n8wgWmPDakc83LORcfJepdOSN6MvWNzzz2ux0MnIbOqdieRZwVYY32zxVx+IFUT8er5KPcyU3XXsn+GzG/0Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-linux-arm@0.34.2': resolution: {integrity: sha512-0DZzkvuEOqQUP9mo2kjjKNok5AmnOr1jB2XYjkaoNRwpAYMDzRmAqUIa1nRi58S2WswqSfPOWLNOr0FDT3H5RQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-linux-s390x@0.34.2': resolution: {integrity: sha512-EGZ1xwhBI7dNISwxjChqBGELCWMGDvmxZXKjQRuqMrakhO8QoMgqCrdjnAqJq/CScxfRn+Bb7suXBElKQpPDiw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-linux-x64@0.34.2': resolution: {integrity: sha512-sD7J+h5nFLMMmOXYH4DD9UtSNBD05tWSSdWAcEyzqW8Cn5UxXvsHAxmxSesYUsTOBmUnjtxghKDl15EvfqLFbQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-linuxmusl-arm64@0.34.2': resolution: {integrity: sha512-NEE2vQ6wcxYav1/A22OOxoSOGiKnNmDzCYFOZ949xFmrWZOVII1Bp3NqVVpvj+3UeHMFyN5eP/V5hzViQ5CZNA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-linuxmusl-x64@0.34.2': resolution: {integrity: sha512-DOYMrDm5E6/8bm/yQLCWyuDJwUnlevR8xtF8bs+gjZ7cyUNYXiSf/E8Kp0Ss5xasIaXSHzb888V1BE4i1hFhAA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-wasm32@0.34.2': resolution: {integrity: sha512-/VI4mdlJ9zkaq53MbIG6rZY+QRN3MLbR6usYlgITEzi4Rpx5S6LFKsycOQjkOGmqTNmkIdLjEvooFKwww6OpdQ==} @@ -1116,28 +1106,24 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@next/swc-linux-arm64-musl@15.3.2': resolution: {integrity: sha512-KQkMEillvlW5Qk5mtGA/3Yz0/tzpNlSw6/3/ttsV1lNtMuOHcGii3zVeXZyi4EJmmLDKYcTcByV2wVsOhDt/zg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@next/swc-linux-x64-gnu@15.3.2': resolution: {integrity: sha512-uRBo6THWei0chz+Y5j37qzx+BtoDRFIkDzZjlpCItBRXyMPIg079eIkOCl3aqr2tkxL4HFyJ4GHDes7W8HuAUg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@next/swc-linux-x64-musl@15.3.2': resolution: {integrity: sha512-+uxFlPuCNx/T9PdMClOqeE8USKzj8tVz37KflT3Kdbx/LOlZBRI2yxuIcmx1mPNK8DwSOMNCr4ureSet7eyC0w==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@next/swc-win32-arm64-msvc@15.3.2': resolution: {integrity: sha512-LLTKmaI5cfD8dVzh5Vt7+OMo+AIOClEdIU/TSKbXXT2iScUTSxOGoBhfuv+FU8R9MLmrkIL1e2fBMkEEjYAtPQ==} @@ -1505,67 +1491,56 @@ packages: resolution: {integrity: sha512-46OzWeqEVQyX3N2/QdiU/CMXYDH/lSHpgfBkuhl3igpZiaB3ZIfSjKuOnybFVBQzjsLwkus2mjaESy8H41SzvA==} cpu: [arm] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.41.0': resolution: {integrity: sha512-lfgW3KtQP4YauqdPpcUZHPcqQXmTmH4nYU0cplNeW583CMkAGjtImw4PKli09NFi2iQgChk4e9erkwlfYem6Lg==} cpu: [arm] os: [linux] - libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.41.0': resolution: {integrity: sha512-nn8mEyzMbdEJzT7cwxgObuwviMx6kPRxzYiOl6o/o+ChQq23gfdlZcUNnt89lPhhz3BYsZ72rp0rxNqBSfqlqw==} cpu: [arm64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.41.0': resolution: {integrity: sha512-l+QK99je2zUKGd31Gh+45c4pGDAqZSuWQiuRFCdHYC2CSiO47qUWsCcenrI6p22hvHZrDje9QjwSMAFL3iwXwQ==} cpu: [arm64] os: [linux] - libc: [musl] '@rollup/rollup-linux-loongarch64-gnu@4.41.0': resolution: {integrity: sha512-WbnJaxPv1gPIm6S8O/Wg+wfE/OzGSXlBMbOe4ie+zMyykMOeqmgD1BhPxZQuDqwUN+0T/xOFtL2RUWBspnZj3w==} cpu: [loong64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-powerpc64le-gnu@4.41.0': resolution: {integrity: sha512-eRDWR5t67/b2g8Q/S8XPi0YdbKcCs4WQ8vklNnUYLaSWF+Cbv2axZsp4jni6/j7eKvMLYCYdcsv8dcU+a6QNFg==} cpu: [ppc64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.41.0': resolution: {integrity: sha512-TWrZb6GF5jsEKG7T1IHwlLMDRy2f3DPqYldmIhnA2DVqvvhY2Ai184vZGgahRrg8k9UBWoSlHv+suRfTN7Ua4A==} cpu: [riscv64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.41.0': resolution: {integrity: sha512-ieQljaZKuJpmWvd8gW87ZmSFwid6AxMDk5bhONJ57U8zT77zpZ/TPKkU9HpnnFrM4zsgr4kiGuzbIbZTGi7u9A==} cpu: [riscv64] os: [linux] - libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.41.0': resolution: {integrity: sha512-/L3pW48SxrWAlVsKCN0dGLB2bi8Nv8pr5S5ocSM+S0XCn5RCVCXqi8GVtHFsOBBCSeR+u9brV2zno5+mg3S4Aw==} cpu: [s390x] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.41.0': resolution: {integrity: sha512-XMLeKjyH8NsEDCRptf6LO8lJk23o9wvB+dJwcXMaH6ZQbbkHu2dbGIUindbMtRN6ux1xKi16iXWu6q9mu7gDhQ==} cpu: [x64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-musl@4.41.0': resolution: {integrity: sha512-m/P7LycHZTvSQeXhFmgmdqEiTqSV80zn6xHaQ1JSqwCtD1YGtwEK515Qmy9DcB2HK4dOUVypQxvhVSy06cJPEg==} cpu: [x64] os: [linux] - libc: [musl] '@rollup/rollup-win32-arm64-msvc@4.41.0': resolution: {integrity: sha512-4yodtcOrFHpbomJGVEqZ8fzD4kfBeCbpsUy5Pqk4RluXOdsWdjLnjhiKy2w3qzcASWd04fp52Xz7JKarVJ5BTg==} @@ -1685,28 +1660,24 @@ packages: engines: {node: '>=10'} cpu: [arm64] os: [linux] - libc: [glibc] '@swc/core-linux-arm64-musl@1.11.29': resolution: {integrity: sha512-PwjB10BC0N+Ce7RU/L23eYch6lXFHz7r3NFavIcwDNa/AAqywfxyxh13OeRy+P0cg7NDpWEETWspXeI4Ek8otw==} engines: {node: '>=10'} cpu: [arm64] os: [linux] - libc: [musl] '@swc/core-linux-x64-gnu@1.11.29': resolution: {integrity: sha512-i62vBVoPaVe9A3mc6gJG07n0/e7FVeAvdD9uzZTtGLiuIfVfIBta8EMquzvf+POLycSk79Z6lRhGPZPJPYiQaA==} engines: {node: '>=10'} cpu: [x64] os: [linux] - libc: [glibc] '@swc/core-linux-x64-musl@1.11.29': resolution: {integrity: sha512-YER0XU1xqFdK0hKkfSVX1YIyCvMDI7K07GIpefPvcfyNGs38AXKhb2byySDjbVxkdl4dycaxxhRyhQ2gKSlsFQ==} engines: {node: '>=10'} cpu: [x64] os: [linux] - libc: [musl] '@swc/core-win32-arm64-msvc@1.11.29': resolution: {integrity: sha512-po+WHw+k9g6FAg5IJ+sMwtA/fIUL3zPQ4m/uJgONBATCVnDDkyW6dBA49uHNVtSEvjvhuD8DVWdFP847YTcITw==} @@ -1785,28 +1756,24 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.1.7': resolution: {integrity: sha512-PjGuNNmJeKHnP58M7XyjJyla8LPo+RmwHQpBI+W/OxqrwojyuCQ+GUtygu7jUqTEexejZHr/z3nBc/gTiXBj4A==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.1.7': resolution: {integrity: sha512-HMs+Va+ZR3gC3mLZE00gXxtBo3JoSQxtu9lobbZd+DmfkIxR54NO7Z+UQNPsa0P/ITn1TevtFxXTpsRU7qEvWg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.1.7': resolution: {integrity: sha512-MHZ6jyNlutdHH8rd+YTdr3QbXrHXqwIhHw9e7yXEBcQdluGwhpQY2Eku8UZK6ReLaWtQ4gijIv5QoM5eE+qlsA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.1.7': resolution: {integrity: sha512-ANaSKt74ZRzE2TvJmUcbFQ8zS201cIPxUDm5qez5rLEwWkie2SkGtA4P+GPTj+u8N6JbPrC8MtY8RmJA35Oo+A==} @@ -2001,9 +1968,6 @@ packages: '@types/oidc-provider@9.1.0': resolution: {integrity: sha512-UoC3ZQur+TtVL5hiUN8LoCbXocS2WI2eAPBtZtv1Y5F3vW0QTBawFAgDoctPqCQF73kah/Nzb5Gd3m5GtxFxiA==} - '@types/prop-types@15.7.14': - resolution: {integrity: sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==} - '@types/qs@6.14.0': resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==} @@ -2015,15 +1979,9 @@ packages: peerDependencies: '@types/react': ^19.0.0 - '@types/react@18.2.45': - resolution: {integrity: sha512-TtAxCNrlrBp8GoeEp1npd5g+d/OejJHFxS3OWmrPBMFaVQMSN0OFySozJio5BHxTuTeug00AVXVAjfDSfk+lUg==} - '@types/react@19.1.5': resolution: {integrity: sha512-piErsCVVbpMMT2r7wbawdZsq4xMvIAhQuac2gedQHysu1TZYEigE6pnFfgZT+/jQnrRuF5r+SHzuehFjfRjr4g==} - '@types/scheduler@0.26.0': - resolution: {integrity: sha512-WFHp9YUJQ6CKshqoC37iOlHnQSmxNc795UhB26CyBBttrN9svdIrUjl/NjnNmfcwtncN0h/0PPAFWv9ovP8mLA==} - '@types/send@0.17.4': resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==} @@ -3534,28 +3492,24 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] lightningcss-linux-arm64-musl@1.30.1: resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [musl] lightningcss-linux-x64-gnu@1.30.1: resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [glibc] lightningcss-linux-x64-musl@1.30.1: resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [musl] lightningcss-win32-arm64-msvc@1.30.1: resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==} @@ -6288,8 +6242,6 @@ snapshots: '@types/koa': 2.15.0 '@types/node': 20.17.50 - '@types/prop-types@15.7.14': {} - '@types/qs@6.14.0': {} '@types/range-parser@1.2.7': {} @@ -6298,18 +6250,10 @@ snapshots: dependencies: '@types/react': 19.1.5 - '@types/react@18.2.45': - dependencies: - '@types/prop-types': 15.7.14 - '@types/scheduler': 0.26.0 - csstype: 3.1.3 - '@types/react@19.1.5': dependencies: csstype: 3.1.3 - '@types/scheduler@0.26.0': {} - '@types/send@0.17.4': dependencies: '@types/mime': 1.3.5 From a0b7f22a251c6f1979f31ab0d464377650c96a94 Mon Sep 17 00:00:00 2001 From: ditiqi Date: Tue, 27 May 2025 10:01:56 +0800 Subject: [PATCH 4/9] add --- .env.example | 8 ++++---- apps/backend/.env.example | 2 +- dockers/search-elasticsearch.yml | 4 ++++ packages/client/src/api/hooks/useHello.ts | 8 +++++++- 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/.env.example b/.env.example index 8d2f688..a3b02d4 100644 --- a/.env.example +++ b/.env.example @@ -4,9 +4,9 @@ TIMEZONE=Asia/Singapore POSTGRES_VERSION=latest # PostgreSQL Env -POSTGRES_DB=nice +POSTGRES_DB=db POSTGRES_USER=nice -POSTGRES_PASSWORD=nice +POSTGRES_PASSWORD=Letusdoit123 # Redis 配置 REDIS_VERSION=7.2.4 @@ -14,8 +14,8 @@ REDIS_PASSWORD=nice # MinIO 配置 MINIO_VERSION=latest -MINIO_ACCESS_KEY=nice -MINIO_SECRET_KEY=nice123 +MINIO_ACCESS_KEY=nice1234 +MINIO_SECRET_KEY=nice1234 # Elasticsearch 配置 ELASTIC_VERSION=9.0.1 ELASTIC_PASSWORD=nice_elastic_password diff --git a/apps/backend/.env.example b/apps/backend/.env.example index d105a17..df91ae3 100644 --- a/apps/backend/.env.example +++ b/apps/backend/.env.example @@ -1,6 +1,6 @@ ELASTICSEARCH_NODE=http://localhost:9200 ELASTICSEARCH_USER=elastic -ELASTICSEARCH_PASSWORD=changeme +ELASTICSEARCH_PASSWORD=nice_elastic_password MINIO_ENDPOINT=localhost MINIO_PORT=9000 MINIO_USE_SSL=false diff --git a/dockers/search-elasticsearch.yml b/dockers/search-elasticsearch.yml index b0469a1..feda168 100644 --- a/dockers/search-elasticsearch.yml +++ b/dockers/search-elasticsearch.yml @@ -17,6 +17,10 @@ services: - type: volume source: elasticsearch_data target: /usr/share/elasticsearch/data + - type: tmpfs + target: /tmp + tmpfs: + size: 1G healthcheck: test: curl -s http://localhost:9200/_cluster/health | grep -v '"status":"red"' interval: 10s diff --git a/packages/client/src/api/hooks/useHello.ts b/packages/client/src/api/hooks/useHello.ts index a2fc1a2..5f06611 100644 --- a/packages/client/src/api/hooks/useHello.ts +++ b/packages/client/src/api/hooks/useHello.ts @@ -1,6 +1,12 @@ +import { useQuery } from '@tanstack/react-query'; +import { useTRPC } from '../trpc'; export function useHello() { + const trpc = useTRPC(); + trpc.user.getUser.queryOptions() + + // useQuery; return { - hello: api.hello, + // hello: api.hello, }; } From d4554cee03345b3b8a89c09b10b7165c120bc06f Mon Sep 17 00:00:00 2001 From: ditiqi Date: Tue, 27 May 2025 15:37:27 +0800 Subject: [PATCH 5/9] add --- apps/backend/src/index.ts | 48 +++-- apps/backend/src/socket.ts | 122 +++++++++++ apps/web/.env.example | 2 +- apps/web/app/page.tsx | 160 ++++++++++++++- dockers/database-postgres.yml | 15 +- packages/client/src/api/hooks/useHello.ts | 6 +- packages/client/src/index.ts | 2 + packages/client/src/websocket/client.ts | 192 ++++++++++++++++++ packages/client/src/websocket/index.ts | 3 + packages/client/src/websocket/type.ts | 18 ++ packages/client/src/websocket/useWebSocket.ts | 189 +++++++++++++++++ packages/db/.env.example | 2 +- 12 files changed, 724 insertions(+), 35 deletions(-) create mode 100644 apps/backend/src/socket.ts create mode 100644 packages/client/src/websocket/client.ts create mode 100644 packages/client/src/websocket/index.ts create mode 100644 packages/client/src/websocket/type.ts create mode 100644 packages/client/src/websocket/useWebSocket.ts diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts index 36e5284..f2f5b74 100644 --- a/apps/backend/src/index.ts +++ b/apps/backend/src/index.ts @@ -1,17 +1,20 @@ -import { Hono } from 'hono' -import { logger } from 'hono/logger' -import { contextStorage, getContext } from 'hono/context-storage' -import { prettyJSON } from 'hono/pretty-json' -import { cors } from 'hono/cors' +import { Hono } from 'hono'; +import { logger } from 'hono/logger'; +import { contextStorage, getContext } from 'hono/context-storage'; +import { prettyJSON } from 'hono/pretty-json'; +import { cors } from 'hono/cors'; -import { trpcServer } from '@hono/trpc-server' +import { trpcServer } from '@hono/trpc-server'; +import Redis from 'ioredis'; +import redis from './redis'; +import minioClient from './minio'; +import { Client } from 'minio'; +import oidc from './oidc/provider'; +import { appRouter } from './trpc'; + +import { createBunWebSocket } from 'hono/bun'; +import { wsHandler, wsConfig } from './socket'; -import Redis from 'ioredis' -import redis from './redis' -import minioClient from './minio' -import { Client } from 'minio' -import oidc from './oidc/provider' -import { appRouter } from './trpc' type Env = { Variables: { redis: Redis; @@ -21,10 +24,13 @@ type Env = { const app = new Hono(); -app.use('*', cors({ - origin: 'http://localhost:3001', - credentials: true, -})) +app.use( + '*', + cors({ + origin: 'http://localhost:3001', + credentials: true, + }), +); app.use('*', async (c, next) => { c.set('redis', redis); @@ -51,4 +57,14 @@ app.use('/oidc/*', async (c, next) => { // return void 也可以 return; }); + +// 添加 WebSocket 路由 +app.get('/ws', wsHandler); +const bunServerConfig = { + port: 3000, + fetch: app.fetch, + ...wsConfig, +}; +// 启动 Bun 服务器 +Bun.serve(bunServerConfig); export default app; diff --git a/apps/backend/src/socket.ts b/apps/backend/src/socket.ts new file mode 100644 index 0000000..2ff3db4 --- /dev/null +++ b/apps/backend/src/socket.ts @@ -0,0 +1,122 @@ +import { createBunWebSocket } from 'hono/bun'; +import type { ServerWebSocket } from 'bun'; +import { WSContext } from 'hono/ws'; +import { MessageType } from '../../../packages/client/src/websocket/type'; + +// 定义消息类型接口 +interface WSMessage { + type: 'join' | 'leave' | 'message'; + roomId: string; + data: any; +} + +// 创建 WebSocket 实例并指定泛型类型 +const { upgradeWebSocket, websocket } = createBunWebSocket(); + +// 存储房间和连接的映射关系 +const rooms = new Map>>(); + +// WebSocket 处理器 +const wsHandler = upgradeWebSocket((c) => { + // 存储当前连接加入的房间 + const joinedRooms = new Set(); + + return { + onOpen(_, ws) { + console.log('WebSocket 连接已建立'); + }, + onMessage(message, ws) { + try { + const parsedMessage: WSMessage = JSON.parse(message.data); + console.log('收到消息:', parsedMessage); + + switch (parsedMessage.type) { + case 'join': + // 加入房间 + if (!rooms.has(parsedMessage.roomId)) { + rooms.set(parsedMessage.roomId, new Set()); + } + rooms.get(parsedMessage.roomId)?.add(ws); + joinedRooms.add(parsedMessage.roomId); + + // 发送加入成功消息 + ws.send( + JSON.stringify({ + type: 'system', + data: { + text: `成功加入房间 ${parsedMessage.roomId}`, + type: MessageType.TEXT, + }, + roomId: parsedMessage.roomId, + }), + ); + break; + + case 'leave': + // 离开房间 + rooms.get(parsedMessage.roomId)?.delete(ws); + joinedRooms.delete(parsedMessage.roomId); + if (rooms.get(parsedMessage.roomId)?.size === 0) { + rooms.delete(parsedMessage.roomId); + } + break; + + case 'message': + // 在指定房间内广播消息 + const room = rooms.get(parsedMessage.roomId); + if (room) { + const messageToSend = { + type: 'message', + data: parsedMessage.data, + roomId: parsedMessage.roomId, + }; + + for (const conn of room) { + if (conn.readyState === WebSocket.OPEN) { + conn.send(JSON.stringify(messageToSend)); + } + } + } + break; + } + } catch (error) { + console.error('处理消息时出错:', error); + ws.send( + JSON.stringify({ + type: 'error', + data: { + text: '消息处理失败', + type: MessageType.TEXT, + }, + }), + ); + } + }, + onClose(_, ws) { + console.log('WebSocket 连接已关闭'); + // 清理所有加入的房间 + for (const roomId of joinedRooms) { + rooms.get(roomId)?.delete(ws); + if (rooms.get(roomId)?.size === 0) { + rooms.delete(roomId); + } + } + }, + onError(error, ws) { + console.error('WebSocket 错误:', error); + // 清理所有加入的房间 + for (const roomId of joinedRooms) { + rooms.get(roomId)?.delete(ws); + if (rooms.get(roomId)?.size === 0) { + rooms.delete(roomId); + } + } + }, + }; +}); + +export const wsConfig = { + websocket, +}; + +export { wsHandler, rooms }; diff --git a/apps/web/.env.example b/apps/web/.env.example index 8183f56..72f3759 100644 --- a/apps/web/.env.example +++ b/apps/web/.env.example @@ -1 +1 @@ -DATABASE_URL="postgresql://root:Letusdoit000@localhost:5432/app?schema=public" \ No newline at end of file +DATABASE_URL="postgresql://nice:Letusdoit123@localhost:5432/db?schema=public" \ No newline at end of file diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index 60c4991..e34e682 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -1,13 +1,161 @@ 'use client'; -import { useTRPC } from '@repo/client'; +import { useHello, useTRPC, useWebSocket, MessageType } from '@repo/client'; import { useQuery } from '@tanstack/react-query'; -import { useEffect } from 'react'; +import { useRef, useState } from 'react'; export default function Home() { const trpc = useTRPC(); const { data, isLoading } = useQuery(trpc.user.getUser.queryOptions()); - useEffect(() => { - console.log(data); - }, [data]); - return
123
; + const [message, setMessage] = useState(''); + const [roomId, setRoomId] = useState(''); + const messagesEndRef = useRef(null); + + // 使用 WebSocket hook + const { messages, currentRoom, connecting, joinRoom, leaveRoom, sendMessage } = useWebSocket(); + + // 滚动到底部 + const scrollToBottom = () => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + setTimeout(scrollToBottom, 100); + }; + + const handleJoinRoom = async () => { + const success = await joinRoom(roomId.trim()); + if (success) { + setRoomId(''); + } + }; + + const handleLeaveRoom = async () => { + await leaveRoom(); + }; + + const handleSendMessage = async () => { + const success = await sendMessage({ + text: message, + type: MessageType.TEXT, + }); + if (success) { + setMessage(''); + } + }; + + // 处理按回车发送消息 + const handleKeyPress = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleSendMessage(); + } + }; + + return ( +
+

WebSocket 房间测试

+ + {/* 房间管理 */} +
+

房间管理

+ {!currentRoom ? ( +
+ setRoomId(e.target.value)} + onKeyPress={handleKeyPress} + disabled={connecting} + className="border border-gray-300 rounded px-3 py-2 flex-1" + placeholder="输入房间ID..." + /> + +
+ ) : ( +
+ 当前房间: {currentRoom} + +
+ )} +
+ + {/* 消息显示区域 */} + {currentRoom && ( +
+
+ {messages.map((msg, index) => ( +
+
+ {msg.data.type === MessageType.IMAGE && msg.data.fileUri && ( + 图片消息 + )} + {msg.data.type === MessageType.FILE && msg.data.fileUri && ( + + 下载文件 + + )} + {msg.data.text &&
{msg.data.text}
} +
+
+ ))} +
+
+
+ setMessage(e.target.value)} + onKeyPress={handleKeyPress} + disabled={connecting} + className="border border-gray-300 rounded px-3 py-2 flex-1" + placeholder="输入消息..." + /> + +
+
+ )} + + {!currentRoom && ( +
+

提示: 请先加入一个房间开始聊天

+

打开多个浏览器窗口,输入相同的房间ID来测试房间内通信

+
+ )} +
+ ); } diff --git a/dockers/database-postgres.yml b/dockers/database-postgres.yml index 91c23e9..27d324c 100644 --- a/dockers/database-postgres.yml +++ b/dockers/database-postgres.yml @@ -3,21 +3,18 @@ services: image: postgres:${POSTGRES_VERSION:-16} container_name: postgres ports: - - "5432:5432" + - '5432:5432' environment: - POSTGRES_DB=${POSTGRES_DB:-nice_db} - POSTGRES_USER=${POSTGRES_USER:-nice_user} - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-nice_password} volumes: - - type: volume - source: postgres_data - target: /var/lib/postgresql/data + - ./volumes/postgres:/var/lib/postgresql/data + # - type: volume + # source: postgres_data + # target: /var/lib/postgresql/data healthcheck: - test: - [ - "CMD-SHELL", - "sh -c 'pg_isready -U ${POSTGRES_USER:-nice_user} -d ${POSTGRES_DB:-nice_db}'", - ] + test: ['CMD-SHELL', "sh -c 'pg_isready -U ${POSTGRES_USER:-nice_user} -d ${POSTGRES_DB:-nice_db}'"] interval: 10s timeout: 3s retries: 3 diff --git a/packages/client/src/api/hooks/useHello.ts b/packages/client/src/api/hooks/useHello.ts index 5f06611..a3bd83d 100644 --- a/packages/client/src/api/hooks/useHello.ts +++ b/packages/client/src/api/hooks/useHello.ts @@ -3,10 +3,12 @@ import { useTRPC } from '../trpc'; export function useHello() { const trpc = useTRPC(); - trpc.user.getUser.queryOptions() - + // trpc.user.getUser.queryOptions(); + const getUser = useQuery(trpc.user.getUser.queryOptions()); + console.log(getUser); // useQuery; return { + getUser: getUser, // hello: api.hello, }; } diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts index b1c13e7..8bac1ff 100755 --- a/packages/client/src/index.ts +++ b/packages/client/src/index.ts @@ -1 +1,3 @@ export * from './api'; +export * from './websocket'; +export { MessageType } from './websocket/type'; diff --git a/packages/client/src/websocket/client.ts b/packages/client/src/websocket/client.ts new file mode 100644 index 0000000..c1427d3 --- /dev/null +++ b/packages/client/src/websocket/client.ts @@ -0,0 +1,192 @@ +// WebSocket 消息接口 + +import { WSMessage, WSMessageParams, MessageType } from './type'; + +export class WebSocketClient { + private ws: WebSocket | null = null; + private url: string; + private currentRoom: string | null = null; + private messageHandler: ((message: WSMessage) => void) | null = null; + private connectionPromise: Promise | null = null; + + constructor(url: string = 'ws://localhost:3000/ws') { + this.url = url; + } + + private async ensureConnected(): Promise { + if (this.ws?.readyState === WebSocket.OPEN) { + return; + } + + if (this.connectionPromise) { + return this.connectionPromise; + } + + this.connectionPromise = new Promise((resolve, reject) => { + this.disconnect(); // 确保先断开旧连接 + + this.ws = new WebSocket(this.url); + + this.ws.onopen = () => { + console.log('WebSocket 连接已建立'); + this.connectionPromise = null; + resolve(); + }; + + this.ws.onmessage = (event) => { + try { + const message = JSON.parse(event.data) as WSMessage; + + // 只处理系统消息、错误消息,或者当前房间的消息 + if ( + message.type === 'system' || + message.type === 'error' || + (message.roomId && message.roomId === this.currentRoom) + ) { + console.log('收到消息:', message); + // 触发消息处理器 + if (this.messageHandler) { + this.messageHandler(message); + } + } + } catch (error) { + console.error('解析消息失败:', error); + } + }; + + this.ws.onclose = () => { + console.log('WebSocket 连接已关闭'); + this.cleanup(); + reject(new Error('WebSocket connection closed')); + }; + + this.ws.onerror = (error) => { + console.error('WebSocket 错误:', error); + this.cleanup(); + reject(error); + }; + + // 5秒超时 + setTimeout(() => { + if (this.ws?.readyState !== WebSocket.OPEN) { + this.cleanup(); + reject(new Error('WebSocket connection timeout')); + } + }, 5000); + }); + + return this.connectionPromise; + } + + async connect() { + return this.ensureConnected(); + } + + private cleanup() { + if (this.ws) { + this.ws.onclose = null; + this.ws.onerror = null; + this.ws.onmessage = null; + this.ws.onopen = null; + this.ws = null; + } + this.currentRoom = null; + this.messageHandler = null; + this.connectionPromise = null; + } + + // 设置消息处理器 + onMessage(handler: (message: WSMessage) => void) { + this.messageHandler = handler; + return () => { + if (this.messageHandler === handler) { + this.messageHandler = null; + } + }; + } + + async joinRoom(roomId: string) { + try { + await this.ensureConnected(); + + // 如果已经在一个房间中,先离开 + if (this.currentRoom) { + await this.leaveRoom(); + } + + const message: WSMessage = { + type: 'join', + roomId, + data: { + text: `加入房间 ${roomId}`, + type: MessageType.TEXT, + }, + }; + this.ws!.send(JSON.stringify(message)); + this.currentRoom = roomId; + } catch (error) { + console.error('加入房间失败:', error); + throw error; + } + } + + async leaveRoom() { + if (this.ws?.readyState === WebSocket.OPEN && this.currentRoom) { + const message: WSMessage = { + type: 'leave', + roomId: this.currentRoom, + data: { + text: `离开房间 ${this.currentRoom}`, + type: MessageType.TEXT, + }, + }; + this.ws.send(JSON.stringify(message)); + this.currentRoom = null; + } + } + + async sendMessage(messageParams: WSMessageParams) { + try { + await this.ensureConnected(); + + if (!this.currentRoom) { + throw new Error('请先加入房间'); + } + + const messageObj: WSMessage = { + type: 'message', + roomId: this.currentRoom, + data: { + text: messageParams.text || '', + type: messageParams.type || MessageType.TEXT, + fileUri: messageParams.fileUri, + }, + }; + this.ws!.send(JSON.stringify(messageObj)); + } catch (error) { + console.error('发送消息失败:', error); + throw error; + } + } + + disconnect() { + if (this.currentRoom) { + this.leaveRoom(); + } + if (this.ws && this.ws.readyState === WebSocket.OPEN) { + this.ws.close(); + } + this.cleanup(); + } + + getCurrentRoom() { + return this.currentRoom; + } + + isConnected() { + return this.ws?.readyState === WebSocket.OPEN; + } +} + +// 导出一个默认的 WebSocket 客户端实例 +export const wsClient = new WebSocketClient(); diff --git a/packages/client/src/websocket/index.ts b/packages/client/src/websocket/index.ts new file mode 100644 index 0000000..9fe5e08 --- /dev/null +++ b/packages/client/src/websocket/index.ts @@ -0,0 +1,3 @@ +export * from './client'; +export * from './useWebSocket'; +export * from './type'; diff --git a/packages/client/src/websocket/type.ts b/packages/client/src/websocket/type.ts new file mode 100644 index 0000000..a8e29b0 --- /dev/null +++ b/packages/client/src/websocket/type.ts @@ -0,0 +1,18 @@ +export enum MessageType { + TEXT = 'text', + IMAGE = 'image', + FILE = 'file', + RETRACT = 'retract', // 用于实现撤回 +} + +export interface WSMessageParams { + text?: string; + type?: MessageType; + fileUri?: string; +} + +export interface WSMessage { + type: 'join' | 'leave' | 'message' | 'system' | 'error'; + roomId?: string; + data: WSMessageParams; +} diff --git a/packages/client/src/websocket/useWebSocket.ts b/packages/client/src/websocket/useWebSocket.ts new file mode 100644 index 0000000..2b8ad2c --- /dev/null +++ b/packages/client/src/websocket/useWebSocket.ts @@ -0,0 +1,189 @@ +import { useCallback, useEffect, useState } from 'react'; +import { wsClient } from './client'; +import { WSMessage, WSMessageParams, MessageType } from './type'; + +interface UseWebSocketReturn { + // 状态 + messages: WSMessage[]; + currentRoom: string | null; + connecting: boolean; + + // 方法 + joinRoom: (roomId: string) => Promise; + leaveRoom: () => Promise; + sendMessage: (messageParams: WSMessageParams) => Promise; + + // 工具方法 + clearMessages: () => void; +} + +export const useWebSocket = (): UseWebSocketReturn => { + const [messages, setMessages] = useState([]); + const [currentRoom, setCurrentRoom] = useState(null); + const [connecting, setConnecting] = useState(false); + + // 消息处理器 + const messageHandler = useCallback((message: WSMessage) => { + setMessages((prev) => [...prev, message]); + }, []); + + // 初始化 WebSocket 连接 + useEffect(() => { + const initConnection = async () => { + try { + setConnecting(true); + await wsClient.connect(); + const unsubscribe = wsClient.onMessage(messageHandler); + return unsubscribe; + } catch (error) { + console.error('连接失败:', error); + setMessages((prev) => [ + ...prev, + { + type: 'error', + data: { + text: '连接失败,请刷新页面重试', + type: MessageType.TEXT, + }, + }, + ]); + } finally { + setConnecting(false); + } + }; + + const unsubscribePromise = initConnection(); + + return () => { + unsubscribePromise.then((unsubscribe) => unsubscribe?.()); + wsClient.disconnect(); + }; + }, [messageHandler]); + + // 加入房间 + const joinRoom = async (roomId: string): Promise => { + // 验证房间ID + if (!roomId.trim()) { + setMessages((prev) => [ + ...prev, + { + type: 'error', + data: { + text: '请输入有效的房间ID', + type: MessageType.TEXT, + }, + }, + ]); + return false; + } + + // 检查是否正在连接 + if (connecting) { + return false; + } + + try { + setConnecting(true); + await wsClient.joinRoom(roomId.trim()); + setCurrentRoom(roomId.trim()); + setMessages([]); // 清空消息历史 + return true; + } catch (error) { + console.error('加入房间失败:', error); + setMessages((prev) => [ + ...prev, + { + type: 'error', + data: { + text: '加入房间失败,请重试', + type: MessageType.TEXT, + }, + }, + ]); + return false; + } finally { + setConnecting(false); + } + }; + + // 离开房间 + const leaveRoom = async (): Promise => { + if (connecting) { + return false; + } + + try { + await wsClient.disconnect(); + setCurrentRoom(null); + setMessages([]); // 清空消息历史 + return true; + } catch (error) { + console.error('离开房间失败:', error); + return false; + } + }; + + // 发送消息 + const sendMessage = async (messageParams: WSMessageParams): Promise => { + // 验证消息内容 + if (!messageParams.text?.trim() && !messageParams.fileUri) { + return false; + } + + // 检查房间状态 + if (!currentRoom) { + setMessages((prev) => [ + ...prev, + { + type: 'error', + data: { + text: '请先加入房间', + type: MessageType.TEXT, + }, + }, + ]); + return false; + } + + // 检查连接状态 + if (connecting) { + return false; + } + + try { + await wsClient.sendMessage(messageParams); + return true; + } catch (error) { + console.error('发送消息失败:', error); + setMessages((prev) => [ + ...prev, + { + type: 'error', + data: { + text: '发送消息失败,请重试', + type: MessageType.TEXT, + }, + }, + ]); + return false; + } + }; + + // 清空消息 + const clearMessages = useCallback(() => { + setMessages([]); + }, []); + + return { + // 状态 + messages, + currentRoom, + connecting, + // 方法 + joinRoom, + leaveRoom, + sendMessage, + // 工具方法 + clearMessages, + }; +}; diff --git a/packages/db/.env.example b/packages/db/.env.example index 8183f56..72f3759 100755 --- a/packages/db/.env.example +++ b/packages/db/.env.example @@ -1 +1 @@ -DATABASE_URL="postgresql://root:Letusdoit000@localhost:5432/app?schema=public" \ No newline at end of file +DATABASE_URL="postgresql://nice:Letusdoit123@localhost:5432/db?schema=public" \ No newline at end of file From 1c3b7bdad015322c927d698003dca1b5f34757dc Mon Sep 17 00:00:00 2001 From: ditiqi Date: Tue, 27 May 2025 15:42:58 +0800 Subject: [PATCH 6/9] add --- apps/web/app/page.tsx | 4 ++-- packages/client/src/websocket/client.ts | 10 +++++----- packages/client/src/websocket/type.ts | 2 +- packages/client/src/websocket/useWebSocket.ts | 10 +++++----- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index e34e682..05d8458 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -100,9 +100,9 @@ export default function Home() {
{ setMessages((prev) => [ ...prev, { - type: 'error', + action: 'error', data: { text: '连接失败,请刷新页面重试', type: MessageType.TEXT, @@ -67,7 +67,7 @@ export const useWebSocket = (): UseWebSocketReturn => { setMessages((prev) => [ ...prev, { - type: 'error', + action: 'error', data: { text: '请输入有效的房间ID', type: MessageType.TEXT, @@ -93,7 +93,7 @@ export const useWebSocket = (): UseWebSocketReturn => { setMessages((prev) => [ ...prev, { - type: 'error', + action: 'error', data: { text: '加入房间失败,请重试', type: MessageType.TEXT, @@ -135,7 +135,7 @@ export const useWebSocket = (): UseWebSocketReturn => { setMessages((prev) => [ ...prev, { - type: 'error', + action: 'error', data: { text: '请先加入房间', type: MessageType.TEXT, @@ -158,7 +158,7 @@ export const useWebSocket = (): UseWebSocketReturn => { setMessages((prev) => [ ...prev, { - type: 'error', + action: 'error', data: { text: '发送消息失败,请重试', type: MessageType.TEXT, From dcd38d453dfd2d9945b498069a2f260e8f7dcf5a Mon Sep 17 00:00:00 2001 From: ditiqi Date: Tue, 27 May 2025 15:46:34 +0800 Subject: [PATCH 7/9] add --- apps/backend/src/socket.ts | 17 +++- apps/web/app/page.tsx | 163 +++----------------------------- apps/web/app/websocket/page.tsx | 161 +++++++++++++++++++++++++++++++ 3 files changed, 189 insertions(+), 152 deletions(-) create mode 100644 apps/web/app/websocket/page.tsx diff --git a/apps/backend/src/socket.ts b/apps/backend/src/socket.ts index 2ff3db4..27c5269 100644 --- a/apps/backend/src/socket.ts +++ b/apps/backend/src/socket.ts @@ -1,13 +1,22 @@ import { createBunWebSocket } from 'hono/bun'; import type { ServerWebSocket } from 'bun'; import { WSContext } from 'hono/ws'; -import { MessageType } from '../../../packages/client/src/websocket/type'; - +enum MessageType { + TEXT = 'text', + IMAGE = 'image', + FILE = 'file', + RETRACT = 'retract', // 用于实现撤回 +} +interface WSMessageParams { + text?: string; + type?: MessageType; + fileUri?: string; +} // 定义消息类型接口 interface WSMessage { type: 'join' | 'leave' | 'message'; roomId: string; - data: any; + data: WSMessageParams; } // 创建 WebSocket 实例并指定泛型类型 @@ -27,7 +36,7 @@ const wsHandler = upgradeWebSocket((c) => { }, onMessage(message, ws) { try { - const parsedMessage: WSMessage = JSON.parse(message.data); + const parsedMessage: WSMessage = JSON.parse(message.data as any); console.log('收到消息:', parsedMessage); switch (parsedMessage.type) { diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index 05d8458..b235c7d 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -1,161 +1,28 @@ 'use client'; -import { useHello, useTRPC, useWebSocket, MessageType } from '@repo/client'; +import { useHello, useTRPC } from '@repo/client'; import { useQuery } from '@tanstack/react-query'; -import { useRef, useState } from 'react'; +import Link from 'next/link'; export default function Home() { const trpc = useTRPC(); const { data, isLoading } = useQuery(trpc.user.getUser.queryOptions()); - const [message, setMessage] = useState(''); - const [roomId, setRoomId] = useState(''); - const messagesEndRef = useRef(null); - - // 使用 WebSocket hook - const { messages, currentRoom, connecting, joinRoom, leaveRoom, sendMessage } = useWebSocket(); - - // 滚动到底部 - const scrollToBottom = () => { - messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); - setTimeout(scrollToBottom, 100); - }; - - const handleJoinRoom = async () => { - const success = await joinRoom(roomId.trim()); - if (success) { - setRoomId(''); - } - }; - - const handleLeaveRoom = async () => { - await leaveRoom(); - }; - - const handleSendMessage = async () => { - const success = await sendMessage({ - text: message, - type: MessageType.TEXT, - }); - if (success) { - setMessage(''); - } - }; - - // 处理按回车发送消息 - const handleKeyPress = (e: React.KeyboardEvent) => { - if (e.key === 'Enter' && !e.shiftKey) { - e.preventDefault(); - handleSendMessage(); - } - }; return (
-

WebSocket 房间测试

- - {/* 房间管理 */} -
-

房间管理

- {!currentRoom ? ( -
- setRoomId(e.target.value)} - onKeyPress={handleKeyPress} - disabled={connecting} - className="border border-gray-300 rounded px-3 py-2 flex-1" - placeholder="输入房间ID..." - /> - -
- ) : ( -
- 当前房间: {currentRoom} - -
- )} +

首页

+
+
+

功能导航

+
    +
  • + + WebSocket 聊天室 + +
  • + {/* 可以在这里添加更多功能链接 */} +
+
- - {/* 消息显示区域 */} - {currentRoom && ( -
-
- {messages.map((msg, index) => ( -
-
- {msg.data.type === MessageType.IMAGE && msg.data.fileUri && ( - 图片消息 - )} - {msg.data.type === MessageType.FILE && msg.data.fileUri && ( - - 下载文件 - - )} - {msg.data.text &&
{msg.data.text}
} -
-
- ))} -
-
-
- setMessage(e.target.value)} - onKeyPress={handleKeyPress} - disabled={connecting} - className="border border-gray-300 rounded px-3 py-2 flex-1" - placeholder="输入消息..." - /> - -
-
- )} - - {!currentRoom && ( -
-

提示: 请先加入一个房间开始聊天

-

打开多个浏览器窗口,输入相同的房间ID来测试房间内通信

-
- )}
); } diff --git a/apps/web/app/websocket/page.tsx b/apps/web/app/websocket/page.tsx new file mode 100644 index 0000000..b044baa --- /dev/null +++ b/apps/web/app/websocket/page.tsx @@ -0,0 +1,161 @@ +'use client'; +import { useHello, useTRPC, useWebSocket, MessageType } from '@repo/client'; +import { useQuery } from '@tanstack/react-query'; +import { useRef, useState } from 'react'; + +export default function WebSocketPage() { + const trpc = useTRPC(); + const { data, isLoading } = useQuery(trpc.user.getUser.queryOptions()); + const [message, setMessage] = useState(''); + const [roomId, setRoomId] = useState(''); + const messagesEndRef = useRef(null); + + // 使用 WebSocket hook + const { messages, currentRoom, connecting, joinRoom, leaveRoom, sendMessage } = useWebSocket(); + + // 滚动到底部 + const scrollToBottom = () => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + setTimeout(scrollToBottom, 100); + }; + + const handleJoinRoom = async () => { + const success = await joinRoom(roomId.trim()); + if (success) { + setRoomId(''); + } + }; + + const handleLeaveRoom = async () => { + await leaveRoom(); + }; + + const handleSendMessage = async () => { + const success = await sendMessage({ + text: message, + type: MessageType.TEXT, + }); + if (success) { + setMessage(''); + } + }; + + // 处理按回车发送消息 + const handleKeyPress = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleSendMessage(); + } + }; + + return ( +
+

WebSocket 房间测试

+ + {/* 房间管理 */} +
+

房间管理

+ {!currentRoom ? ( +
+ setRoomId(e.target.value)} + onKeyPress={handleKeyPress} + disabled={connecting} + className="border border-gray-300 rounded px-3 py-2 flex-1" + placeholder="输入房间ID..." + /> + +
+ ) : ( +
+ 当前房间: {currentRoom} + +
+ )} +
+ + {/* 消息显示区域 */} + {currentRoom && ( +
+
+ {messages.map((msg, index) => ( +
+
+ {msg.data.type === MessageType.IMAGE && msg.data.fileUri && ( + 图片消息 + )} + {msg.data.type === MessageType.FILE && msg.data.fileUri && ( + + 下载文件 + + )} + {msg.data.text &&
{msg.data.text}
} +
+
+ ))} +
+
+
+ setMessage(e.target.value)} + onKeyPress={handleKeyPress} + disabled={connecting} + className="border border-gray-300 rounded px-3 py-2 flex-1" + placeholder="输入消息..." + /> + +
+
+ )} + + {!currentRoom && ( +
+

提示: 请先加入一个房间开始聊天

+

打开多个浏览器窗口,输入相同的房间ID来测试房间内通信

+
+ )} +
+ ); +} From 4a70b731b8cc501f42ae8e3f704ccfa72cc58534 Mon Sep 17 00:00:00 2001 From: ditiqi Date: Tue, 27 May 2025 16:56:50 +0800 Subject: [PATCH 8/9] add --- apps/backend/package.json | 53 +- packages/tus/package.json | 36 + packages/tus/src/handlers/BaseHandler.ts | 364 +++++ packages/tus/src/handlers/DeleteHandler.ts | 64 + packages/tus/src/handlers/GetHandler.ts | 189 +++ packages/tus/src/handlers/HeadHandler.ts | 90 ++ packages/tus/src/handlers/OptionsHandler.ts | 61 + packages/tus/src/handlers/PatchHandler.ts | 256 ++++ packages/tus/src/handlers/PostHandler.ts | 257 ++++ packages/tus/src/index.ts | 5 + packages/tus/src/lockers/MemoryLocker.ts | 145 ++ packages/tus/src/lockers/index.ts | 1 + packages/tus/src/server.ts | 519 +++++++ packages/tus/src/store/file-store/index.ts | 230 +++ packages/tus/src/store/index.ts | 2 + packages/tus/src/store/s3-store/index.ts | 803 ++++++++++ packages/tus/src/types.ts | 211 +++ packages/tus/src/utils/constants.ts | 132 ++ packages/tus/src/utils/index.ts | 3 + .../tus/src/utils/kvstores/FileKvStore.ts | 94 ++ .../tus/src/utils/kvstores/IoRedisKvStore.ts | 54 + .../tus/src/utils/kvstores/MemoryKvStore.ts | 26 + .../tus/src/utils/kvstores/RedisKvStore.ts | 94 ++ packages/tus/src/utils/kvstores/Types.ts | 43 + packages/tus/src/utils/kvstores/index.ts | 5 + packages/tus/src/utils/models/Context.ts | 14 + packages/tus/src/utils/models/DataStore.ts | 72 + packages/tus/src/utils/models/Locker.ts | 12 + packages/tus/src/utils/models/Metadata.ts | 103 ++ .../tus/src/utils/models/StreamLimiter.ts | 54 + .../tus/src/utils/models/StreamSplitter.ts | 183 +++ packages/tus/src/utils/models/Uid.ts | 21 + packages/tus/src/utils/models/Upload.ts | 72 + packages/tus/src/utils/models/index.ts | 8 + .../tus/src/validators/HeaderValidator.ts | 138 ++ packages/tus/tsconfig.json | 40 + packages/tus/tsup.config.ts | 10 + pnpm-lock.yaml | 1344 +++++++++++++++++ 38 files changed, 5782 insertions(+), 26 deletions(-) create mode 100644 packages/tus/package.json create mode 100644 packages/tus/src/handlers/BaseHandler.ts create mode 100644 packages/tus/src/handlers/DeleteHandler.ts create mode 100644 packages/tus/src/handlers/GetHandler.ts create mode 100644 packages/tus/src/handlers/HeadHandler.ts create mode 100644 packages/tus/src/handlers/OptionsHandler.ts create mode 100644 packages/tus/src/handlers/PatchHandler.ts create mode 100644 packages/tus/src/handlers/PostHandler.ts create mode 100644 packages/tus/src/index.ts create mode 100644 packages/tus/src/lockers/MemoryLocker.ts create mode 100644 packages/tus/src/lockers/index.ts create mode 100644 packages/tus/src/server.ts create mode 100644 packages/tus/src/store/file-store/index.ts create mode 100644 packages/tus/src/store/index.ts create mode 100644 packages/tus/src/store/s3-store/index.ts create mode 100644 packages/tus/src/types.ts create mode 100644 packages/tus/src/utils/constants.ts create mode 100644 packages/tus/src/utils/index.ts create mode 100644 packages/tus/src/utils/kvstores/FileKvStore.ts create mode 100644 packages/tus/src/utils/kvstores/IoRedisKvStore.ts create mode 100644 packages/tus/src/utils/kvstores/MemoryKvStore.ts create mode 100644 packages/tus/src/utils/kvstores/RedisKvStore.ts create mode 100644 packages/tus/src/utils/kvstores/Types.ts create mode 100644 packages/tus/src/utils/kvstores/index.ts create mode 100644 packages/tus/src/utils/models/Context.ts create mode 100644 packages/tus/src/utils/models/DataStore.ts create mode 100644 packages/tus/src/utils/models/Locker.ts create mode 100644 packages/tus/src/utils/models/Metadata.ts create mode 100644 packages/tus/src/utils/models/StreamLimiter.ts create mode 100644 packages/tus/src/utils/models/StreamSplitter.ts create mode 100644 packages/tus/src/utils/models/Uid.ts create mode 100644 packages/tus/src/utils/models/Upload.ts create mode 100644 packages/tus/src/utils/models/index.ts create mode 100644 packages/tus/src/validators/HeaderValidator.ts create mode 100644 packages/tus/tsconfig.json create mode 100644 packages/tus/tsup.config.ts diff --git a/apps/backend/package.json b/apps/backend/package.json index 6bb44a7..c08ed25 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -1,27 +1,28 @@ { - "name": "backend", - "scripts": { - "dev": "bun run --hot src/index.ts" - }, - "dependencies": { - "@elastic/elasticsearch": "^9.0.2", - "@hono/trpc-server": "^0.3.4", - "@hono/zod-validator": "^0.5.0", - "@repo/db": "workspace:*", - "@trpc/server": "11.1.2", - "@types/oidc-provider": "^9.1.0", - "hono": "^4.7.10", - "ioredis": "5.4.1", - "jose": "^6.0.11", - "minio": "7.1.3", - "nanoid": "^5.1.5", - "node-cron": "^4.0.7", - "oidc-provider": "^9.1.1", - "superjson": "^2.2.2", - "zod": "^3.25.23" - }, - "devDependencies": { - "@types/bun": "latest", - "@types/node": "^22.15.21" - } -} \ No newline at end of file + "name": "backend", + "scripts": { + "dev": "bun run --hot src/index.ts" + }, + "dependencies": { + "@elastic/elasticsearch": "^9.0.2", + "@hono/trpc-server": "^0.3.4", + "@hono/zod-validator": "^0.5.0", + "@repo/db": "workspace:*", + "@repo/tus": "workspace:*", + "@trpc/server": "11.1.2", + "@types/oidc-provider": "^9.1.0", + "hono": "^4.7.10", + "ioredis": "5.4.1", + "jose": "^6.0.11", + "minio": "7.1.3", + "nanoid": "^5.1.5", + "node-cron": "^4.0.7", + "oidc-provider": "^9.1.1", + "superjson": "^2.2.2", + "zod": "^3.25.23" + }, + "devDependencies": { + "@types/bun": "latest", + "@types/node": "^22.15.21" + } +} diff --git a/packages/tus/package.json b/packages/tus/package.json new file mode 100644 index 0000000..c0d332c --- /dev/null +++ b/packages/tus/package.json @@ -0,0 +1,36 @@ +{ + "name": "@repo/tus", + "version": "1.0.0", + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "private": true, + "scripts": { + "build": "tsup", + "dev": "tsup --watch", + "dev-static": "tsup --no-watch", + "clean": "rimraf dist", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@aws-sdk/client-s3": "^3.723.0", + "@shopify/semaphore": "^3.1.0", + "debug": "^4.4.0", + "lodash.throttle": "^4.1.1", + "multistream": "^4.1.0" + }, + "devDependencies": { + "@types/debug": "^4.1.12", + "@types/lodash.throttle": "^4.1.9", + "@types/multistream": "^4.1.3", + "@types/node": "^20.3.1", + "concurrently": "^8.0.0", + "ioredis": "^5.4.1", + "rimraf": "^6.0.1", + "should": "^13.2.3", + "ts-node": "^10.9.1", + "tsup": "^8.3.5", + "typescript": "^5.5.4", + "@redis/client": "^1.6.0" + } +} diff --git a/packages/tus/src/handlers/BaseHandler.ts b/packages/tus/src/handlers/BaseHandler.ts new file mode 100644 index 0000000..2315295 --- /dev/null +++ b/packages/tus/src/handlers/BaseHandler.ts @@ -0,0 +1,364 @@ +import EventEmitter from 'node:events' +import stream from 'node:stream/promises' +import { addAbortSignal, PassThrough } from 'node:stream' +import type http from 'node:http' + +import type { ServerOptions } from '../types' +import throttle from 'lodash.throttle' +import { CancellationContext, DataStore, ERRORS, EVENTS, StreamLimiter, Upload } from '../utils' + +/** + * 正则表达式,用于从请求 URL 中提取文件 ID。 + * 该正则表达式匹配 URL 中最后一个斜杠后的所有字符,直到字符串结束。 + * - `([^/]+)`:捕获组,匹配一个或多个非斜杠字符。 + * - `\/?$`:匹配可选的斜杠,并确保匹配到字符串的末尾。 + * 示例: + * - 输入 `/files/12345`,匹配结果为 `12345`。 + * - 输入 `/files/12345/`,匹配结果为 `12345`。 + */ +const reExtractFileID = /([^/]+)\/?$/ + +/** + * 正则表达式,用于从 HTTP 请求头中的 `forwarded` 字段提取主机名。 + * 该正则表达式匹配 `host=""` 或 `host=` 格式的字符串,并提取 `` 部分。 + * - `host="?`:匹配 `host=` 或 `host="`。 + * - `([^";]+)`:捕获组,匹配一个或多个非分号和双引号的字符。 + * 示例: + * - 输入 `host="example.com"`,匹配结果为 `example.com`。 + * - 输入 `host=example.com`,匹配结果为 `example.com`。 + */ +const reForwardedHost = /host="?([^";]+)/ + +/** + * 正则表达式,用于从 HTTP 请求头中的 `forwarded` 字段提取协议(如 `http` 或 `https`)。 + * 该正则表达式匹配 `proto=` 格式的字符串,并提取 `` 部分。 + * - `proto=`:匹配 `proto=` 字符串。 + * - `(https?)`:捕获组,匹配 `http` 或 `https`。 + * 示例: + * - 输入 `proto=https`,匹配结果为 `https`。 + * - 输入 `proto=http`,匹配结果为 `http`。 + */ +const reForwardedProto = /proto=(https?)/ + +/** + * BaseHandler 类是一个基础处理器,用于处理 TUS 协议的上传请求。 + * 它继承自 Node.js 的 EventEmitter,允许发出和监听事件。 + */ +export class BaseHandler extends EventEmitter { + options: ServerOptions + store: DataStore + + /** + * 构造函数,初始化 BaseHandler 实例。 + * @param store - 数据存储对象,用于处理上传数据的存储。 + * @param options - 服务器配置选项。 + * @throws 如果未提供 store 参数,则抛出错误。 + */ + constructor(store: DataStore, options: ServerOptions) { + super() + if (!store) { + throw new Error('Store must be defined') + } + + this.store = store + this.options = options + } + + /** + * 向客户端发送 HTTP 响应。 + * @param res - HTTP 响应对象。 + * @param status - HTTP 状态码。 + * @param headers - 响应头对象。 + * @param body - 响应体内容。 + * @returns 返回结束的响应对象。 + */ + write(res: http.ServerResponse, status: number, headers = {}, body = '') { + if (status !== 204) { + // @ts-expect-error not explicitly typed but possible + headers['Content-Length'] = Buffer.byteLength(body, 'utf8') + } + + res.writeHead(status, headers) + res.write(body) + return res.end() + } + + /** + * 生成上传文件的 URL。 + * @param req - HTTP 请求对象。 + * @param id - 文件 ID。 + * @returns 返回生成的 URL。 + */ + generateUrl(req: http.IncomingMessage, id: string) { + const path = this.options.path === '/' ? '' : this.options.path + if (this.options.generateUrl) { + // 使用用户定义的 generateUrl 函数生成 URL + const { proto, host } = this.extractHostAndProto(req) + return this.options.generateUrl(req, { + proto, + host, + path: path, + id, + }) + } + + // 默认实现 + if (this.options.relativeLocation) { + return `${path}/${id}` + } + + const { proto, host } = this.extractHostAndProto(req) + + return `${proto}://${host}${path}/${id}` + } + + /** + * 从请求中提取文件 ID。 + * @param req - HTTP 请求对象。 + * @returns 返回提取的文件 ID,如果未找到则返回 undefined。 + */ + getFileIdFromRequest(req: http.IncomingMessage) { + const match = reExtractFileID.exec(req.url as string) + + if (this.options.getFileIdFromRequest) { + const lastPath = match ? decodeURIComponent(match[1]) : undefined + return this.options.getFileIdFromRequest(req, lastPath) + } + + if (!match || this.options.path.includes(match[1])) { + return + } + + return decodeURIComponent(match[1]) + } + + /** + * 从 HTTP 请求中提取主机名和协议信息。 + * 该方法首先检查是否启用了尊重转发头(respectForwardedHeaders)选项, + * 如果启用,则从请求头中提取转发的主机名和协议信息。 + * 如果未启用或未找到转发信息,则使用请求头中的主机名和默认协议(http)。 + * + * @param req - HTTP 请求对象,包含请求头等信息。 + * @returns 返回包含主机名和协议的对象。 + */ + protected extractHostAndProto(req: http.IncomingMessage) { + let proto: string | undefined + let host: string | undefined + + // 如果启用了尊重转发头选项 + if (this.options.respectForwardedHeaders) { + // 从请求头中获取 forwarded 字段 + const forwarded = req.headers.forwarded as string | undefined + if (forwarded) { + // 使用正则表达式从 forwarded 字段中提取主机名和协议 + host ??= reForwardedHost.exec(forwarded)?.[1] + proto ??= reForwardedProto.exec(forwarded)?.[1] + } + + // 从请求头中获取 x-forwarded-host 和 x-forwarded-proto 字段 + const forwardHost = req.headers['x-forwarded-host'] + const forwardProto = req.headers['x-forwarded-proto'] + + // 检查 x-forwarded-proto 是否为有效的协议(http 或 https) + // @ts-expect-error we can pass undefined + if (['http', 'https'].includes(forwardProto)) { + proto ??= forwardProto as string + } + + // 如果 x-forwarded-host 存在,则使用它作为主机名 + host ??= forwardHost as string + } + + // 如果未从转发头中获取到主机名,则使用请求头中的 host 字段 + host ??= req.headers.host + // 如果未从转发头中获取到协议,则默认使用 http + proto ??= 'http' + + // 返回包含主机名和协议的对象 + return { host: host as string, proto } + } + + /** + * 获取锁对象。 + * @param req - HTTP 请求对象。 + * @returns 返回锁对象。 + */ + protected async getLocker(req: http.IncomingMessage) { + if (typeof this.options.locker === 'function') { + return this.options.locker(req) + } + return this.options.locker + } + + /** + * 获取锁并锁定资源。 + * @param req - HTTP 请求对象。 + * @param id - 文件 ID。 + * @param context - 取消上下文对象。 + * @returns 返回锁对象。 + */ + protected async acquireLock( + req: http.IncomingMessage, + id: string, + context: CancellationContext + ) { + const locker = await this.getLocker(req) + + const lock = locker.newLock(id) + + await lock.lock(() => { + context.cancel() + }) + + return lock + } + + + /** + * 将请求体数据写入存储。 + * 该方法负责将 HTTP 请求体中的数据流式传输到存储系统中,同时处理取消操作、错误处理和进度更新。 + * + * @param req - HTTP 请求对象,包含请求体数据流。 + * @param upload - 上传对象,包含上传的元数据(如文件 ID、偏移量等)。 + * @param maxFileSize - 允许的最大文件大小,用于限制写入的数据量。 + * @param context - 取消上下文对象,用于处理取消操作。 + * @returns 返回一个 Promise,解析为写入的字节数。 + */ + protected writeToStore( + req: http.IncomingMessage, + upload: Upload, + maxFileSize: number, + context: CancellationContext + ) { + // 使用 Promise 包装异步操作,以便更好地处理取消和错误。 + // biome-ignore lint/suspicious/noAsyncPromiseExecutor: + return new Promise(async (resolve, reject) => { + // 检查是否已被取消,如果已取消则直接拒绝 Promise。 + if (context.signal.aborted) { + reject(ERRORS.ABORTED) + return + } + + // 创建一个 PassThrough 流作为代理,用于管理请求流。 + // PassThrough 流是一个透明的流,它允许数据通过而不进行任何修改。 + // 使用代理流的好处是可以在不影响原始请求流的情况下中止写入过程。 + const proxy = new PassThrough() + // 将取消信号与代理流关联,以便在取消时自动中止流。 + addAbortSignal(context.signal, proxy) + // 监听代理流的错误事件,处理流中的错误。 + proxy.on('error', (err) => { + // 取消请求流与代理流的管道连接。 + req.unpipe(proxy) + // 如果错误是 AbortError,则返回 ABORTED 错误,否则返回原始错误。 + reject(err.name === 'AbortError' ? ERRORS.ABORTED : err) + }) + // 使用 throttle 函数创建一个节流函数,用于定期触发 POST_RECEIVE_V2 事件。 + // 该事件用于通知上传进度,避免频繁触发事件导致性能问题。 + const postReceive = throttle( + (offset: number) => { + // 触发 POST_RECEIVE_V2 事件,传递当前上传的偏移量。 + this.emit(EVENTS.POST_RECEIVE_V2, req, { ...upload, offset }) + }, + // 设置节流的时间间隔,避免事件触发过于频繁。 + this.options.postReceiveInterval, + { leading: false } + ) + // 临时变量,用于跟踪当前写入的偏移量。 + let tempOffset = upload.offset + // 监听代理流的 data 事件,每当有数据块通过时更新偏移量并触发进度事件。 + proxy.on('data', (chunk: Buffer) => { + tempOffset += chunk.byteLength + postReceive(tempOffset) + }) + // 监听请求流的 error 事件,处理请求流中的错误。 + req.on('error', () => { + // 如果代理流未关闭,则优雅地结束流,以便将剩余的字节作为 incompletePart 上传到存储。 + if (!proxy.closed) { + proxy.end() + } + }) + // 使用 stream.pipeline 将请求流通过代理流和 StreamLimiter 传输到存储系统。 + // StreamLimiter 用于限制写入的数据量,确保不超过最大文件大小。 + stream + .pipeline( + // 将请求流通过代理流传输。 + req.pipe(proxy), + // 使用 StreamLimiter 限制写入的数据量。 + new StreamLimiter(maxFileSize), + // 将数据流写入存储系统。 + async (stream) => { + return this.store.write(stream as StreamLimiter, upload.id, upload.offset) + } + ) + // 如果管道操作成功,则解析 Promise 并返回写入的字节数。 + .then(resolve) + // 如果管道操作失败,则拒绝 Promise 并返回错误。 + .catch(reject) + }) + } + + /** + * 获取配置的最大文件大小。 + * @param req - HTTP 请求对象。 + * @param id - 文件 ID。 + * @returns 返回配置的最大文件大小。 + */ + getConfiguredMaxSize(req: http.IncomingMessage, id: string | null) { + if (typeof this.options.maxSize === 'function') { + return this.options.maxSize(req, id) + } + return this.options.maxSize ?? 0 + } + + /** + * 计算上传请求体的最大允许大小。 + * 该函数考虑了服务器配置的最大大小和上传的具体情况,例如大小是延迟的还是固定的。 + * @param req - HTTP 请求对象。 + * @param file - 上传对象。 + * @param configuredMaxSize - 配置的最大大小。 + * @returns 返回计算出的最大请求体大小。 + * @throws 如果上传大小超过允许的最大大小,则抛出 ERRORS.ERR_SIZE_EXCEEDED 错误。 + */ + async calculateMaxBodySize( + req: http.IncomingMessage, + file: Upload, + configuredMaxSize?: number + ) { + // 如果未明确提供,则使用服务器配置的最大大小。 + configuredMaxSize ??= await this.getConfiguredMaxSize(req, file.id) + + // 从请求中解析 Content-Length 头(如果未设置,则默认为 0)。 + const length = Number.parseInt(req.headers['content-length'] || '0', 10) + const offset = file.offset + + const hasContentLengthSet = req.headers['content-length'] !== undefined + const hasConfiguredMaxSizeSet = configuredMaxSize > 0 + + if (file.sizeIsDeferred) { + // 对于延迟大小的上传,如果不是分块传输,则检查配置的最大大小。 + if ( + hasContentLengthSet && + hasConfiguredMaxSizeSet && + offset + length > configuredMaxSize + ) { + throw ERRORS.ERR_SIZE_EXCEEDED + } + + if (hasConfiguredMaxSizeSet) { + return configuredMaxSize - offset + } + return Number.MAX_SAFE_INTEGER + } + + // 检查上传是否适合文件的大小(当大小不是延迟的时)。 + if (offset + length > (file.size || 0)) { + throw ERRORS.ERR_SIZE_EXCEEDED + } + + if (hasContentLengthSet) { + return length + } + + return (file.size || 0) - offset + } +} \ No newline at end of file diff --git a/packages/tus/src/handlers/DeleteHandler.ts b/packages/tus/src/handlers/DeleteHandler.ts new file mode 100644 index 0000000..e0012f3 --- /dev/null +++ b/packages/tus/src/handlers/DeleteHandler.ts @@ -0,0 +1,64 @@ +import { CancellationContext, ERRORS, EVENTS } from '../utils' +import { BaseHandler } from './BaseHandler' + +import type http from 'node:http' + +export class DeleteHandler extends BaseHandler { + /** + * 处理DELETE请求的核心方法 + * @param req HTTP请求对象,包含请求头、请求体等信息 + * @param res HTTP响应对象,用于返回响应状态和数据 + * @param context 取消上下文,用于处理请求取消逻辑 + * @returns 返回处理后的HTTP响应对象 + * + * 技术原理: + * - 通过HTTP DELETE方法删除指定资源 + * - 使用锁机制保证并发安全 + * - 支持自定义请求处理钩子 + * + * 优化建议: + * - 可考虑添加批量删除支持 + * - 可优化锁机制,使用更细粒度的锁 + */ + async send( + req: http.IncomingMessage, + res: http.ServerResponse, + context: CancellationContext + ) { + // 从请求中提取文件ID + const id = this.getFileIdFromRequest(req) + // 文件ID不存在时抛出异常 + if (!id) { + throw ERRORS.FILE_NOT_FOUND + } + + // 执行自定义的请求处理钩子 + if (this.options.onIncomingRequest) { + await this.options.onIncomingRequest(req, res, id) + } + + // 获取文件操作锁,保证并发安全 + const lock = await this.acquireLock(req, id, context) + try { + // 检查是否禁止删除已完成的上传 + if (this.options.disableTerminationForFinishedUploads) { + const upload = await this.store.getUpload(id) + // 上传已完成时抛出异常 + if (upload.offset === upload.size) { + throw ERRORS.INVALID_TERMINATION + } + } + + // 从存储中删除指定文件 + await this.store.remove(id) + } finally { + // 无论成功与否,最终都要释放锁 + await lock.unlock() + } + // 返回204 No Content响应 + const writtenRes = this.write(res, 204, {}) + // 触发删除完成事件 + this.emit(EVENTS.POST_TERMINATE, req, writtenRes, id) + return writtenRes + } +} diff --git a/packages/tus/src/handlers/GetHandler.ts b/packages/tus/src/handlers/GetHandler.ts new file mode 100644 index 0000000..4dc3e8a --- /dev/null +++ b/packages/tus/src/handlers/GetHandler.ts @@ -0,0 +1,189 @@ +/** + * 文件模块:GetHandler.ts + * 功能描述:负责处理HTTP GET请求,提供文件下载和流式传输功能 + * 使用场景:适用于需要实现文件下载、流媒体播放等功能的Web服务 + */ + +import stream from 'node:stream' +import { BaseHandler } from './BaseHandler' +import type http from 'node:http' +import type { RouteHandler } from '../types' +import { ERRORS, Upload } from '../utils' +/** + * GetHandler类 + * 核心功能:处理GET请求,支持自定义路径处理和文件流传输 + * 设计模式:基于策略模式实现路径处理函数的动态注册 + * 使用示例: + * const handler = new GetHandler() + * handler.registerPath('/custom', customHandler) + */ +export class GetHandler extends BaseHandler { + // 使用Map存储路径与处理函数的映射关系,提供O(1)的查找时间复杂度 + paths: Map = new Map() + /** + * 正则表达式用于验证MIME类型是否符合RFC1341规范 + * 支持带参数的MIME类型,如:text/plain; charset=utf-8 + * 时间复杂度:O(n),n为字符串长度 + * 优化建议:可考虑预编译正则表达式以提高性能 + */ + reMimeType = + // biome-ignore lint/suspicious/noControlCharactersInRegex: it's fine + /^(?:application|audio|example|font|haptics|image|message|model|multipart|text|video|x-(?:[0-9A-Za-z!#$%&'*+.^_`|~-]+))\/([0-9A-Za-z!#$%&'*+.^_`|~-]+)((?:[ ]*;[ ]*[0-9A-Za-z!#$%&'*+.^_`|~-]+=(?:[0-9A-Za-z!#$%&'*+.^_`|~-]+|"(?:[^"\\]|\.)*"))*)$/ + /** + * 允许浏览器内联渲染的MIME类型白名单 + * 使用Set数据结构,提供O(1)的查找时间复杂度 + * 优化建议:可根据实际业务需求动态调整白名单 + */ + mimeInlineBrowserWhitelist = new Set([ + 'text/plain', + 'image/png', + 'image/jpeg', + 'image/gif', + 'image/bmp', + 'image/webp', + 'audio/wave', + 'audio/wav', + 'audio/x-wav', + 'audio/x-pn-wav', + 'audio/webm', + 'audio/ogg', + 'video/mp4', + 'video/webm', + 'video/ogg', + 'application/ogg', + ]) + /** + * 注册路径处理函数 + * 功能描述:将路径与处理函数进行绑定 + * 输入参数: + * - path: 请求路径 + * - handler: 处理函数 + * 时间复杂度:O(1) + * 优化建议:可添加路径冲突检测机制 + */ + registerPath(path: string, handler: RouteHandler): void { + this.paths.set(path, handler) + } + /** + * 发送文件流 + * 功能描述:处理GET请求,返回文件流或执行自定义处理 + * 输入参数: + * - req: HTTP请求对象 + * - res: HTTP响应对象 + * 返回值:可写流或void + * 异常处理:抛出FILE_NOT_FOUND错误 + * 时间复杂度:O(n),n为文件大小 + * 优化建议:可添加流控机制防止内存溢出 + */ + async send( + req: http.IncomingMessage, + res: http.ServerResponse + // biome-ignore lint/suspicious/noConfusingVoidType: it's fine + ): Promise { + // 检查是否注册了自定义路径处理 + if (this.paths.has(req.url as string)) { + const handler = this.paths.get(req.url as string) as RouteHandler + return handler(req, res) + } + + // 检查数据存储是否支持读取操作 + if (!('read' in this.store)) { + throw ERRORS.FILE_NOT_FOUND + } + + // 从请求中提取文件ID + const id = this.getFileIdFromRequest(req) + if (!id) { + throw ERRORS.FILE_NOT_FOUND + } + + // 执行自定义请求处理回调 + if (this.options.onIncomingRequest) { + await this.options.onIncomingRequest(req, res, id) + } + + // 获取文件上传状态 + const stats = await this.store.getUpload(id) + + // 验证文件是否完整 + if (!stats || stats.offset !== stats.size) { + throw ERRORS.FILE_NOT_FOUND + } + + // 处理内容类型和内容处置头 + const { contentType, contentDisposition } = this.filterContentType(stats) + + // 创建文件读取流 + // @ts-expect-error exists if supported + const file_stream = await this.store.read(id) + const headers = { + 'Content-Length': stats.offset, + 'Content-Type': contentType, + 'Content-Disposition': contentDisposition, + } + res.writeHead(200, headers) + // 使用流管道传输数据 + return stream.pipeline(file_stream, res, () => { + // 忽略流传输错误 + }) + } + + /** + * 过滤内容类型 + * 功能描述:根据文件类型生成Content-Type和Content-Disposition头 + * 输入参数: + * - stats: 文件上传状态对象 + * 返回值:包含contentType和contentDisposition的对象 + * 时间复杂度:O(1) + * 优化建议:可添加更多MIME类型验证规则 + */ + filterContentType(stats: Upload): { + contentType: string + contentDisposition: string + } { + let contentType: string + let contentDisposition: string + + // 从元数据中提取文件类型和名称 + const { filetype, filename } = stats.metadata ?? {} + + // 验证文件类型格式 + if (filetype && this.reMimeType.test(filetype)) { + contentType = filetype + + // 检查是否在白名单中 + if (this.mimeInlineBrowserWhitelist.has(filetype)) { + contentDisposition = 'inline' + } else { + contentDisposition = 'attachment' + } + } else { + // 使用默认类型并强制下载 + contentType = 'application/octet-stream' + contentDisposition = 'attachment' + } + + // 添加文件名到内容处置头 + if (filename) { + contentDisposition += `; filename=${this.quote(filename)}` + } + + return { + contentType, + contentDisposition, + } + } + + /** + * 字符串转义 + * 功能描述:将字符串转换为带引号的字符串字面量 + * 输入参数: + * - value: 需要转义的字符串 + * 返回值:转义后的字符串 + * 时间复杂度:O(n),n为字符串长度 + * 优化建议:可考虑使用正则表达式优化替换操作 + */ + quote(value: string) { + return `"${value.replace(/"/g, '\\"')}"` + } +} diff --git a/packages/tus/src/handlers/HeadHandler.ts b/packages/tus/src/handlers/HeadHandler.ts new file mode 100644 index 0000000..8f4605c --- /dev/null +++ b/packages/tus/src/handlers/HeadHandler.ts @@ -0,0 +1,90 @@ +/** + * 模块:HeadHandler + * 功能:处理TUS协议的HEAD请求,用于获取上传文件的状态信息 + * 使用场景:在文件分片上传过程中,客户端需要定期查询上传进度时使用 + */ +import { CancellationContext, ERRORS, Upload, Metadata } from '../utils' +import { BaseHandler } from './BaseHandler' +import type http from 'node:http' + +/** + * HeadHandler类 + * 核心功能:处理TUS协议的HEAD请求,返回文件上传的元数据和进度信息 + * 设计模式:继承自BaseHandler,采用模板方法模式 + * 使用示例: + * const handler = new HeadHandler(store, options) + * await handler.send(req, res, context) + */ +export class HeadHandler extends BaseHandler { + /** + * 处理HEAD请求的核心方法 + * @param req HTTP请求对象 + * @param res HTTP响应对象 + * @param context 取消操作的上下文 + * @returns 返回HTTP响应 + * @throws ERRORS.FILE_NOT_FOUND 文件ID不存在时抛出 + * @throws ERRORS.FILE_NO_LONGER_EXISTS 文件已过期时抛出 + */ + async send( + req: http.IncomingMessage, + res: http.ServerResponse, + context: CancellationContext + ) { + // 从请求中提取文件ID + const id = this.getFileIdFromRequest(req) + if (!id) { + throw ERRORS.FILE_NOT_FOUND + } + + // 执行自定义的请求预处理逻辑 + if (this.options.onIncomingRequest) { + await this.options.onIncomingRequest(req, res, id) + } + + // 获取文件锁,防止并发操作 + const lock = await this.acquireLock(req, id, context) + + let file: Upload + try { + // 从存储中获取文件上传信息 + file = await this.store.getUpload(id) + } finally { + // 无论成功与否,都释放锁 + await lock.unlock() + } + + // 检查文件是否已过期 + const now = new Date() + if ( + this.store.hasExtension('expiration') && + this.store.getExpiration() > 0 && + file.creation_date && + now > new Date(new Date(file.creation_date).getTime() + this.store.getExpiration()) + ) { + throw ERRORS.FILE_NO_LONGER_EXISTS + } + + // 设置响应头,防止缓存 + res.setHeader('Cache-Control', 'no-store') + // 返回当前上传偏移量 + res.setHeader('Upload-Offset', file.offset) + + // 处理文件大小信息 + if (file.sizeIsDeferred) { + // 如果文件大小未知,设置延迟长度标志 + res.setHeader('Upload-Defer-Length', '1') + } else { + // 如果文件大小已知,返回实际大小 + res.setHeader('Upload-Length', file.size as number) + } + + // 处理文件元数据 + if (file.metadata !== undefined) { + // 将元数据转换为字符串格式返回 + res.setHeader('Upload-Metadata', Metadata.stringify(file.metadata) as string) + } + + // 结束响应 + return res.end() + } +} diff --git a/packages/tus/src/handlers/OptionsHandler.ts b/packages/tus/src/handlers/OptionsHandler.ts new file mode 100644 index 0000000..fb001dd --- /dev/null +++ b/packages/tus/src/handlers/OptionsHandler.ts @@ -0,0 +1,61 @@ +/** + * 模块:OptionsHandler + * 功能:处理TUS协议的OPTIONS请求,返回服务器支持的TUS协议版本、扩展功能等信息 + * 使用场景:在TUS文件上传协议中,客户端通过OPTIONS请求获取服务器支持的功能和配置 + */ + +import { ALLOWED_METHODS, HEADERS, MAX_AGE } from '../utils' +import { BaseHandler } from './BaseHandler' + +import type http from 'node:http' + +/** + * OptionsHandler类 + * 核心功能:处理TUS协议的OPTIONS请求,返回服务器支持的配置信息 + * 设计模式:继承自BaseHandler,采用模板方法模式,复用基础处理逻辑 + * 使用示例: + * const handler = new OptionsHandler(store, options) + * handler.send(req, res) + */ +export class OptionsHandler extends BaseHandler { + /** + * 处理OPTIONS请求并发送响应 + * @param req - HTTP请求对象,包含客户端请求信息 + * @param res - HTTP响应对象,用于向客户端返回数据 + * @returns Promise 无返回值 + * 功能详细描述: + * 1. 获取服务器配置的最大文件大小 + * 2. 设置TUS协议版本、扩展功能和最大文件大小等响应头 + * 3. 配置CORS相关头信息 + * 4. 返回204 No Content状态码 + * 异常处理:继承自BaseHandler的异常处理机制 + */ + async send(req: http.IncomingMessage, res: http.ServerResponse) { + // 获取服务器配置的最大文件大小 + const maxSize = await this.getConfiguredMaxSize(req, null) + + // 设置TUS协议版本头,固定为1.0.0 + res.setHeader('Tus-Version', '1.0.0') + + // 如果存储模块支持扩展功能,设置TUS扩展头 + if (this.store.extensions.length > 0) { + res.setHeader('Tus-Extension', this.store.extensions.join(',')) + } + + // 如果配置了最大文件大小,设置TUS最大文件大小头 + if (maxSize) { + res.setHeader('Tus-Max-Size', maxSize) + } + + // 合并默认和自定义的允许头信息 + const allowedHeaders = [...HEADERS, ...(this.options.allowedHeaders ?? [])] + + // 设置CORS相关头信息 + res.setHeader('Access-Control-Allow-Methods', ALLOWED_METHODS) + res.setHeader('Access-Control-Allow-Headers', allowedHeaders.join(', ')) + res.setHeader('Access-Control-Max-Age', MAX_AGE) + + // 返回204 No Content状态码,表示请求成功但无内容返回 + return this.write(res, 204) + } +} diff --git a/packages/tus/src/handlers/PatchHandler.ts b/packages/tus/src/handlers/PatchHandler.ts new file mode 100644 index 0000000..72da508 --- /dev/null +++ b/packages/tus/src/handlers/PatchHandler.ts @@ -0,0 +1,256 @@ +/** + * PATCH请求处理器模块 + * + * 本模块负责处理TUS协议中的PATCH请求,用于上传文件的分块数据。 + * 主要功能包括:验证请求头、处理文件偏移量、写入数据到存储、处理上传完成事件等。 + * + * 使用场景: + * - 大文件分块上传 + * - 断点续传 + * - 文件上传进度管理 + */ +import debug from 'debug' +import { BaseHandler } from './BaseHandler' +import type http from 'node:http' +import { CancellationContext, ERRORS, Upload, EVENTS } from '../utils' + +const log = debug('tus-node-server:handlers:patch') + +/** + * PATCH请求处理器类 + * + * 继承自BaseHandler,专门处理TUS协议的PATCH请求。 + * 采用责任链模式,与其它处理器协同工作。 + * + * 设计模式解析: + * - 继承:扩展基础处理器功能 + * - 异步编程:使用async/await处理异步操作 + * - 事件驱动:通过EVENTS触发相关事件 + * + * 使用示例: + * const handler = new PatchHandler(store, options) + * handler.send(req, res, context) + */ +export class PatchHandler extends BaseHandler { + /** + * 处理PATCH请求的核心方法 + * + * 功能描述: + * 1. 验证请求头信息 + * 2. 获取文件上传偏移量 + * 3. 写入数据到存储 + * 4. 处理上传完成事件 + * 5. 返回响应结果 + * + * @param req HTTP请求对象 + * @param res HTTP响应对象 + * @param context 取消操作上下文 + * @returns 处理后的HTTP响应 + * + * 异常处理: + * - 文件未找到:抛出ERRORS.FILE_NOT_FOUND + * - 偏移量缺失:抛出ERRORS.MISSING_OFFSET + * - 内容类型无效:抛出ERRORS.INVALID_CONTENT_TYPE + * - 文件已过期:抛出ERRORS.FILE_NO_LONGER_EXISTS + * - 偏移量不匹配:抛出ERRORS.INVALID_OFFSET + */ + async send( + req: http.IncomingMessage, + res: http.ServerResponse, + context: CancellationContext + ) { + try { + // 从请求中获取文件ID + const id = this.getFileIdFromRequest(req) + // console.log('id', id) + if (!id) { + throw ERRORS.FILE_NOT_FOUND + } + + // 验证Upload-Offset头是否存在 + if (req.headers['upload-offset'] === undefined) { + throw ERRORS.MISSING_OFFSET + } + + // 解析偏移量 + const offset = Number.parseInt(req.headers['upload-offset'] as string, 10) + + // 验证Content-Type头是否存在 + const content_type = req.headers['content-type'] + if (content_type === undefined) { + throw ERRORS.INVALID_CONTENT_TYPE + } + + // 触发请求到达事件 + if (this.options.onIncomingRequest) { + await this.options.onIncomingRequest(req, res, id) + } + + // 获取配置的最大文件大小 + const maxFileSize = await this.getConfiguredMaxSize(req, id) + + // 获取文件锁 + const lock = await this.acquireLock(req, id, context) + + let upload: Upload + let newOffset: number + try { + // 从存储中获取上传信息 + upload = await this.store.getUpload(id) + + // 检查文件是否已过期 + const now = Date.now() + const creation = upload.creation_date + ? new Date(upload.creation_date).getTime() + : now + const expiration = creation + this.store.getExpiration() + if ( + this.store.hasExtension('expiration') && + this.store.getExpiration() > 0 && + now > expiration + ) { + throw ERRORS.FILE_NO_LONGER_EXISTS + } + + // 验证偏移量是否匹配 + if (upload.offset !== offset) { + log( + `[PatchHandler] send: Incorrect offset - ${offset} sent but file is ${upload.offset}` + ) + throw ERRORS.INVALID_OFFSET + } + + // 处理上传长度相关头信息 + const upload_length = req.headers['upload-length'] as string | undefined + if (upload_length !== undefined) { + const size = Number.parseInt(upload_length, 10) + // 检查是否支持延迟长度声明 + if (!this.store.hasExtension('creation-defer-length')) { + throw ERRORS.UNSUPPORTED_CREATION_DEFER_LENGTH_EXTENSION + } + + // 检查上传长度是否已设置 + if (upload.size !== undefined) { + throw ERRORS.INVALID_LENGTH + } + + // 验证长度是否有效 + if (size < upload.offset) { + throw ERRORS.INVALID_LENGTH + } + + // 检查是否超过最大文件大小 + if (maxFileSize > 0 && size > maxFileSize) { + throw ERRORS.ERR_MAX_SIZE_EXCEEDED + } + + // 声明上传长度 + await this.store.declareUploadLength(id, size) + upload.size = size + } + + // 计算最大请求体大小 + const maxBodySize = await this.calculateMaxBodySize(req, upload, maxFileSize) + // 写入数据到存储 + newOffset = await this.writeToStore(req, upload, maxBodySize, context) + } finally { + // 释放文件锁 + await lock.unlock() + } + + // 更新上传偏移量 + upload.offset = newOffset + // 触发数据接收完成事件 + this.emit(EVENTS.POST_RECEIVE, req, res, upload) + + // 构建响应数据 + const responseData = { + status: 204, + headers: { + 'Upload-Offset': newOffset, + } as Record, + body: '', + } + + // 处理上传完成事件 + // 文件上传完成后的处理逻辑块 + if (newOffset === upload.size && this.options.onUploadFinish) { + try { + // 调用上传完成回调函数,支持异步处理 + // 允许用户自定义上传完成后的处理逻辑 + const resOrObject = await this.options.onUploadFinish(req, res, upload) + + // 兼容性处理:支持两种返回类型 + // 1. 直接返回 http.ServerResponse 对象 + // 2. 返回包含自定义响应信息的对象 + if ( + // 检查是否为标准 ServerResponse 对象 + typeof (resOrObject as http.ServerResponse).write === 'function' && + typeof (resOrObject as http.ServerResponse).writeHead === 'function' + ) { + // 直接使用返回的服务器响应对象 + res = resOrObject as http.ServerResponse + } else { + // 处理自定义响应对象的类型定义 + // 排除 ServerResponse 类型,确保类型安全 + type ExcludeServerResponse = T extends http.ServerResponse ? never : T + + // 将返回对象转换为自定义响应对象 + const obj = resOrObject as ExcludeServerResponse + + // 更新响应对象 + res = obj.res + + // 可选地更新响应状态码 + if (obj.status_code) responseData.status = obj.status_code + + // 可选地更新响应体 + if (obj.body) responseData.body = obj.body + + // 合并响应头,允许覆盖默认头 + if (obj.headers) + responseData.headers = Object.assign(obj.headers, responseData.headers) + } + } catch (error: any) { + // 错误处理:记录上传完成回调中的错误 + // 使用日志记录错误信息,并重新抛出异常 + log(`onUploadFinish: ${error.body}`) + throw error + } + } + + // 处理文件过期时间 + if ( + this.store.hasExtension('expiration') && + this.store.getExpiration() > 0 && + upload.creation_date && + (upload.size === undefined || newOffset < upload.size) + ) { + const creation = new Date(upload.creation_date) + const dateString = new Date( + creation.getTime() + this.store.getExpiration() + ).toUTCString() + responseData.headers['Upload-Expires'] = dateString + } + + // 发送响应 + const writtenRes = this.write( + res, + responseData.status, + responseData.headers, + responseData.body + ) + + // 触发上传完成事件 + if (newOffset === upload.size) { + this.emit(EVENTS.POST_FINISH, req, writtenRes, upload) + } + + return writtenRes + } catch (e) { + // 取消操作 + context.abort() + throw e + } + } +} diff --git a/packages/tus/src/handlers/PostHandler.ts b/packages/tus/src/handlers/PostHandler.ts new file mode 100644 index 0000000..fb733db --- /dev/null +++ b/packages/tus/src/handlers/PostHandler.ts @@ -0,0 +1,257 @@ +import debug from 'debug' + +import { BaseHandler } from './BaseHandler' + +import { validateHeader } from '../validators/HeaderValidator' + +import type http from 'node:http' +import type { ServerOptions, WithRequired } from '../types' +import { DataStore, Uid, CancellationContext, ERRORS, Metadata, Upload, EVENTS } from '../utils' + +const log = debug('tus-node-server:handlers:post') + +/** + * PostHandler 类用于处理 HTTP POST 请求,主要用于在 DataStore 中创建文件。 + * 该类继承自 BaseHandler,并重写了部分方法以实现特定的功能。 + */ +export class PostHandler extends BaseHandler { + // 重写 BaseHandler 中的 options 类型,确保在构造函数中设置了 namingFunction + declare options: WithRequired + + /** + * 构造函数,初始化 PostHandler 实例。 + * @param store - DataStore 实例,用于存储上传的文件。 + * @param options - 服务器配置选项,包含 namingFunction 等。 + * @throws 如果 namingFunction 不是函数,则抛出错误。 + */ + constructor(store: DataStore, options: ServerOptions) { + if (options.namingFunction && typeof options.namingFunction !== 'function') { + throw new Error("'namingFunction' must be a function") + } + + if (!options.namingFunction) { + options.namingFunction = Uid.rand + } + + super(store, options) + } + + /** + * 在 DataStore 中创建文件。 + * @param req - HTTP 请求对象。 + * @param res - HTTP 响应对象。 + * @param context - 取消操作的上下文。 + * @returns 返回处理后的 HTTP 响应对象。 + * @throws 如果请求头中包含 'upload-concat' 但 DataStore 不支持 'concatentation' 扩展,则抛出错误。 + * @throws 如果请求头中 'upload-length' 和 'upload-defer-length' 同时存在或同时不存在,则抛出错误。 + * @throws 如果 'upload-metadata' 解析失败,则抛出错误。 + * @throws 如果文件大小超过配置的最大值,则抛出错误。 + */ + async send( + req: http.IncomingMessage, + res: http.ServerResponse, + context: CancellationContext + ) { + if ('upload-concat' in req.headers && !this.store.hasExtension('concatentation')) { + throw ERRORS.UNSUPPORTED_CONCATENATION_EXTENSION + } + + const upload_length = req.headers['upload-length'] as string | undefined + const upload_defer_length = req.headers['upload-defer-length'] as string | undefined + const upload_metadata = req.headers['upload-metadata'] as string | undefined + + if ( + upload_defer_length !== undefined && // 如果扩展不支持,则抛出错误 + !this.store.hasExtension('creation-defer-length') + ) { + throw ERRORS.UNSUPPORTED_CREATION_DEFER_LENGTH_EXTENSION + } + + if ((upload_length === undefined) === (upload_defer_length === undefined)) { + throw ERRORS.INVALID_LENGTH + } + + let metadata: ReturnType<(typeof Metadata)['parse']> | undefined + if ('upload-metadata' in req.headers) { + try { + metadata = Metadata.parse(upload_metadata) + } catch { + throw ERRORS.INVALID_METADATA + } + } + + let id: string + try { + id = await this.options.namingFunction(req, metadata) + } catch (error) { + log('create: check your `namingFunction`. Error', error) + throw error + } + + const maxFileSize = await this.getConfiguredMaxSize(req, id) + + if ( + upload_length && + maxFileSize > 0 && + Number.parseInt(upload_length, 10) > maxFileSize + ) { + throw ERRORS.ERR_MAX_SIZE_EXCEEDED + } + + if (this.options.onIncomingRequest) { + await this.options.onIncomingRequest(req, res, id) + } + + const upload = new Upload({ + id, + size: upload_length ? Number.parseInt(upload_length, 10) : undefined, + offset: 0, + metadata, + }) + + if (this.options.onUploadCreate) { + try { + const resOrObject = await this.options.onUploadCreate(req, res, upload) + // 向后兼容,将在下一个主要版本中移除 + // 由于在测试中模拟了实例,因此无法使用 `instanceof` 进行检查 + if ( + typeof (resOrObject as http.ServerResponse).write === 'function' && + typeof (resOrObject as http.ServerResponse).writeHead === 'function' + ) { + res = resOrObject as http.ServerResponse + } else { + // 由于 TS 只理解 instanceof,因此类型定义较为丑陋 + type ExcludeServerResponse = T extends http.ServerResponse ? never : T + const obj = resOrObject as ExcludeServerResponse + res = obj.res + if (obj.metadata) { + upload.metadata = obj.metadata + } + } + } catch (error: any) { + log(`onUploadCreate error: ${error.body}`) + throw error + } + } + + const lock = await this.acquireLock(req, id, context) + + let isFinal: boolean + let url: string + + // 推荐的响应默认值 + const responseData = { + status: 201, + headers: {} as Record, + body: '', + } + + try { + await this.store.create(upload) + url = this.generateUrl(req, upload.id) + + this.emit(EVENTS.POST_CREATE, req, res, upload, url) + + isFinal = upload.size === 0 && !upload.sizeIsDeferred + + // 如果请求中包含 Content-Type 头,并且使用了 creation-with-upload 扩展 + if (validateHeader('content-type', req.headers['content-type'])) { + const bodyMaxSize = await this.calculateMaxBodySize(req, upload, maxFileSize) + const newOffset = await this.writeToStore(req, upload, bodyMaxSize, context) + + responseData.headers['Upload-Offset'] = newOffset.toString() + isFinal = newOffset === Number.parseInt(upload_length as string, 10) + upload.offset = newOffset + } + } catch (e) { + context.abort() + throw e + } finally { + await lock.unlock() + } + // 上传完成后的处理逻辑 + if (isFinal && this.options.onUploadFinish) { + try { + // 调用自定义的上传完成回调函数,传入请求、响应和上传对象 + // 允许用户自定义上传完成后的处理逻辑 + const resOrObject = await this.options.onUploadFinish(req, res, upload) + + // 兼容性处理:检查返回值是否为 HTTP 响应对象 + // 通过检查对象是否具有 write 和 writeHead 方法来判断 + if ( + typeof (resOrObject as http.ServerResponse).write === 'function' && + typeof (resOrObject as http.ServerResponse).writeHead === 'function' + ) { + // 如果直接返回 HTTP 响应对象,直接覆盖原响应对象 + res = resOrObject as http.ServerResponse + } else { + // 处理自定义返回对象的情况 + // 使用复杂的类型定义排除 ServerResponse 类型 + type ExcludeServerResponse = T extends http.ServerResponse ? never : T + + // 将返回对象转换为非 ServerResponse 类型 + const obj = resOrObject as ExcludeServerResponse + + // 更新响应对象 + res = obj.res + + // 根据返回对象更新响应状态码 + if (obj.status_code) responseData.status = obj.status_code + + // 更新响应体 + if (obj.body) responseData.body = obj.body + + // 合并响应头,允许覆盖默认头 + if (obj.headers) + responseData.headers = Object.assign(obj.headers, responseData.headers) + } + } catch (error: any) { + // 记录上传完成回调中的错误 + log(`onUploadFinish: ${error.body}`) + + // 抛出错误,中断上传流程 + throw error + } + } + + + // Upload-Expires 响应头指示未完成的上传何时过期。 + // 如果在创建时已知过期时间,则必须在响应中包含 Upload-Expires 头 + if ( + this.store.hasExtension('expiration') && + this.store.getExpiration() > 0 && + upload.creation_date + ) { + const created = await this.store.getUpload(upload.id) + + if (created.offset !== Number.parseInt(upload_length as string, 10)) { + const creation = new Date(upload.creation_date) + // 值必须为 RFC 7231 日期时间格式 + responseData.headers['Upload-Expires'] = new Date( + creation.getTime() + this.store.getExpiration() + ).toUTCString() + } + } + + // 仅在最终的 HTTP 状态码为 201 或 3xx 时附加 Location 头 + if ( + responseData.status === 201 || + (responseData.status >= 300 && responseData.status < 400) + ) { + responseData.headers.Location = url + } + + const writtenRes = this.write( + res, + responseData.status, + responseData.headers, + responseData.body + ) + + if (isFinal) { + this.emit(EVENTS.POST_FINISH, req, writtenRes, upload) + } + + return writtenRes + } +} \ No newline at end of file diff --git a/packages/tus/src/index.ts b/packages/tus/src/index.ts new file mode 100644 index 0000000..f403e0e --- /dev/null +++ b/packages/tus/src/index.ts @@ -0,0 +1,5 @@ +export { Server } from './server' +export * from './types' +export * from './lockers' +export * from './utils' +export * from "./store" \ No newline at end of file diff --git a/packages/tus/src/lockers/MemoryLocker.ts b/packages/tus/src/lockers/MemoryLocker.ts new file mode 100644 index 0000000..58e801e --- /dev/null +++ b/packages/tus/src/lockers/MemoryLocker.ts @@ -0,0 +1,145 @@ +/** + * MemoryLocker 是一个实现了 Locker 接口的类,用于在内存中管理锁。 + * 该类设计用于对资源进行独占访问控制,常用于上传管理等场景。 + * + * 主要特性: + * - 通过使用基于内存的映射来跟踪锁,确保资源的独占访问。 + * - 实现锁获取的超时机制,缓解死锁情况。 + * - 通过不同的机制促进锁的立即和优雅释放。 + * + * 锁定行为: + * - 当对已经锁定的资源调用 `lock` 方法时,会调用 `cancelReq` 回调。 + * 这向当前锁持有者发出信号,表示另一个进程正在请求锁,鼓励他们尽快释放锁。 + * - 锁尝试会持续到指定的超时时间。如果超时到期且锁仍然不可用,则抛出错误以指示锁获取失败。 + * + * 锁的获取和释放: + * - `lock` 方法实现了等待机制,允许锁请求在锁可用时成功,或在超时期间失败。 + * - `unlock` 方法释放锁,使资源可供其他请求使用。 + */ + +import { RequestRelease, Locker, ERRORS, Lock } from "../utils" + +export interface MemoryLockerOptions { + acquireLockTimeout: number +} + +interface LockEntry { + requestRelease: RequestRelease +} + +export class MemoryLocker implements Locker { + timeout: number + locks = new Map() + + constructor(options?: MemoryLockerOptions) { + this.timeout = options?.acquireLockTimeout ?? 1000 * 30 + } + + /** + * 创建一个新的 MemoryLock 实例。 + * @param id 锁的唯一标识符。 + * @returns 返回一个新的 MemoryLock 实例。 + */ + newLock(id: string) { + return new MemoryLock(id, this, this.timeout) + } +} + +class MemoryLock implements Lock { + constructor( + private id: string, + private locker: MemoryLocker, + private timeout: number = 1000 * 30 + ) { } + + /** + * 尝试获取锁。 + * @param requestRelease 当锁被请求时调用的回调函数。 + * @throws 如果锁获取超时,则抛出 ERRORS.ERR_LOCK_TIMEOUT 错误。 + */ + async lock(requestRelease: RequestRelease): Promise { + const abortController = new AbortController() + const lock = await Promise.race([ + this.waitTimeout(abortController.signal), + this.acquireLock(this.id, requestRelease, abortController.signal), + ]) + + abortController.abort() + + if (!lock) { + throw ERRORS.ERR_LOCK_TIMEOUT + } + } + + /** + * 尝试获取指定 ID 的锁。 + * @param id 锁的唯一标识符。 + * @param requestRelease 当锁被请求释放时调用的回调函数。 + * @param signal 用于取消操作的 AbortSignal。 + * @returns 如果成功获取锁,则返回 true;否则返回 false。 + */ + protected async acquireLock( + id: string, + requestRelease: RequestRelease, + signal: AbortSignal + ): Promise { + if (signal.aborted) { + return false + } + + const lock = this.locker.locks.get(id) + + if (!lock) { + const lock = { + requestRelease, + } + this.locker.locks.set(id, lock) + return true + } + + await lock.requestRelease?.() + + return await new Promise((resolve, reject) => { + // 使用 setImmediate 的原因: + // 1. 通过将递归调用推迟到下一个事件循环迭代来防止堆栈溢出。 + // 2. 允许事件循环处理其他挂起的事件,保持服务器的响应性。 + // 3. 通过给其他请求获取锁的机会,确保锁获取的公平性。 + setImmediate(() => { + this.acquireLock(id, requestRelease, signal).then(resolve).catch(reject) + }) + }) + } + + /** + * 释放锁。 + * @throws 如果尝试释放未锁定的锁,则抛出错误。 + */ + async unlock(): Promise { + const lock = this.locker.locks.get(this.id) + if (!lock) { + throw new Error('Releasing an unlocked lock!') + } + + this.locker.locks.delete(this.id) + } + + /** + * 等待超时。 + * @param signal 用于取消操作的 AbortSignal。 + * @returns 如果超时,则返回 false。 + */ + protected waitTimeout(signal: AbortSignal) { + return new Promise((resolve) => { + const timeout = setTimeout(() => { + resolve(false) + }, this.timeout) + + const abortListener = () => { + clearTimeout(timeout) + signal.removeEventListener('abort', abortListener) + resolve(false) + } + signal.addEventListener('abort', abortListener) + }) + } +} \ No newline at end of file diff --git a/packages/tus/src/lockers/index.ts b/packages/tus/src/lockers/index.ts new file mode 100644 index 0000000..b6de79c --- /dev/null +++ b/packages/tus/src/lockers/index.ts @@ -0,0 +1 @@ +export * from './MemoryLocker' diff --git a/packages/tus/src/server.ts b/packages/tus/src/server.ts new file mode 100644 index 0000000..ab1aeaf --- /dev/null +++ b/packages/tus/src/server.ts @@ -0,0 +1,519 @@ +import http from "node:http"; +import { EventEmitter } from "node:events"; +import debug from "debug"; +import { GetHandler } from "./handlers/GetHandler"; +import { HeadHandler } from "./handlers/HeadHandler"; +import { OptionsHandler } from "./handlers/OptionsHandler"; +import { PatchHandler } from "./handlers/PatchHandler"; +import { PostHandler } from "./handlers/PostHandler"; +import { DeleteHandler } from "./handlers/DeleteHandler"; +import { validateHeader } from "./validators/HeaderValidator"; +import type stream from "node:stream"; +import type { ServerOptions, RouteHandler, WithOptional } from "./types"; +import { MemoryLocker } from "./lockers"; +import { + EVENTS, + Upload, + DataStore, + REQUEST_METHODS, + ERRORS, + TUS_RESUMABLE, + EXPOSED_HEADERS, + CancellationContext, +} from "./utils"; + +/** + * 处理器类型映射 + * 定义了TUS服务器支持的各种HTTP方法对应的处理器实例类型 + */ +type Handlers = { + GET: InstanceType; // GET请求处理器 + HEAD: InstanceType; // HEAD请求处理器 + OPTIONS: InstanceType; // OPTIONS请求处理器 + PATCH: InstanceType; // PATCH请求处理器 + POST: InstanceType; // POST请求处理器 + DELETE: InstanceType; // DELETE请求处理器 +}; + +/** + * TUS服务器事件接口定义 + * 描述了服务器在不同阶段触发的事件及其处理函数签名 + */ +interface TusEvents { + /** + * 文件创建后触发 + * @param req HTTP请求对象 + * @param res HTTP响应对象 + * @param upload 上传对象实例 + * @param url 生成的文件URL + */ + [EVENTS.POST_CREATE]: ( + req: http.IncomingMessage, + res: http.ServerResponse, + upload: Upload, + url: string + ) => void; + + /** + * @deprecated 文件接收事件(已废弃) + * 建议使用 POST_RECEIVE_V2 替代 + */ + [EVENTS.POST_RECEIVE]: ( + req: http.IncomingMessage, + res: http.ServerResponse, + upload: Upload + ) => void; + + /** + * 文件接收事件V2版本 + * @param req HTTP请求对象 + * @param upload 上传对象实例 + */ + [EVENTS.POST_RECEIVE_V2]: ( + req: http.IncomingMessage, + upload: Upload + ) => void; + + /** + * 文件上传完成事件 + * @param req HTTP请求对象 + * @param res HTTP响应对象 + * @param upload 上传对象实例 + */ + [EVENTS.POST_FINISH]: ( + req: http.IncomingMessage, + res: http.ServerResponse, + upload: Upload + ) => void; + + /** + * 文件终止上传事件 + * @param req HTTP请求对象 + * @param res HTTP响应对象 + * @param id 文件唯一标识符 + */ + [EVENTS.POST_TERMINATE]: ( + req: http.IncomingMessage, + res: http.ServerResponse, + id: string + ) => void; +} + +/** + * EventEmitter事件处理器类型别名 + */ +type on = EventEmitter["on"]; +type emit = EventEmitter["emit"]; + +/** + * TUS服务器接口声明 + * 继承EventEmitter,支持事件监听和触发 + */ +export declare interface Server { + /** + * 为指定事件注册监听器 + * @param event 事件名称,必须是TusEvents的键之一 + * @param listener 事件触发时执行的回调函数 + * @returns 返回Server实例以支持链式调用 + */ + on( + event: Event, + listener: TusEvents[Event] + ): this; + /** + * 为指定事件注册监听器(通用版本) + * @param eventName 事件名称 + * @param listener 事件触发时执行的回调函数 + * @returns 返回Server实例以支持链式调用 + */ + on(eventName: Parameters[0], listener: Parameters[1]): this; + /** + * 触发指定事件 + * @param event 事件名称,必须是TusEvents的键之一 + * @param listener 事件触发时执行的回调函数 + * @returns 返回emit函数的返回值 + */ + emit( + event: Event, + listener: TusEvents[Event] + ): ReturnType; + /** + * 触发指定事件(通用版本) + * @param eventName 事件名称 + * @param listener 事件触发时执行的回调函数 + * @returns 返回emit函数的返回值 + */ + emit( + eventName: Parameters[0], + listener: Parameters[1] + ): ReturnType; +} +/** + * 调试日志工具实例 + */ +const log = debug("tus-node-server"); + +// biome-ignore lint/suspicious/noUnsafeDeclarationMerging: it's fine +export class Server extends EventEmitter { + datastore: DataStore; + handlers: Handlers; + options: ServerOptions; + + /** + * Server 构造函数 + * @param options - 服务器配置选项,包含数据存储和可选配置 + * @throws 如果未提供 options、path 或 datastore,将抛出错误 + */ + constructor( + options: WithOptional & { + datastore: DataStore; + } + ) { + super(); + + if (!options) { + throw new Error("'options' must be defined"); + } + + if (!options.path) { + throw new Error("'path' is not defined; must have a path"); + } + + if (!options.datastore) { + throw new Error( + "'datastore' is not defined; must have a datastore" + ); + } + + if (!options.locker) { + options.locker = new MemoryLocker(); + } + + if (!options.lockDrainTimeout) { + options.lockDrainTimeout = 3000; + } + + if (!options.postReceiveInterval) { + options.postReceiveInterval = 1000; + } + + const { datastore, ...rest } = options; + this.options = rest as ServerOptions; + this.datastore = datastore; + this.handlers = { + // GET 请求处理器应在具体实现中编写 + GET: new GetHandler(this.datastore, this.options), + // 这些方法按照 tus 协议处理 + HEAD: new HeadHandler(this.datastore, this.options), + OPTIONS: new OptionsHandler(this.datastore, this.options), + PATCH: new PatchHandler(this.datastore, this.options), + POST: new PostHandler(this.datastore, this.options), + DELETE: new DeleteHandler(this.datastore, this.options), + }; + // 任何以方法为键分配给此对象的处理器将用于响应这些请求。 + // 当数据存储分配给服务器时,它们会被设置/重置。 + // 从服务器中移除任何事件监听器时,必须先从每个处理器中移除监听器。 + // 这必须在添加 'newListener' 监听器之前完成,以避免为所有请求处理器添加 'removeListener' 事件监听器。 + this.on("removeListener", (event: string, listener) => { + this.datastore.removeListener(event, listener); + for (const method of REQUEST_METHODS) { + this.handlers[method].removeListener(event, listener); + } + }); + // 当事件监听器被添加到服务器时,确保它们从请求处理器冒泡到服务器级别。 + this.on("newListener", (event: string, listener) => { + this.datastore.on(event, listener); + for (const method of REQUEST_METHODS) { + this.handlers[method].on(event, listener); + } + }); + } + + /** + * 注册 GET 请求处理器 + * @param path - 请求路径 + * @param handler - 请求处理器 + */ + get(path: string, handler: RouteHandler) { + this.handlers.GET.registerPath(path, handler); + } + + /** + * 主服务器请求监听器,在每个 'request' 事件上调用 + * @param req - HTTP 请求对象 + * @param res - HTTP 响应对象 + * @returns 返回 HTTP 响应对象或流 + */ + async handle( + req: http.IncomingMessage, + res: http.ServerResponse + // biome-ignore lint/suspicious/noConfusingVoidType: it's fine + ): Promise { + const context = this.createContext(req); + log(`[TusServer] handle: ${req.method} ${req.url}`); + // 允许覆盖 HTTP 方法。这样做的原因是某些库/环境不支持 PATCH 和 DELETE 请求,例如浏览器中的 Flash 和 Java 部分环境 + if (req.headers["x-http-method-override"]) { + req.method = ( + req.headers["x-http-method-override"] as string + ).toUpperCase(); + } + const onError = async (error: { + status_code?: number; + body?: string; + message: string; + }) => { + let status_code = + error.status_code || ERRORS.UNKNOWN_ERROR.status_code; + let body = + error.body || + `${ERRORS.UNKNOWN_ERROR.body}${error.message || ""}\n`; + if (this.options.onResponseError) { + const errorMapping = await this.options.onResponseError( + req, + res, + error as Error + ); + if (errorMapping) { + status_code = errorMapping.status_code; + body = errorMapping.body; + } + } + return this.write(context, req, res, status_code, body); + }; + if (req.method === "GET") { + const handler = this.handlers.GET; + return handler.send(req, res).catch(onError); + } + + // Tus-Resumable 头部必须包含在每个请求和响应中,除了 OPTIONS 请求。其值必须是客户端或服务器使用的协议版本。 + res.setHeader("Tus-Resumable", TUS_RESUMABLE); + if ( + req.method !== "OPTIONS" && + req.headers["tus-resumable"] === undefined + ) { + return this.write( + context, + req, + res, + 412, + "Tus-Resumable Required\n" + ); + } + // 验证所有必需的头部以符合 tus 协议 + const invalid_headers = []; + for (const header_name in req.headers) { + if (req.method === "OPTIONS") { + continue; + } + // 内容类型仅对 PATCH 请求进行检查。对于所有其他请求方法,它将被忽略并视为未设置内容类型, + // 因为某些 HTTP 客户端可能会为此头部强制执行默认值。 + // 参见 https://github.com/tus/tus-node-server/pull/116 + if ( + header_name.toLowerCase() === "content-type" && + req.method !== "PATCH" + ) { + continue; + } + if ( + !validateHeader( + header_name, + req.headers[header_name] as string | undefined + ) + ) { + log( + `Invalid ${header_name} header: ${req.headers[header_name]}` + ); + invalid_headers.push(header_name); + } + } + + if (invalid_headers.length > 0) { + return this.write( + context, + req, + res, + 400, + `Invalid ${invalid_headers.join(" ")}\n` + ); + } + // 启用 CORS + res.setHeader("Access-Control-Allow-Origin", this.getCorsOrigin(req)); + res.setHeader("Access-Control-Expose-Headers", EXPOSED_HEADERS); + if (this.options.allowedCredentials === true) { + res.setHeader("Access-Control-Allow-Credentials", "true"); + } + + // 调用请求方法的处理器 + const handler = this.handlers[req.method as keyof Handlers]; + if (handler) { + return handler.send(req, res, context).catch(onError); + } + + return this.write(context, req, res, 404, "Not found\n"); + } + + /** + * 获取CORS(跨域资源共享)允许的源地址 + * + * 该方法用于确定并返回允许的CORS源地址。首先检查请求头中的`origin`是否在允许的源列表中, + * 如果在则返回该`origin`;如果不在但允许的源列表不为空,则返回列表中的第一个源地址; + * 如果允许的源列表为空,则返回通配符`*`,表示允许所有源地址。 + * + * @param req HTTP请求对象,包含请求头等信息 + * @returns 返回允许的CORS源地址,可能是请求头中的`origin`、允许的源列表中的第一个源地址或通配符`*` + * + * 设计考量: + * - 该方法考虑了CORS策略的灵活性,允许通过配置动态指定允许的源地址。 + * - 通过返回通配符`*`,简化了默认情况下的CORS配置,但需要注意这可能带来安全风险。 + */ + private getCorsOrigin(req: http.IncomingMessage): string { + const origin = req.headers.origin; + // 检查请求头中的`origin`是否在允许的源列表中 + const isOriginAllowed = + this.options.allowedOrigins?.some( + (allowedOrigin) => allowedOrigin === origin + ) ?? true; + // 如果`origin`存在且在允许的源列表中,则返回该`origin` + if (origin && isOriginAllowed) { + return origin; + } + // 如果允许的源列表不为空,则返回列表中的第一个源地址 + if ( + this.options.allowedOrigins && + this.options.allowedOrigins.length > 0 + ) { + return this.options.allowedOrigins[0]; + } + + // 如果允许的源列表为空,则返回通配符`*`,表示允许所有源地址 + return "*"; + } + + /** + * 写入响应 + * @param context - 取消上下文 + * @param req - HTTP 请求对象 + * @param res - HTTP 响应对象 + * @param status - HTTP 状态码 + * @param body - 响应体 + * @param headers - 响应头部 + * @returns 返回 HTTP 响应对象 + */ + write( + context: CancellationContext, + req: http.IncomingMessage, + res: http.ServerResponse, + status: number, + body = "", + headers = {} + ) { + const isAborted = context.signal.aborted; + + if (status !== 204) { + // @ts-expect-error not explicitly typed but possible + headers["Content-Length"] = Buffer.byteLength(body, "utf8"); + } + + if (isAborted) { + // 此条件处理请求被标记为中止的情况。 + // 在这种情况下,服务器通知客户端连接将被关闭。 + // 这是通过在响应中设置 'Connection' 头部为 'close' 来传达的。 + // 这一步对于防止服务器继续处理不再需要的请求至关重要,从而节省资源。 + + // @ts-expect-error not explicitly typed but possible + headers.Connection = "close"; + + // 为响应 ('res') 添加 'finish' 事件的事件监听器。 + // 'finish' 事件在响应已发送给客户端时触发。 + // 一旦响应完成,请求 ('req') 对象将被销毁。 + // 销毁请求对象是释放与此请求相关的任何资源的关键步骤,因为它已经被中止。 + res.on("finish", () => { + req.destroy(); + }); + } + + res.writeHead(status, headers); + res.write(body); + return res.end(); + } + + /** + * 启动服务器监听 + * @param args - 监听参数 + * @returns 返回 HTTP 服务器实例 + */ + // biome-ignore lint/suspicious/noExplicitAny: todo + listen(...args: any[]): http.Server { + return http.createServer(this.handle.bind(this)).listen(...args); + } + + /** + * 清理过期的上传 + * @returns 返回删除的过期上传数量 + * @throws 如果数据存储不支持过期扩展,将抛出错误 + */ + cleanUpExpiredUploads(): Promise { + if (!this.datastore.hasExtension("expiration")) { + throw ERRORS.UNSUPPORTED_EXPIRATION_EXTENSION; + } + + return this.datastore.deleteExpired(); + } + + /** + * 创建取消上下文 + * @param req - HTTP 请求对象 + * @returns 返回取消上下文 + */ + protected createContext(req: http.IncomingMessage) { + // 初始化两个 AbortController: + // 1. `requestAbortController` 用于即时请求终止,特别适用于在发生错误时停止客户端上传。 + // 2. `abortWithDelayController` 用于在终止前引入延迟,允许服务器有时间完成正在进行的操作。 + // 这在未来的请求可能需要获取当前请求持有的锁时特别有用。 + const requestAbortController = new AbortController(); + const abortWithDelayController = new AbortController(); + + // 当 `abortWithDelayController` 被触发时调用此函数,以在指定延迟后中止请求。 + const onDelayedAbort = (err: unknown) => { + abortWithDelayController.signal.removeEventListener( + "abort", + onDelayedAbort + ); + setTimeout(() => { + requestAbortController.abort(err); + }, this.options.lockDrainTimeout); + }; + abortWithDelayController.signal.addEventListener( + "abort", + onDelayedAbort + ); + + // 当请求关闭时,移除监听器以避免内存泄漏。 + req.on("close", () => { + abortWithDelayController.signal.removeEventListener( + "abort", + onDelayedAbort + ); + }); + + // 返回一个对象,包含信号和两个中止请求的方法。 + // `signal` 用于监听请求中止事件。 + // `abort` 方法用于立即中止请求。 + // `cancel` 方法用于启动延迟中止序列。 + return { + signal: requestAbortController.signal, + abort: () => { + // 立即中止请求 + if (!requestAbortController.signal.aborted) { + requestAbortController.abort(ERRORS.ABORTED); + } + }, + cancel: () => { + // 启动延迟中止序列,除非它已经在进行中。 + if (!abortWithDelayController.signal.aborted) { + abortWithDelayController.abort(ERRORS.ABORTED); + } + }, + }; + } +} diff --git a/packages/tus/src/store/file-store/index.ts b/packages/tus/src/store/file-store/index.ts new file mode 100644 index 0000000..b9e11d7 --- /dev/null +++ b/packages/tus/src/store/file-store/index.ts @@ -0,0 +1,230 @@ +// TODO: use /promises versions +import fs from 'node:fs' +import fsProm from 'node:fs/promises' +import path from 'node:path' +import stream from 'node:stream' +import type http from 'node:http' + +import debug from 'debug' +import { DataStore, Upload, ERRORS } from '../../utils' +import { + FileKvStore as FileConfigstore, + MemoryKvStore as MemoryConfigstore, + RedisKvStore as RedisConfigstore, + KvStore as Configstore, +} from '../../utils' + +type Options = { + directory: string + configstore?: Configstore + expirationPeriodInMilliseconds?: number +} + +const MASK = '0777' +const IGNORED_MKDIR_ERROR = 'EEXIST' +const FILE_DOESNT_EXIST = 'ENOENT' +const log = debug('tus-node-server:stores:filestore') + +export class FileStore extends DataStore { + directory: string + configstore: Configstore + expirationPeriodInMilliseconds: number + + constructor({ directory, configstore, expirationPeriodInMilliseconds }: Options) { + super() + this.directory = directory + this.configstore = configstore ?? new FileConfigstore(directory) + this.expirationPeriodInMilliseconds = expirationPeriodInMilliseconds ?? 0 + this.extensions = [ + 'creation', + 'creation-with-upload', + 'creation-defer-length', + 'termination', + 'expiration', + ] + // TODO: this async call can not happen in the constructor + this.checkOrCreateDirectory() + } + + /** + * Ensure the directory exists. + */ + private checkOrCreateDirectory() { + fs.mkdir(this.directory, { mode: MASK, recursive: true }, (error) => { + if (error && error.code !== IGNORED_MKDIR_ERROR) { + throw error + } + }) + } + + /** + * Create an empty file. + */ + async create(file: Upload): Promise { + const dirs = file.id.split('/').slice(0, -1) + const filePath = path.join(this.directory, file.id) + + await fsProm.mkdir(path.join(this.directory, ...dirs), { recursive: true }) + await fsProm.writeFile(filePath, '') + await this.configstore.set(file.id, file) + + file.storage = { type: 'file', path: filePath } + + return file + } + + read(file_id: string) { + return fs.createReadStream(path.join(this.directory, file_id)) + } + + remove(file_id: string): Promise { + return new Promise((resolve, reject) => { + fs.unlink(`${this.directory}/${file_id}`, (err) => { + if (err) { + log('[FileStore] delete: Error', err) + reject(ERRORS.FILE_NOT_FOUND) + return + } + + try { + resolve(this.configstore.delete(file_id)) + } catch (error) { + reject(error) + } + }) + }) + } + + write( + readable: http.IncomingMessage | stream.Readable, + file_id: string, + offset: number + ): Promise { + const file_path = path.join(this.directory, file_id) + const writeable = fs.createWriteStream(file_path, { + flags: 'r+', + start: offset, + }) + + let bytes_received = 0 + const transform = new stream.Transform({ + transform(chunk, _, callback) { + bytes_received += chunk.length + callback(null, chunk) + }, + }) + + return new Promise((resolve, reject) => { + stream.pipeline(readable, transform, writeable, (err) => { + if (err) { + log('[FileStore] write: Error', err) + return reject(ERRORS.FILE_WRITE_ERROR) + } + + log(`[FileStore] write: ${bytes_received} bytes written to ${file_path}`) + offset += bytes_received + log(`[FileStore] write: File is now ${offset} bytes`) + + return resolve(offset) + }) + }) + } + + async getUpload(id: string): Promise { + const file = await this.configstore.get(id) + + if (!file) { + throw ERRORS.FILE_NOT_FOUND + } + + return new Promise((resolve, reject) => { + const file_path = `${this.directory}/${id}` + fs.stat(file_path, (error, stats) => { + if (error && error.code === FILE_DOESNT_EXIST && file) { + log( + `[FileStore] getUpload: No file found at ${file_path} but db record exists`, + file + ) + return reject(ERRORS.FILE_NO_LONGER_EXISTS) + } + + if (error && error.code === FILE_DOESNT_EXIST) { + log(`[FileStore] getUpload: No file found at ${file_path}`) + return reject(ERRORS.FILE_NOT_FOUND) + } + + if (error) { + return reject(error) + } + + if (stats.isDirectory()) { + log(`[FileStore] getUpload: ${file_path} is a directory`) + return reject(ERRORS.FILE_NOT_FOUND) + } + + return resolve( + new Upload({ + id, + size: file.size, + offset: stats.size, + metadata: file.metadata, + creation_date: file.creation_date, + storage: { type: 'file', path: file_path }, + }) + ) + }) + }) + } + + async declareUploadLength(id: string, upload_length: number) { + const file = await this.configstore.get(id) + + if (!file) { + throw ERRORS.FILE_NOT_FOUND + } + + file.size = upload_length + + await this.configstore.set(id, file) + } + + async deleteExpired(): Promise { + const now = new Date() + const toDelete: Promise[] = [] + + if (!this.configstore.list) { + throw ERRORS.UNSUPPORTED_EXPIRATION_EXTENSION + } + + const uploadKeys = await this.configstore.list() + for (const file_id of uploadKeys) { + try { + const info = await this.configstore.get(file_id) + if ( + info && + 'creation_date' in info && + this.getExpiration() > 0 && + info.size !== info.offset && + info.creation_date + ) { + const creation = new Date(info.creation_date) + const expires = new Date(creation.getTime() + this.getExpiration()) + if (now > expires) { + toDelete.push(this.remove(file_id)) + } + } + } catch (error) { + if (error !== ERRORS.FILE_NO_LONGER_EXISTS) { + throw error + } + } + } + + await Promise.all(toDelete) + return toDelete.length + } + + getExpiration(): number { + return this.expirationPeriodInMilliseconds + } +} \ No newline at end of file diff --git a/packages/tus/src/store/index.ts b/packages/tus/src/store/index.ts new file mode 100644 index 0000000..bf02d53 --- /dev/null +++ b/packages/tus/src/store/index.ts @@ -0,0 +1,2 @@ +export * from "./file-store" +export * from "./s3-store" \ No newline at end of file diff --git a/packages/tus/src/store/s3-store/index.ts b/packages/tus/src/store/s3-store/index.ts new file mode 100644 index 0000000..2b58506 --- /dev/null +++ b/packages/tus/src/store/s3-store/index.ts @@ -0,0 +1,803 @@ +import os from 'node:os' +import fs, { promises as fsProm } from 'node:fs' +import stream, { promises as streamProm } from 'node:stream' +import type { Readable } from 'node:stream' + +import type AWS from '@aws-sdk/client-s3' +import { NoSuchKey, NotFound, S3, type S3ClientConfig } from '@aws-sdk/client-s3' +import debug from 'debug' + +import { + DataStore, + StreamSplitter, + Upload, + ERRORS, + TUS_RESUMABLE, + type KvStore, + MemoryKvStore, +} from '../../utils' + +import { Semaphore, type Permit } from '@shopify/semaphore' +import MultiStream from 'multistream' +import crypto from 'node:crypto' +import path from 'node:path' + +const log = debug('tus-node-server:stores:s3store') + +type Options = { + // The preferred part size for parts send to S3. Can not be lower than 5MiB or more than 5GiB. + // The server calculates the optimal part size, which takes this size into account, + // but may increase it to not exceed the S3 10K parts limit. + partSize?: number + useTags?: boolean + maxConcurrentPartUploads?: number + cache?: KvStore + expirationPeriodInMilliseconds?: number + // Options to pass to the AWS S3 SDK. + s3ClientConfig: S3ClientConfig & { bucket: string } +} + +export type MetadataValue = { + file: Upload + 'upload-id': string + 'tus-version': string +} + +function calcOffsetFromParts(parts?: Array) { + // @ts-expect-error not undefined + return parts && parts.length > 0 ? parts.reduce((a, b) => a + b.Size, 0) : 0 +} + +// Implementation (based on https://github.com/tus/tusd/blob/master/s3store/s3store.go) +// +// Once a new tus upload is initiated, multiple objects in S3 are created: +// +// First of all, a new info object is stored which contains (as Metadata) a JSON-encoded +// blob of general information about the upload including its size and meta data. +// This kind of objects have the suffix ".info" in their key. +// +// In addition a new multipart upload +// (http://docs.aws.amazon.com/AmazonS3/latest/dev/uploadobjusingmpu.html) is +// created. Whenever a new chunk is uploaded to tus-node-server using a PATCH request, a +// new part is pushed to the multipart upload on S3. +// +// If meta data is associated with the upload during creation, it will be added +// to the multipart upload and after finishing it, the meta data will be passed +// to the final object. However, the metadata which will be attached to the +// final object can only contain ASCII characters and every non-ASCII character +// will be replaced by a question mark (for example, "Menü" will be "Men?"). +// However, this does not apply for the metadata returned by the `_getMetadata` +// function since it relies on the info object for reading the metadata. +// Therefore, HEAD responses will always contain the unchanged metadata, Base64- +// encoded, even if it contains non-ASCII characters. +// +// Once the upload is finished, the multipart upload is completed, resulting in +// the entire file being stored in the bucket. The info object, containing +// meta data is not deleted. +// +// Considerations +// +// In order to support tus' principle of resumable upload, S3's Multipart-Uploads +// are internally used. +// For each incoming PATCH request (a call to `write`), a new part is uploaded +// to S3. +export class S3Store extends DataStore { + private bucket: string + private cache: KvStore + private client: S3 + private preferredPartSize: number + private expirationPeriodInMilliseconds = 0 + private useTags = true + private partUploadSemaphore: Semaphore + public maxMultipartParts = 10_000 as const + public minPartSize = 5_242_880 as const // 5MiB + public maxUploadSize = 5_497_558_138_880 as const // 5TiB + + constructor(options: Options) { + super() + const { partSize, s3ClientConfig } = options + const { bucket, ...restS3ClientConfig } = s3ClientConfig + this.extensions = [ + 'creation', + 'creation-with-upload', + 'creation-defer-length', + 'termination', + 'expiration', + ] + this.bucket = bucket + this.preferredPartSize = partSize || 8 * 1024 * 1024 + this.expirationPeriodInMilliseconds = options.expirationPeriodInMilliseconds ?? 0 + this.useTags = options.useTags ?? true + this.cache = options.cache ?? new MemoryKvStore() + this.client = new S3(restS3ClientConfig) + this.partUploadSemaphore = new Semaphore(options.maxConcurrentPartUploads ?? 60) + } + + protected shouldUseExpirationTags() { + return this.expirationPeriodInMilliseconds !== 0 && this.useTags + } + + protected useCompleteTag(value: 'true' | 'false') { + if (!this.shouldUseExpirationTags()) { + return undefined + } + + return `Tus-Completed=${value}` + } + + /** + * Saves upload metadata to a `${file_id}.info` file on S3. + * Please note that the file is empty and the metadata is saved + * on the S3 object's `Metadata` field, so that only a `headObject` + * is necessary to retrieve the data. + */ + private async saveMetadata(upload: Upload, uploadId: string) { + log(`[${upload.id}] saving metadata`) + await this.client.putObject({ + Bucket: this.bucket, + Key: this.infoKey(upload.id), + Body: JSON.stringify(upload), + Tagging: this.useCompleteTag('false'), + Metadata: { + 'upload-id': uploadId, + 'tus-version': TUS_RESUMABLE, + }, + }) + log(`[${upload.id}] metadata file saved`) + } + + private async completeMetadata(upload: Upload) { + if (!this.shouldUseExpirationTags()) { + return + } + + const { 'upload-id': uploadId } = await this.getMetadata(upload.id) + await this.client.putObject({ + Bucket: this.bucket, + Key: this.infoKey(upload.id), + Body: JSON.stringify(upload), + Tagging: this.useCompleteTag('true'), + Metadata: { + 'upload-id': uploadId, + 'tus-version': TUS_RESUMABLE, + }, + }) + } + + /** + * Retrieves upload metadata previously saved in `${file_id}.info`. + * There's a small and simple caching mechanism to avoid multiple + * HTTP calls to S3. + */ + private async getMetadata(id: string): Promise { + const cached = await this.cache.get(id) + if (cached) { + return cached + } + + const { Metadata, Body } = await this.client.getObject({ + Bucket: this.bucket, + Key: this.infoKey(id), + }) + const file = JSON.parse((await Body?.transformToString()) as string) + const metadata: MetadataValue = { + 'tus-version': Metadata?.['tus-version'] as string, + 'upload-id': Metadata?.['upload-id'] as string, + file: new Upload({ + id, + size: file.size ? Number.parseInt(file.size, 10) : undefined, + offset: Number.parseInt(file.offset, 10), + metadata: file.metadata, + creation_date: file.creation_date, + storage: file.storage, + }), + } + await this.cache.set(id, metadata) + return metadata + } + + private infoKey(id: string) { + return `${id}.info` + } + + private partKey(id: string, isIncomplete = false) { + if (isIncomplete) { + id += '.part' + } + + // TODO: introduce ObjectPrefixing for parts and incomplete parts. + // ObjectPrefix is prepended to the name of each S3 object that is created + // to store uploaded files. It can be used to create a pseudo-directory + // structure in the bucket, e.g. "path/to/my/uploads". + return id + } + + private async uploadPart( + metadata: MetadataValue, + readStream: fs.ReadStream | Readable, + partNumber: number + ): Promise { + const data = await this.client.uploadPart({ + Bucket: this.bucket, + Key: metadata.file.id, + UploadId: metadata['upload-id'], + PartNumber: partNumber, + Body: readStream, + }) + log(`[${metadata.file.id}] finished uploading part #${partNumber}`) + return data.ETag as string + } + + private async uploadIncompletePart( + id: string, + readStream: fs.ReadStream | Readable + ): Promise { + const data = await this.client.putObject({ + Bucket: this.bucket, + Key: this.partKey(id, true), + Body: readStream, + Tagging: this.useCompleteTag('false'), + }) + log(`[${id}] finished uploading incomplete part`) + return data.ETag as string + } + + private async downloadIncompletePart(id: string) { + const incompletePart = await this.getIncompletePart(id) + + if (!incompletePart) { + return + } + const filePath = await this.uniqueTmpFileName('tus-s3-incomplete-part-') + + try { + let incompletePartSize = 0 + + const byteCounterTransform = new stream.Transform({ + transform(chunk, _, callback) { + incompletePartSize += chunk.length + callback(null, chunk) + }, + }) + + // write to temporary file + await streamProm.pipeline( + incompletePart, + byteCounterTransform, + fs.createWriteStream(filePath) + ) + + const createReadStream = (options: { cleanUpOnEnd: boolean }) => { + const fileReader = fs.createReadStream(filePath) + + if (options.cleanUpOnEnd) { + fileReader.on('end', () => { + fs.unlink(filePath, () => { + // ignore + }) + }) + + fileReader.on('error', (err) => { + fileReader.destroy(err) + fs.unlink(filePath, () => { + // ignore + }) + }) + } + + return fileReader + } + + return { + size: incompletePartSize, + path: filePath, + createReader: createReadStream, + } + } catch (err) { + fsProm.rm(filePath).catch(() => { + /* ignore */ + }) + throw err + } + } + + private async getIncompletePart(id: string): Promise { + try { + const data = await this.client.getObject({ + Bucket: this.bucket, + Key: this.partKey(id, true), + }) + return data.Body as Readable + } catch (error) { + if (error instanceof NoSuchKey) { + return undefined + } + + throw error + } + } + + private async getIncompletePartSize(id: string): Promise { + try { + const data = await this.client.headObject({ + Bucket: this.bucket, + Key: this.partKey(id, true), + }) + return data.ContentLength + } catch (error) { + if (error instanceof NotFound) { + return undefined + } + throw error + } + } + + private async deleteIncompletePart(id: string): Promise { + await this.client.deleteObject({ + Bucket: this.bucket, + Key: this.partKey(id, true), + }) + } + + /** + * Uploads a stream to s3 using multiple parts + */ + private async uploadParts( + metadata: MetadataValue, + readStream: stream.Readable, + currentPartNumber: number, + offset: number + ): Promise { + const size = metadata.file.size + const promises: Promise[] = [] + let pendingChunkFilepath: string | null = null + let bytesUploaded = 0 + let permit: Permit | undefined = undefined + + const splitterStream = new StreamSplitter({ + chunkSize: this.calcOptimalPartSize(size), + directory: os.tmpdir(), + }) + .on('beforeChunkStarted', async () => { + permit = await this.partUploadSemaphore.acquire() + }) + .on('chunkStarted', (filepath) => { + pendingChunkFilepath = filepath + }) + .on('chunkFinished', ({ path, size: partSize }) => { + pendingChunkFilepath = null + + const acquiredPermit = permit + const partNumber = currentPartNumber++ + + offset += partSize + + const isFinalPart = size === offset + + // biome-ignore lint/suspicious/noAsyncPromiseExecutor: it's fine + const deferred = new Promise(async (resolve, reject) => { + try { + // Only the first chunk of each PATCH request can prepend + // an incomplete part (last chunk) from the previous request. + const readable = fs.createReadStream(path) + readable.on('error', reject) + + if (partSize >= this.minPartSize || isFinalPart) { + await this.uploadPart(metadata, readable, partNumber) + } else { + await this.uploadIncompletePart(metadata.file.id, readable) + } + + bytesUploaded += partSize + resolve() + } catch (error) { + reject(error) + } finally { + fsProm.rm(path).catch(() => { + /* ignore */ + }) + acquiredPermit?.release() + } + }) + + promises.push(deferred) + }) + .on('chunkError', () => { + permit?.release() + }) + + try { + await streamProm.pipeline(readStream, splitterStream) + } catch (error) { + if (pendingChunkFilepath !== null) { + try { + await fsProm.rm(pendingChunkFilepath) + } catch { + log(`[${metadata.file.id}] failed to remove chunk ${pendingChunkFilepath}`) + } + } + + promises.push(Promise.reject(error)) + } finally { + await Promise.all(promises) + } + + return bytesUploaded + } + + /** + * Completes a multipart upload on S3. + * This is where S3 concatenates all the uploaded parts. + */ + private async finishMultipartUpload(metadata: MetadataValue, parts: Array) { + const response = await this.client.completeMultipartUpload({ + Bucket: this.bucket, + Key: metadata.file.id, + UploadId: metadata['upload-id'], + MultipartUpload: { + Parts: parts.map((part) => { + return { + ETag: part.ETag, + PartNumber: part.PartNumber, + } + }), + }, + }) + return response.Location + } + + /** + * Gets the number of complete parts/chunks already uploaded to S3. + * Retrieves only consecutive parts. + */ + private async retrieveParts( + id: string, + partNumberMarker?: string + ): Promise> { + const metadata = await this.getMetadata(id) + + const params: AWS.ListPartsCommandInput = { + Bucket: this.bucket, + Key: id, + UploadId: metadata['upload-id'], + PartNumberMarker: partNumberMarker, + } + + const data = await this.client.listParts(params) + + let parts = data.Parts ?? [] + + if (data.IsTruncated) { + const rest = await this.retrieveParts(id, data.NextPartNumberMarker) + parts = [...parts, ...rest] + } + + if (!partNumberMarker) { + // biome-ignore lint/style/noNonNullAssertion: it's fine + parts.sort((a, b) => a.PartNumber! - b.PartNumber!) + } + + return parts + } + + /** + * Removes cached data for a given file. + */ + private async clearCache(id: string) { + log(`[${id}] removing cached data`) + await this.cache.delete(id) + } + + private calcOptimalPartSize(size?: number): number { + // When upload size is not know we assume largest possible value (`maxUploadSize`) + if (size === undefined) { + size = this.maxUploadSize + } + + let optimalPartSize: number + + // When upload is smaller or equal to PreferredPartSize, we upload in just one part. + if (size <= this.preferredPartSize) { + optimalPartSize = size + } + // Does the upload fit in MaxMultipartParts parts or less with PreferredPartSize. + else if (size <= this.preferredPartSize * this.maxMultipartParts) { + optimalPartSize = this.preferredPartSize + // The upload is too big for the preferred size. + // We devide the size with the max amount of parts and round it up. + } else { + optimalPartSize = Math.ceil(size / this.maxMultipartParts) + } + + return optimalPartSize + } + + /** + * Creates a multipart upload on S3 attaching any metadata to it. + * Also, a `${file_id}.info` file is created which holds some information + * about the upload itself like: `upload-id`, `upload-length`, etc. + */ + public async create(upload: Upload) { + log(`[${upload.id}] initializing multipart upload`) + const request: AWS.CreateMultipartUploadCommandInput = { + Bucket: this.bucket, + Key: upload.id, + Metadata: { 'tus-version': TUS_RESUMABLE }, + } + + if (upload.metadata?.contentType) { + request.ContentType = upload.metadata.contentType + } + + if (upload.metadata?.cacheControl) { + request.CacheControl = upload.metadata.cacheControl + } + + upload.creation_date = new Date().toISOString() + + const res = await this.client.createMultipartUpload(request) + upload.storage = { + type: 's3', + path: res.Key as string, + bucket: this.bucket, + } + await this.saveMetadata(upload, res.UploadId as string) + log(`[${upload.id}] multipart upload created (${res.UploadId})`) + + return upload + } + + async read(id: string) { + const data = await this.client.getObject({ + Bucket: this.bucket, + Key: id, + }) + return data.Body as Readable + } + + /** + * Write to the file, starting at the provided offset + */ + public async write(src: stream.Readable, id: string, offset: number): Promise { + // Metadata request needs to happen first + const metadata = await this.getMetadata(id) + const parts = await this.retrieveParts(id) + // biome-ignore lint/style/noNonNullAssertion: it's fine + const partNumber: number = parts.length > 0 ? parts[parts.length - 1].PartNumber! : 0 + const nextPartNumber = partNumber + 1 + + const incompletePart = await this.downloadIncompletePart(id) + const requestedOffset = offset + + if (incompletePart) { + // once the file is on disk, we delete the incomplete part + await this.deleteIncompletePart(id) + + offset = requestedOffset - incompletePart.size + src = new MultiStream([incompletePart.createReader({ cleanUpOnEnd: true }), src]) + } + + const bytesUploaded = await this.uploadParts(metadata, src, nextPartNumber, offset) + + // The size of the incomplete part should not be counted, because the + // process of the incomplete part should be fully transparent to the user. + const newOffset = requestedOffset + bytesUploaded - (incompletePart?.size ?? 0) + + if (metadata.file.size === newOffset) { + try { + const parts = await this.retrieveParts(id) + await this.finishMultipartUpload(metadata, parts) + await this.completeMetadata(metadata.file) + await this.clearCache(id) + } catch (error) { + log(`[${id}] failed to finish upload`, error) + throw error + } + } + + return newOffset + } + + public async getUpload(id: string): Promise { + let metadata: MetadataValue + try { + metadata = await this.getMetadata(id) + } catch (error) { + log('getUpload: No file found.', error) + throw ERRORS.FILE_NOT_FOUND + } + + let offset = 0 + + try { + const parts = await this.retrieveParts(id) + offset = calcOffsetFromParts(parts) + } catch (error: any) { + // Check if the error is caused by the upload not being found. This happens + // when the multipart upload has already been completed or aborted. Since + // we already found the info object, we know that the upload has been + // completed and therefore can ensure the the offset is the size. + // AWS S3 returns NoSuchUpload, but other implementations, such as DigitalOcean + // Spaces, can also return NoSuchKey. + if (error.Code === 'NoSuchUpload' || error.Code === 'NoSuchKey') { + return new Upload({ + ...metadata.file, + offset: metadata.file.size as number, + size: metadata.file.size, + metadata: metadata.file.metadata, + storage: metadata.file.storage, + }) + } + + log(error) + throw error + } + + const incompletePartSize = await this.getIncompletePartSize(id) + + return new Upload({ + ...metadata.file, + offset: offset + (incompletePartSize ?? 0), + size: metadata.file.size, + storage: metadata.file.storage, + }) + } + + public async declareUploadLength(file_id: string, upload_length: number) { + const { file, 'upload-id': uploadId } = await this.getMetadata(file_id) + if (!file) { + throw ERRORS.FILE_NOT_FOUND + } + + file.size = upload_length + + await this.saveMetadata(file, uploadId) + } + + public async remove(id: string): Promise { + try { + const { 'upload-id': uploadId } = await this.getMetadata(id) + if (uploadId) { + await this.client.abortMultipartUpload({ + Bucket: this.bucket, + Key: id, + UploadId: uploadId, + }) + } + } catch (error: any) { + if (error?.code && ['NotFound', 'NoSuchKey', 'NoSuchUpload'].includes(error.Code)) { + log('remove: No file found.', error) + throw ERRORS.FILE_NOT_FOUND + } + throw error + } + + await this.client.deleteObjects({ + Bucket: this.bucket, + Delete: { + Objects: [{ Key: id }, { Key: this.infoKey(id) }], + }, + }) + + this.clearCache(id) + } + + protected getExpirationDate(created_at: string) { + const date = new Date(created_at) + + return new Date(date.getTime() + this.getExpiration()) + } + + getExpiration(): number { + return this.expirationPeriodInMilliseconds + } + + async deleteExpired(): Promise { + if (this.getExpiration() === 0) { + return 0 + } + + let keyMarker: string | undefined = undefined + let uploadIdMarker: string | undefined = undefined + let isTruncated = true + let deleted = 0 + + while (isTruncated) { + const listResponse: AWS.ListMultipartUploadsCommandOutput = + await this.client.listMultipartUploads({ + Bucket: this.bucket, + KeyMarker: keyMarker, + UploadIdMarker: uploadIdMarker, + }) + + const expiredUploads = + listResponse.Uploads?.filter((multiPartUpload) => { + const initiatedDate = multiPartUpload.Initiated + return ( + initiatedDate && + new Date().getTime() > + this.getExpirationDate(initiatedDate.toISOString()).getTime() + ) + }) || [] + + const objectsToDelete = expiredUploads.reduce( + (all, expiredUpload) => { + all.push( + { + key: this.infoKey(expiredUpload.Key as string), + }, + { + key: this.partKey(expiredUpload.Key as string, true), + } + ) + return all + }, + [] as { key: string }[] + ) + + const deletions: Promise[] = [] + + // Batch delete 1000 items at a time + while (objectsToDelete.length > 0) { + const objects = objectsToDelete.splice(0, 1000) + deletions.push( + this.client.deleteObjects({ + Bucket: this.bucket, + Delete: { + Objects: objects.map((object) => ({ + Key: object.key, + })), + }, + }) + ) + } + + const [objectsDeleted] = await Promise.all([ + Promise.all(deletions), + ...expiredUploads.map((expiredUpload) => { + return this.client.abortMultipartUpload({ + Bucket: this.bucket, + Key: expiredUpload.Key, + UploadId: expiredUpload.UploadId, + }) + }), + ]) + + deleted += objectsDeleted.reduce((all, acc) => all + (acc.Deleted?.length ?? 0), 0) + + isTruncated = Boolean(listResponse.IsTruncated) + + if (isTruncated) { + keyMarker = listResponse.NextKeyMarker + uploadIdMarker = listResponse.NextUploadIdMarker + } + } + + return deleted + } + + private async uniqueTmpFileName(template: string): Promise { + let tries = 0 + const maxTries = 10 + + while (tries < maxTries) { + const fileName = + template + crypto.randomBytes(10).toString('base64url').slice(0, 10) + const filePath = path.join(os.tmpdir(), fileName) + + try { + await fsProm.lstat(filePath) + // If no error, file exists, so try again + tries++ + } catch (e: any) { + if (e.code === 'ENOENT') { + // File does not exist, return the path + return filePath + } + throw e // For other errors, rethrow + } + } + + throw new Error(`Could not find a unique file name after ${maxTries} tries`) + } +} \ No newline at end of file diff --git a/packages/tus/src/types.ts b/packages/tus/src/types.ts new file mode 100644 index 0000000..d502366 --- /dev/null +++ b/packages/tus/src/types.ts @@ -0,0 +1,211 @@ +/** + * @file tus协议服务端类型定义文件 + * @description 定义了tus文件上传服务器所需的各种类型接口 + * @version 1.0.0 + */ + +import type http from 'node:http' +import { Locker, Upload } from './utils' + + +/** + * tus服务器配置选项接口 + * @interface ServerOptions + * @description 包含了配置tus服务器所需的所有选项 + */ +export type ServerOptions = { + /** + * 服务器接收上传请求的路由路径 + * @example '/files' + */ + path: string + + /** + * 允许上传的最大文件大小限制 + * @param req HTTP请求对象 + * @param uploadId 上传ID + * @returns 文件大小限制(字节) + */ + maxSize?: + | number + | ((req: http.IncomingMessage, uploadId: string | null) => Promise | number) + + /** + * 是否返回相对URL作为Location响应头 + * @description 默认返回绝对URL,设为true则返回相对URL + */ + relativeLocation?: boolean + + /** + * 是否支持代理转发头 + * @description 允许使用Forwarded、X-Forwarded-Proto和X-Forwarded-Host头 + * 来覆盖服务器返回的Location头 + */ + respectForwardedHeaders?: boolean + + /** + * CORS允许的自定义请求头 + * @description 这些头会被添加到Access-Control-Allow-Headers响应头中 + */ + allowedHeaders?: string[] + + /** + * 是否允许跨域请求携带凭证 + * @description 设置Access-Control-Allow-Credentials响应头 + */ + allowedCredentials?: boolean + + /** + * CORS允许的来源域名列表 + * @description 这些域名会被添加到Access-Control-Allow-Origin响应头中 + */ + allowedOrigins?: string[] + + /** + * 上传进度事件发送间隔(毫秒) + * @description 通过EVENTS.POST_RECEIVE_V2事件发送上传进度的时间间隔 + */ + postReceiveInterval?: number + + /** + * 自定义上传URL生成逻辑 + * @param req HTTP请求对象 + * @param options URL生成选项 + * @returns 生成的上传URL + */ + generateUrl?: ( + req: http.IncomingMessage, + options: { proto: string; host: string; path: string; id: string } + ) => string + + /** + * 自定义从请求中提取上传ID的逻辑 + * @param req HTTP请求对象 + * @param lastPath URL最后一段路径 + * @returns 提取的上传ID + */ + getFileIdFromRequest?: ( + req: http.IncomingMessage, + lastPath?: string + ) => string | undefined + + /** + * 自定义文件命名函数 + * @description 默认使用crypto.randomBytes(16).toString('hex')生成 + * @param req HTTP请求对象 + * @param metadata 上传文件的元数据 + * @returns 生成的文件名 + */ + namingFunction?: ( + req: http.IncomingMessage, + metadata?: Record + ) => string | Promise + + /** + * 文件锁接口实现 + * @description 用于确保对上传文件和元数据的独占访问 + */ + locker: + | Locker + | Promise + | ((req: http.IncomingMessage) => Locker | Promise) + + /** + * 锁清理超时时间(毫秒) + * @description 服务器等待已取消的锁进行清理的最长时间 + */ + lockDrainTimeout?: number + + /** + * 是否禁止终止已完成的上传 + * @description 设为true时无法删除已上传完成的文件 + */ + disableTerminationForFinishedUploads?: boolean + + /** + * 上传创建前的钩子函数 + * @description 在新上传创建前调用,可用于验证上传元数据或添加响应头 + * @param req HTTP请求对象 + * @param res HTTP响应对象 + * @param upload 上传对象 + * @throws 抛出错误时会中止请求 + */ + onUploadCreate?: ( + req: http.IncomingMessage, + res: http.ServerResponse, + upload: Upload + ) => Promise< + http.ServerResponse | { res: http.ServerResponse; metadata?: Upload['metadata'] } + > + + /** + * 上传完成后的钩子函数 + * @description 在上传完成后、返回响应前调用,可用于后处理验证 + * @param req HTTP请求对象 + * @param res HTTP响应对象 + * @param upload 上传对象 + * @throws 抛出错误时会中止请求 + */ + onUploadFinish?: ( + req: http.IncomingMessage, + res: http.ServerResponse, + upload: Upload + ) => Promise< + | http.ServerResponse + | { + res: http.ServerResponse + status_code?: number + headers?: Record + body?: string + } + > + + /** + * 请求接收时的钩子函数 + * @description 在收到新请求时调用 + * @param req HTTP请求对象 + * @param res HTTP响应对象 + * @param uploadId 上传ID + */ + onIncomingRequest?: ( + req: http.IncomingMessage, + res: http.ServerResponse, + uploadId: string + ) => Promise + + /** + * 错误响应处理钩子 + * @description 在服务器即将发送错误响应时调用,用于自定义错误处理 + * @param req HTTP请求对象 + * @param res HTTP响应对象 + * @param err 错误对象 + */ + onResponseError?: ( + req: http.IncomingMessage, + res: http.ServerResponse, + err: Error | { status_code: number; body: string } + ) => + | Promise<{ status_code: number; body: string } | undefined> + | { status_code: number; body: string } + | undefined +} + +/** + * 路由处理器类型 + * @description HTTP请求处理函数类型定义 + */ +export type RouteHandler = (req: http.IncomingMessage, res: http.ServerResponse) => void + +/** + * 工具类型:使指定属性变为可选 + * @template T 原始类型 + * @template K 需要变为可选的属性键 + */ +export type WithOptional = Omit & { [P in K]+?: T[P] } + +/** + * 工具类型:使指定属性变为必需 + * @template T 原始类型 + * @template K 需要变为必需的属性键 + */ +export type WithRequired = T & { [P in K]-?: T[P] } \ No newline at end of file diff --git a/packages/tus/src/utils/constants.ts b/packages/tus/src/utils/constants.ts new file mode 100644 index 0000000..4eed674 --- /dev/null +++ b/packages/tus/src/utils/constants.ts @@ -0,0 +1,132 @@ +/** + * 该模块定义了与TUS协议相关的常量,包括请求方法、头部信息、错误码、事件类型等。 + * TUS是一种基于HTTP的可恢复文件上传协议,适用于大文件上传场景。 + */ + +// 定义TUS协议支持的HTTP请求方法 +export const REQUEST_METHODS = ['POST', 'HEAD', 'PATCH', 'OPTIONS', 'DELETE'] as const + +// 定义TUS协议中使用的HTTP头部信息 +export const HEADERS = [ + 'Authorization', + 'Content-Type', + 'Location', + 'Tus-Extension', + 'Tus-Max-Size', + 'Tus-Resumable', + 'Tus-Version', + 'Upload-Concat', + 'Upload-Defer-Length', + 'Upload-Length', + 'Upload-Metadata', + 'Upload-Offset', + 'X-HTTP-Method-Override', + 'X-Requested-With', + 'X-Forwarded-Host', + 'X-Forwarded-Proto', + 'Forwarded', +] as const + + +// 将头部信息转换为小写形式,便于处理 +export const HEADERS_LOWERCASE = HEADERS.map((header) => { + return header.toLowerCase() +}) as Array> + +// 定义允许的头部信息、请求方法和暴露的头部信息 +export const ALLOWED_HEADERS = HEADERS.join(', ') +export const ALLOWED_METHODS = REQUEST_METHODS.join(', ') +export const EXPOSED_HEADERS = HEADERS.join(', ') + +// 定义TUS协议中可能遇到的错误信息 +export const ERRORS = { + MISSING_OFFSET: { + status_code: 403, + body: 'Upload-Offset header required\n', + }, + ABORTED: { + status_code: 400, + body: 'Request aborted due to lock acquired', + }, + INVALID_TERMINATION: { + status_code: 400, + body: 'Cannot terminate an already completed upload', + }, + ERR_LOCK_TIMEOUT: { + status_code: 500, + body: 'failed to acquire lock before timeout', + }, + INVALID_CONTENT_TYPE: { + status_code: 403, + body: 'Content-Type header required\n', + }, + FILE_NOT_FOUND: { + status_code: 404, + body: 'The file for this url was not found\n', + }, + INVALID_OFFSET: { + status_code: 409, + body: 'Upload-Offset conflict\n', + }, + FILE_NO_LONGER_EXISTS: { + status_code: 410, + body: 'The file for this url no longer exists\n', + }, + ERR_SIZE_EXCEEDED: { + status_code: 413, + body: "upload's size exceeded\n", + }, + ERR_MAX_SIZE_EXCEEDED: { + status_code: 413, + body: 'Maximum size exceeded\n', + }, + INVALID_LENGTH: { + status_code: 400, + body: 'Upload-Length or Upload-Defer-Length header required\n', + }, + INVALID_METADATA: { + status_code: 400, + body: 'Upload-Metadata is invalid. It MUST consist of one or more comma-separated key-value pairs. The key and value MUST be separated by a space. The key MUST NOT contain spaces and commas and MUST NOT be empty. The key SHOULD be ASCII encoded and the value MUST be Base64 encoded. All keys MUST be unique', + }, + UNKNOWN_ERROR: { + status_code: 500, + body: 'Something went wrong with that request\n', + }, + FILE_WRITE_ERROR: { + status_code: 500, + body: 'Something went wrong receiving the file\n', + }, + UNSUPPORTED_CONCATENATION_EXTENSION: { + status_code: 501, + body: 'Concatenation extension is not (yet) supported. Disable parallel uploads in the tus client.\n', + }, + UNSUPPORTED_CREATION_DEFER_LENGTH_EXTENSION: { + status_code: 501, + body: 'creation-defer-length extension is not (yet) supported.\n', + }, + UNSUPPORTED_EXPIRATION_EXTENSION: { + status_code: 501, + body: 'expiration extension is not (yet) supported.\n', + }, +} as const + +// 定义TUS协议中的事件类型 +export const POST_CREATE = 'POST_CREATE' as const +/** @deprecated this is almost the same as POST_FINISH, use POST_RECEIVE_V2 instead */ +export const POST_RECEIVE = 'POST_RECEIVE' as const +export const POST_RECEIVE_V2 = 'POST_RECEIVE_V2' as const +export const POST_FINISH = 'POST_FINISH' as const +export const POST_TERMINATE = 'POST_TERMINATE' as const +export const EVENTS = { + POST_CREATE, + /** @deprecated this is almost the same as POST_FINISH, use POST_RECEIVE_V2 instead */ + POST_RECEIVE, + POST_RECEIVE_V2, + POST_FINISH, + POST_TERMINATE, +} as const + +// 定义TUS协议中的最大年龄和版本信息 +export const MAX_AGE = 86_400 as const +export const TUS_RESUMABLE = '1.0.0' as const +export const TUS_VERSION = ['1.0.0'] as const \ No newline at end of file diff --git a/packages/tus/src/utils/index.ts b/packages/tus/src/utils/index.ts new file mode 100644 index 0000000..78b880f --- /dev/null +++ b/packages/tus/src/utils/index.ts @@ -0,0 +1,3 @@ +export * from './models' +export * from './constants' +export * from './kvstores' diff --git a/packages/tus/src/utils/kvstores/FileKvStore.ts b/packages/tus/src/utils/kvstores/FileKvStore.ts new file mode 100644 index 0000000..752a776 --- /dev/null +++ b/packages/tus/src/utils/kvstores/FileKvStore.ts @@ -0,0 +1,94 @@ +import fs from 'node:fs/promises' +import path from 'node:path' + +import type {KvStore} from './Types' +import type {Upload} from '../models' + +/** + * 文件键值存储(FileKvStore) + * + * @description 基于文件系统的键值对存储实现,专门用于存储上传文件的元数据 + * @remarks + * - 将上传文件的JSON元数据存储在磁盘上,与上传文件同目录 + * - 使用队列机制确保并发安全,每次仅处理一个操作 + * + * @typeparam T 存储的数据类型,默认为Upload类型 + */ +export class FileKvStore implements KvStore { + /** 存储目录路径 */ + directory: string + /** + * 构造函数 + * + * @param path 指定存储元数据的目录路径 + */ + constructor(path: string) { + this.directory = path + } + /** + * 根据键获取存储的值 + * + * @param key 键名 + * @returns 返回对应的值,如果不存在则返回undefined + */ + async get(key: string): Promise { + try { + // 读取对应键的JSON文件 + const buffer = await fs.readFile(this.resolve(key), 'utf8') + // 解析JSON并返回 + return JSON.parse(buffer as string) + } catch { + // 文件不存在或读取失败时返回undefined + return undefined + } + } + /** + * 存储键值对 + * @param key 键名 + * @param value 要存储的值 + */ + async set(key: string, value: T): Promise { + // 将值转换为JSON并写入文件 + await fs.writeFile(this.resolve(key), JSON.stringify(value)) + } + /** + * 删除指定键的值 + * + * @param key 要删除的键名 + */ + async delete(key: string): Promise { + // 删除对应的JSON文件 + await fs.rm(this.resolve(key)) + } + + /** + * 列出所有存储的键 + * + * @returns 返回已存储的键名数组 + */ + async list(): Promise> { + // 读取目录中的所有文件 + const files = await fs.readdir(this.directory) + // 对文件名进行排序 + const sorted = files.sort((a, b) => a.localeCompare(b)) + // 提取文件名(不包含扩展名) + const name = (file: string) => path.basename(file, '.json') + // 过滤出有效的tus文件ID + // 仅保留成对出现的文件(文件名相同,一个有.json扩展名) + return sorted.filter( + (file, idx) => idx < sorted.length - 1 && name(file) === name(sorted[idx + 1]) + ) + } + + /** + * 将键转换为完整的文件路径 + * + * @param key 键名 + * @returns 返回完整的文件路径 + * @private + */ + private resolve(key: string): string { + // 将键名转换为完整的JSON文件路径 + return path.resolve(this.directory, `${key}.json`) + } +} \ No newline at end of file diff --git a/packages/tus/src/utils/kvstores/IoRedisKvStore.ts b/packages/tus/src/utils/kvstores/IoRedisKvStore.ts new file mode 100644 index 0000000..8d4a189 --- /dev/null +++ b/packages/tus/src/utils/kvstores/IoRedisKvStore.ts @@ -0,0 +1,54 @@ +import type {Redis as IoRedis} from 'ioredis' +import type {KvStore} from './Types' +import type {Upload} from '../models' + +export class IoRedisKvStore implements KvStore { + constructor( + private redis: IoRedis, + private prefix = '' + ) { + this.redis = redis + this.prefix = prefix + } + + private prefixed(key: string): string { + return `${this.prefix}${key}` + } + + async get(key: string): Promise { + return this.deserializeValue(await this.redis.get(this.prefixed(key))) + } + + async set(key: string, value: T): Promise { + await this.redis.set(this.prefixed(key), this.serializeValue(value)) + } + + async delete(key: string): Promise { + await this.redis.del(this.prefixed(key)) + } + + async list(): Promise> { + const keys = new Set() + let cursor = '0' + do { + const [next, batch] = await this.redis.scan( + cursor, + 'MATCH', + this.prefixed('*'), + 'COUNT', + '20' + ) + cursor = next + for (const key of batch) keys.add(key) + } while (cursor !== '0') + return Array.from(keys) + } + + private serializeValue(value: T): string { + return JSON.stringify(value) + } + + private deserializeValue(buffer: string | null): T | undefined { + return buffer ? JSON.parse(buffer) : undefined + } +} diff --git a/packages/tus/src/utils/kvstores/MemoryKvStore.ts b/packages/tus/src/utils/kvstores/MemoryKvStore.ts new file mode 100644 index 0000000..8c01a52 --- /dev/null +++ b/packages/tus/src/utils/kvstores/MemoryKvStore.ts @@ -0,0 +1,26 @@ +import type {Upload} from '../models' +import type {KvStore} from './Types' + +/** + * Memory based configstore. + * Used mostly for unit tests. + */ +export class MemoryKvStore implements KvStore { + data: Map = new Map() + + async get(key: string): Promise { + return this.data.get(key) + } + + async set(key: string, value: T): Promise { + this.data.set(key, value) + } + + async delete(key: string): Promise { + this.data.delete(key) + } + + async list(): Promise> { + return [...this.data.keys()] + } +} diff --git a/packages/tus/src/utils/kvstores/RedisKvStore.ts b/packages/tus/src/utils/kvstores/RedisKvStore.ts new file mode 100644 index 0000000..c62717b --- /dev/null +++ b/packages/tus/src/utils/kvstores/RedisKvStore.ts @@ -0,0 +1,94 @@ +import type { RedisClientType } from '@redis/client' +import type { KvStore } from './Types' +import type { Upload } from '../models' + +/** + * Redis 基于键值存储的配置存储类。 + * 该类实现了 KvStore 接口,使用 Redis 作为后端存储,支持数据的增删改查操作。 + * + * 使用场景: + * - 适用于需要持久化存储键值对数据的场景。 + * - 适用于需要高性能、高可用性的数据存储需求。 + * + * @author Mitja Puzigaća + */ +export class RedisKvStore implements KvStore { + /** + * 构造函数,初始化 RedisKvStore 实例。 + * + * @param redis Redis 客户端实例,用于与 Redis 服务器进行交互。 + * @param prefix 键的前缀,用于区分不同的存储空间,默认为空字符串。 + */ + constructor( + private redis: RedisClientType, + private prefix = '' + ) { + this.redis = redis + this.prefix = prefix + } + + /** + * 根据键获取存储的值。 + * + * @param key 要获取的值的键。 + * @returns 返回解析后的值,如果键不存在则返回 undefined。 + */ + async get(key: string): Promise { + return this.deserializeValue(await this.redis.get(this.prefix + key)) + } + + /** + * 设置键值对。 + * + * @param key 要设置的键。 + * @param value 要设置的值。 + */ + async set(key: string, value: T): Promise { + await this.redis.set(this.prefix + key, this.serializeValue(value)) + } + + /** + * 删除指定的键值对。 + * + * @param key 要删除的键。 + */ + async delete(key: string): Promise { + await this.redis.del(this.prefix + key) + } + + /** + * 列出所有键。 + * + * @returns 返回所有键的数组。 + */ + async list(): Promise> { + const keys = new Set() + let cursor = 0 + do { + const result = await this.redis.scan(cursor, { MATCH: `${this.prefix}*`, COUNT: 20 }) + cursor = result.cursor + for (const key of result.keys) keys.add(key) + } while (cursor !== 0) + return Array.from(keys) + } + + /** + * 序列化值。 + * + * @param value 要序列化的值。 + * @returns 返回序列化后的字符串。 + */ + private serializeValue(value: T): string { + return JSON.stringify(value) + } + + /** + * 反序列化值。 + * + * @param buffer 要反序列化的字符串。 + * @returns 返回反序列化后的值,如果字符串为空则返回 undefined。 + */ + private deserializeValue(buffer: string | null): T | undefined { + return buffer ? JSON.parse(buffer) : undefined + } +} \ No newline at end of file diff --git a/packages/tus/src/utils/kvstores/Types.ts b/packages/tus/src/utils/kvstores/Types.ts new file mode 100644 index 0000000..69e571c --- /dev/null +++ b/packages/tus/src/utils/kvstores/Types.ts @@ -0,0 +1,43 @@ +/** + * 键值存储接口定义模块 + * @description 提供通用的键值存储抽象接口,用于管理上传相关的持久化数据 + * @module KvStore + * @remarks 支持基本的增删改查操作,适用于多种存储后端实现 + */ +import type { Upload } from '../models' + +/** + * 键值存储接口 + * @template T 存储的数据类型,默认为Upload + * @description 定义了通用的键值存储操作方法 + * @interface + */ +export interface KvStore { + /** + * 根据键获取对应的值 + * @param key 查询的键 + * @returns 异步返回对应的值,如果不存在则返回undefined + */ + get(key: string): Promise + + /** + * 存储键值对 + * @param key 存储的键 + * @param value 存储的值 + * @returns 异步操作,完成存储 + */ + set(key: string, value: T): Promise + + /** + * 删除指定键的值 + * @param key 要删除的键 + * @returns 异步操作,完成删除 + */ + delete(key: string): Promise + + /** + * 可选的列出所有键的方法 + * @returns 异步返回所有键的数组,可选实现 + */ + list?(): Promise> +} \ No newline at end of file diff --git a/packages/tus/src/utils/kvstores/index.ts b/packages/tus/src/utils/kvstores/index.ts new file mode 100644 index 0000000..e8f5e4b --- /dev/null +++ b/packages/tus/src/utils/kvstores/index.ts @@ -0,0 +1,5 @@ +export { FileKvStore } from './FileKvStore' +export { MemoryKvStore } from './MemoryKvStore' +export { RedisKvStore } from './RedisKvStore' +export { IoRedisKvStore } from './IoRedisKvStore' +export type { KvStore } from './Types' diff --git a/packages/tus/src/utils/models/Context.ts b/packages/tus/src/utils/models/Context.ts new file mode 100644 index 0000000..8486fcf --- /dev/null +++ b/packages/tus/src/utils/models/Context.ts @@ -0,0 +1,14 @@ +/** + * 本模块定义了CancellationContext接口,用于管理请求的终止机制。 + * 该接口提供了两种请求终止方式:立即中止和优雅取消,适用于需要精细控制请求生命周期的场景。 + * + * 使用场景: + * - 文件上传/下载过程中出现错误需要立即停止 + * - 用户主动取消长时间运行的请求 + * - 需要优雅关闭资源连接的场景 + */ +export interface CancellationContext { + signal: AbortSignal + abort: () => void + cancel: () => void +} diff --git a/packages/tus/src/utils/models/DataStore.ts b/packages/tus/src/utils/models/DataStore.ts new file mode 100644 index 0000000..6d36759 --- /dev/null +++ b/packages/tus/src/utils/models/DataStore.ts @@ -0,0 +1,72 @@ +import EventEmitter from 'node:events' + +import {Upload} from './Upload' + +import type stream from 'node:stream' +import type http from 'node:http' + +export class DataStore extends EventEmitter { + extensions: string[] = [] + + hasExtension(extension: string) { + return this.extensions?.includes(extension) + } + + /** + * Called in POST requests. This method just creates a + * file, implementing the creation extension. + * + * http://tus.io/protocols/resumable-upload.html#creation + */ + async create(file: Upload) { + return file + } + + /** + * Called in DELETE requests. This method just deletes the file from the store. + * http://tus.io/protocols/resumable-upload.html#termination + */ + async remove(id: string) {} + + /** + * Called in PATCH requests. This method should write data + * to the DataStore file, and possibly implement the + * concatenation extension. + * + * http://tus.io/protocols/resumable-upload.html#concatenation + */ + async write( + stream: http.IncomingMessage | stream.Readable, + id: string, + offset: number + ) { + return 0 + } + + /** + * Called in HEAD requests. This method should return the bytes + * written to the DataStore, for the client to know where to resume + * the upload. + */ + async getUpload(id: string): Promise { + return new Upload({ + id, + size: 0, + offset: 0, + storage: {type: 'datastore', path: ''}, + }) + } + /** + * Called in PATCH requests when upload length is known after being defered. + */ + async declareUploadLength(id: string, upload_length: number) {} + /** + * Returns number of expired uploads that were deleted. + */ + async deleteExpired(): Promise { + return 0 + } + getExpiration(): number { + return 0 + } +} diff --git a/packages/tus/src/utils/models/Locker.ts b/packages/tus/src/utils/models/Locker.ts new file mode 100644 index 0000000..8e8629c --- /dev/null +++ b/packages/tus/src/utils/models/Locker.ts @@ -0,0 +1,12 @@ +export type RequestRelease = () => Promise | void + + +export interface Locker { + newLock(id: string): Lock +} + + +export interface Lock { + lock(cancelReq: RequestRelease): Promise + unlock(): Promise +} \ No newline at end of file diff --git a/packages/tus/src/utils/models/Metadata.ts b/packages/tus/src/utils/models/Metadata.ts new file mode 100644 index 0000000..a8a2914 --- /dev/null +++ b/packages/tus/src/utils/models/Metadata.ts @@ -0,0 +1,103 @@ +import type {Upload} from './Upload' + +// 定义ASCII码中的空格和逗号字符的码点 +const ASCII_SPACE = ' '.codePointAt(0) +const ASCII_COMMA = ','.codePointAt(0) +// 定义用于验证Base64字符串的正则表达式 +const BASE64_REGEX = /^[\d+/A-Za-z]*={0,2}$/ + +/** + * 验证元数据键的有效性 + * @param key 需要验证的键 + * @returns 如果键有效则返回true,否则返回false + */ +export function validateKey(key: string) { + // 如果键的长度为0,则无效 + if (key.length === 0) { + return false + } + + // 遍历键的每个字符,检查其码点是否在有效范围内 + for (let i = 0; i < key.length; ++i) { + const charCodePoint = key.codePointAt(i) as number + if ( + charCodePoint > 127 || // 非ASCII字符 + charCodePoint === ASCII_SPACE || // 空格字符 + charCodePoint === ASCII_COMMA // 逗号字符 + ) { + return false + } + } + + return true +} + +/** + * 验证元数据值的有效性 + * @param value 需要验证的值 + * @returns 如果值是有效的Base64字符串则返回true,否则返回false + */ +export function validateValue(value: string) { + // Base64字符串的长度必须是4的倍数 + if (value.length % 4 !== 0) { + return false + } + + // 使用正则表达式验证Base64字符串的格式 + return BASE64_REGEX.test(value) +} + +/** + * 解析元数据字符串 + * @param str 需要解析的元数据字符串 + * @returns 返回解析后的元数据对象 + * @throws 如果元数据字符串无效则抛出错误 + */ +export function parse(str?: string) { + const meta: Record = {} + + // 如果字符串为空或仅包含空白字符,则无效 + if (!str || str.trim().length === 0) { + throw new Error('Metadata string is not valid') + } + + // 遍历字符串中的每个键值对 + for (const pair of str.split(',')) { + const tokens = pair.split(' ') + const [key, value] = tokens + // 验证键和值的有效性,并确保键在元数据对象中不存在 + if ( + ((tokens.length === 1 && validateKey(key)) || + (tokens.length === 2 && validateKey(key) && validateValue(value))) && + !(key in meta) + ) { + // 如果值存在,则将其从Base64解码为UTF-8字符串 + const decodedValue = value ? Buffer.from(value, 'base64').toString('utf8') : null + meta[key] = decodedValue + } else { + throw new Error('Metadata string is not valid') + } + } + + return meta +} + +/** + * 将元数据对象序列化为字符串 + * @param metadata 需要序列化的元数据对象 + * @returns 返回序列化后的元数据字符串 + */ +export function stringify(metadata: NonNullable): string { + return Object.entries(metadata) + .map(([key, value]) => { + // 如果值为null,则仅返回键 + if (value === null) { + return key + } + + // 将值编码为Base64字符串,并与键组合 + const encodedValue = Buffer.from(value, 'utf8').toString('base64') + return `${key} ${encodedValue}` + }) + .join(',') +} \ No newline at end of file diff --git a/packages/tus/src/utils/models/StreamLimiter.ts b/packages/tus/src/utils/models/StreamLimiter.ts new file mode 100644 index 0000000..fd8e2ec --- /dev/null +++ b/packages/tus/src/utils/models/StreamLimiter.ts @@ -0,0 +1,54 @@ +import { Transform, type TransformCallback } from 'node:stream' +import { ERRORS } from '../constants' + +// TODO: create HttpError and use it everywhere instead of throwing objects +/** + * MaxFileExceededError 类用于表示文件大小超出限制的错误。 + * 继承自 Error 类,包含状态码和错误信息体。 + */ +export class MaxFileExceededError extends Error { + status_code: number + body: string + + constructor() { + super(ERRORS.ERR_MAX_SIZE_EXCEEDED.body) + this.status_code = ERRORS.ERR_MAX_SIZE_EXCEEDED.status_code + this.body = ERRORS.ERR_MAX_SIZE_EXCEEDED.body + Object.setPrototypeOf(this, MaxFileExceededError.prototype) + } +} + +/** + * StreamLimiter 类用于限制流的大小,确保流的大小不超过指定的最大值。 + * 继承自 Transform 类,用于对流进行转换操作。 + */ +export class StreamLimiter extends Transform { + private maxSize: number // 允许的最大流大小 + private currentSize = 0 // 当前流的大小 + + /** + * 构造函数,初始化 StreamLimiter 实例。 + * @param maxSize 允许的最大流大小。 + */ + constructor(maxSize: number) { + super() + this.maxSize = maxSize + } + + /** + * _transform 方法是 Transform 类的核心方法,用于处理流中的数据块。 + * 每次接收到数据块时,更新当前流的大小,并检查是否超出最大限制。 + * 如果超出限制,则抛出 MaxFileExceededError 错误。 + * @param chunk 当前处理的数据块。 + * @param encoding 数据块的编码格式。 + * @param callback 回调函数,用于处理完成后的操作。 + */ + _transform(chunk: Buffer, encoding: BufferEncoding, callback: TransformCallback): void { + this.currentSize += chunk.length // 更新当前流的大小 + if (this.currentSize > this.maxSize) { + callback(new MaxFileExceededError()) // 如果超出最大限制,抛出错误 + } else { + callback(null, chunk) // 否则,继续处理数据块 + } + } +} \ No newline at end of file diff --git a/packages/tus/src/utils/models/StreamSplitter.ts b/packages/tus/src/utils/models/StreamSplitter.ts new file mode 100644 index 0000000..4dd94da --- /dev/null +++ b/packages/tus/src/utils/models/StreamSplitter.ts @@ -0,0 +1,183 @@ +/* global BufferEncoding */ +import crypto from 'node:crypto' +import fs from 'node:fs/promises' +import path from 'node:path' +import stream from 'node:stream' + +/** + * 生成指定长度的随机字符串 + * @param size 字符串长度 + * @returns 随机生成的字符串 + */ +function randomString(size: number) { + return crypto.randomBytes(size).toString('base64url').slice(0, size) +} + +/** + * StreamSplitter 配置选项 + */ +type Options = { + chunkSize: number // 每个块的大小 + directory: string // 存储块的目录 +} + +/** + * 回调函数类型 + */ +type Callback = (error: Error | null) => void + +/** + * StreamSplitter 类,用于将流数据分割成指定大小的块 + */ +export class StreamSplitter extends stream.Writable { + directory: Options['directory'] // 存储块的目录 + currentChunkPath: string | null // 当前块的路径 + currentChunkSize: number // 当前块的大小 + fileHandle: fs.FileHandle | null // 当前块的文件句柄 + filenameTemplate: string // 文件名模板 + chunkSize: Options['chunkSize'] // 每个块的大小 + part: number // 当前块的编号 + + /** + * 构造函数 + * @param chunkSize 每个块的大小 + * @param directory 存储块的目录 + * @param options 可选的流写入选项 + */ + constructor({ chunkSize, directory }: Options, options?: stream.WritableOptions) { + super(options) + this.chunkSize = chunkSize + this.currentChunkPath = null + this.currentChunkSize = 0 + this.fileHandle = null + this.directory = directory + this.filenameTemplate = randomString(10) + this.part = 0 + + this.on('error', this._handleError.bind(this)) + } + + /** + * 写入数据到当前块 + * @param chunk 数据块 + * @param _ 编码方式(未使用) + * @param callback 回调函数 + */ + async _write(chunk: Buffer, _: BufferEncoding, callback: Callback) { + try { + // 如果当前没有文件句柄,则创建一个新的块 + if (this.fileHandle === null) { + await this._newChunk() + } + + let overflow = this.currentChunkSize + chunk.length - this.chunkSize + + // 如果写入的数据会导致当前块超过指定大小,则进行分割 + while (overflow > 0) { + // 只写入不超过指定大小的部分 + await this._writeChunk(chunk.subarray(0, chunk.length - overflow)) + await this._finishChunk() + + // 剩余的数据写入新的块 + await this._newChunk() + chunk = chunk.subarray(chunk.length - overflow, chunk.length) + overflow = this.currentChunkSize + chunk.length - this.chunkSize + } + + // 如果数据块小于指定大小,则直接写入 + await this._writeChunk(chunk) + callback(null) + } catch (error: any) { + callback(error) + } + } + + /** + * 完成写入操作 + * @param callback 回调函数 + */ + async _final(callback: Callback) { + if (this.fileHandle === null) { + callback(null) + return + } + + try { + await this._finishChunk() + callback(null) + } catch (error: any) { + callback(error) + } + } + + /** + * 写入数据块到文件 + * @param chunk 数据块 + */ + async _writeChunk(chunk: Buffer): Promise { + await fs.appendFile(this.fileHandle as fs.FileHandle, chunk) + this.currentChunkSize += chunk.length + } + + /** + * 处理错误 + */ + async _handleError() { + await this.emitEvent('chunkError', this.currentChunkPath) + // 如果发生错误,停止写入操作,防止数据丢失 + if (this.fileHandle === null) { return } + await this.fileHandle.close() + this.currentChunkPath = null + this.fileHandle = null + } + + /** + * 完成当前块的写入 + */ + async _finishChunk(): Promise { + if (this.fileHandle === null) { + return + } + + await this.fileHandle.close() + + await this.emitEvent('chunkFinished', { + path: this.currentChunkPath, + size: this.currentChunkSize, + }) + + this.currentChunkPath = null + this.fileHandle = null + this.currentChunkSize = 0 + this.part += 1 + } + + /** + * 触发事件 + * @param name 事件名称 + * @param payload 事件负载 + */ + async emitEvent(name: string, payload: T) { + const listeners = this.listeners(name) + for (const listener of listeners) { + await listener(payload) + } + } + + /** + * 创建新的块 + */ + async _newChunk(): Promise { + const currentChunkPath = path.join( + this.directory, + `${this.filenameTemplate}-${this.part}` + ) + await this.emitEvent('beforeChunkStarted', currentChunkPath) + this.currentChunkPath = currentChunkPath + + const fileHandle = await fs.open(this.currentChunkPath, 'w') + await this.emitEvent('chunkStarted', this.currentChunkPath) + this.currentChunkSize = 0 + this.fileHandle = fileHandle + } +} \ No newline at end of file diff --git a/packages/tus/src/utils/models/Uid.ts b/packages/tus/src/utils/models/Uid.ts new file mode 100644 index 0000000..ab592d3 --- /dev/null +++ b/packages/tus/src/utils/models/Uid.ts @@ -0,0 +1,21 @@ +import crypto from 'node:crypto' + +/** + * Uid 工具模块 + * + * 该模块提供了一个生成唯一标识符(UID)的工具函数。 + * 使用场景:在需要生成唯一标识符的场景下,如生成会话ID、临时文件名等。 + */ +export const Uid = { + /** + * 生成一个随机的唯一标识符 + * + * 该函数使用 Node.js 的 crypto 模块生成 16 字节的随机数据, + * 并将其转换为十六进制字符串形式,以确保唯一性和可读性。 + * + * @returns {string} 返回一个32字符长度的十六进制字符串,作为唯一标识符。 + */ + rand(): string { + return crypto.randomBytes(16).toString('hex') + }, +} \ No newline at end of file diff --git a/packages/tus/src/utils/models/Upload.ts b/packages/tus/src/utils/models/Upload.ts new file mode 100644 index 0000000..05809ae --- /dev/null +++ b/packages/tus/src/utils/models/Upload.ts @@ -0,0 +1,72 @@ +/** + * 模块: Upload + * 文件功能描述: 该模块定义了上传文件的数据模型,包括文件的基本信息和存储信息。 + * 使用场景: 用于管理文件上传过程中的元数据和状态,适用于需要处理文件上传的Web应用或服务。 + */ + +/** + * 类型: TUpload + * 核心功能概述: 定义了上传文件的数据结构,包括文件ID、大小、偏移量、元数据、存储信息等。 + */ +type TUpload = { + id: string // 文件唯一标识符 + size?: number // 文件大小,可选 + offset: number // 文件上传的偏移量 + metadata?: Record // 文件的元数据,可选 + storage?: { // 文件的存储信息,可选 + type: string // 存储类型 + path: string // 存储路径 + bucket?: string // 存储桶,可选 + } + creation_date?: string // 文件创建日期,可选 +} + +/** + * 类: Upload + * 核心功能概述: 封装了上传文件的数据模型,提供了文件信息的初始化和访问方法。 + * 设计模式解析: 使用构造函数模式初始化对象,通过getter方法提供属性访问。 + * 使用示例: + * const upload = new Upload({ id: '123', size: 1024, offset: 0 }); + * console.log(upload.sizeIsDeferred); // 检查文件大小是否延迟 + */ +export class Upload { + id: TUpload['id'] // 文件ID + metadata: TUpload['metadata'] // 文件元数据 + size: TUpload['size'] // 文件大小 + offset: TUpload['offset'] // 文件上传偏移量 + creation_date: TUpload['creation_date'] // 文件创建日期 + storage: TUpload['storage'] // 文件存储信息 + + /** + * 构造函数 + * 功能详细描述: 初始化Upload对象,检查必要的ID属性,并设置默认的创建日期。 + * 输入参数解析: + * - upload: TUpload类型,包含文件的基本信息和存储信息。 + * 异常处理机制: 如果未提供ID,则抛出错误。 + */ + constructor(upload: TUpload) { + // 检查ID是否存在,不存在则抛出错误 + if (!upload.id) { + throw new Error('[File] constructor must be given an ID') + } + + // 初始化属性 + this.id = upload.id + this.size = upload.size + this.offset = upload.offset + this.metadata = upload.metadata + this.storage = upload.storage + + // 如果未提供创建日期,则设置为当前时间 + this.creation_date = upload.creation_date ?? new Date().toISOString() + } + + /** + * 方法: sizeIsDeferred + * 功能详细描述: 检查文件大小是否未定义,即是否延迟上传。 + * 返回值说明: 返回布尔值,true表示文件大小未定义,false表示已定义。 + */ + get sizeIsDeferred(): boolean { + return this.size === undefined + } +} \ No newline at end of file diff --git a/packages/tus/src/utils/models/index.ts b/packages/tus/src/utils/models/index.ts new file mode 100644 index 0000000..80724bc --- /dev/null +++ b/packages/tus/src/utils/models/index.ts @@ -0,0 +1,8 @@ +export { DataStore } from './DataStore' +export * as Metadata from './Metadata' +export { StreamSplitter } from './StreamSplitter' +export { StreamLimiter } from './StreamLimiter' +export { Uid } from './Uid' +export { Upload } from './Upload' +export type { Locker, Lock, RequestRelease } from './Locker' +export type { CancellationContext } from './Context' diff --git a/packages/tus/src/validators/HeaderValidator.ts b/packages/tus/src/validators/HeaderValidator.ts new file mode 100644 index 0000000..48ad2ee --- /dev/null +++ b/packages/tus/src/validators/HeaderValidator.ts @@ -0,0 +1,138 @@ +/** + * TUS协议头部验证器 + * + * 该模块实现了TUS协议中各种HTTP头部的验证逻辑 + * TUS是一个用于可恢复文件上传的开放协议 + * + * @version 1.0.0 + * @see https://tus.io/protocols/resumable-upload.html + */ + +import { Metadata, TUS_VERSION, TUS_RESUMABLE } from "../utils" + + +/** 验证器函数类型定义,接收可选的字符串值,返回布尔值表示验证结果 */ +type validator = (value?: string) => boolean + +/** + * TUS协议头部验证器映射表 + * 包含了所有TUS协议规定的HTTP头部的验证规则 + */ +export const validators = new Map([ + [ + 'upload-offset', + /** + * Upload-Offset头部验证 + * 用于指示资源内的字节偏移量 + * 必须是非负整数 + */ + (value) => { + const n = Number(value) + return Number.isInteger(n) && String(n) === value && n >= 0 + }, + ], + [ + 'upload-length', + /** + * Upload-Length头部验证 + * 表示整个上传文件的字节大小 + * 必须是非负整数 + */ + (value) => { + const n = Number(value) + return Number.isInteger(n) && String(n) === value && n >= 0 + }, + ], + [ + 'upload-defer-length', + /** + * Upload-Defer-Length头部验证 + * 表示上传大小当前未知,将在后续传输 + * 值必须为1,如果长度未延迟则必须省略此头部 + */ + (value) => value === '1', + ], + [ + 'upload-metadata', + /** + * Upload-Metadata头部验证 + * 必须由一个或多个逗号分隔的键值对组成 + * 键和值必须用空格分隔 + * 键不能包含空格和逗号且不能为空 + * 键应该是ASCII编码,值必须是Base64编码 + * 所有键必须唯一 + */ + (value) => { + try { + Metadata.parse(value) + return true + } catch { + return false + } + }, + ], + [ + 'x-forwarded-proto', + /** + * X-Forwarded-Proto头部验证 + * 用于标识客户端与服务器之间的协议 + * 仅允许http或https + */ + (value) => { + if (value === 'http' || value === 'https') { + return true + } + return false + }, + ], + [ + 'tus-version', + /** + * Tus-Version头部验证 + * 服务器支持的协议版本列表 + * 必须按服务器偏好排序,第一个是最优先的 + */ + (value) => { + return TUS_VERSION.includes(value as any) + }, + ], + [ + 'tus-resumable', + /** + * Tus-Resumable头部验证 + * 除OPTIONS请求外的每个请求和响应都必须包含 + * 值必须是客户端或服务器使用的协议版本 + */ + (value) => value === TUS_RESUMABLE, + ], + ['content-type', (value) => value === 'application/offset+octet-stream'], + [ + 'upload-concat', + /** + * Upload-Concat头部验证 + * 用于部分上传和最终上传的创建请求 + * 部分上传值必须为partial + * 最终上传值必须以final开头,后跟分号和空格分隔的部分上传URL列表 + */ + (value) => { + if (!value) return false + const valid_partial = value === 'partial' + const valid_final = value.startsWith('final;') + return valid_partial || valid_final + }, + ], +]) + +/** + * 验证HTTP头部值是否符合TUS协议规范 + * @param name 头部名称 + * @param value 头部值 + * @returns 验证是否通过 + */ +export function validateHeader(name: string, value?: string): boolean { + const lowercaseName = name.toLowerCase() + if (!validators.has(lowercaseName)) { + return true + } + return validators.get(lowercaseName)!(value) +} \ No newline at end of file diff --git a/packages/tus/tsconfig.json b/packages/tus/tsconfig.json new file mode 100644 index 0000000..5244c89 --- /dev/null +++ b/packages/tus/tsconfig.json @@ -0,0 +1,40 @@ +{ + "compilerOptions": { + "target": "es2022", + "module": "esnext", + "lib": [ + "DOM", + "es2022" + ], + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "moduleResolution": "node", + "removeComments": true, + "skipLibCheck": true, + "strict": true, + "isolatedModules": true, + "esModuleInterop": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noImplicitReturns": false, + "noFallthroughCasesInSwitch": false, + "noUncheckedIndexedAccess": false, + "noImplicitOverride": false, + "noPropertyAccessFromIndexSignature": false, + "emitDeclarationOnly": true, + "outDir": "dist", + "incremental": true, + "tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo" + }, + "include": [ + "src" + ], + "exclude": [ + "node_modules", + "dist", + "**/*.test.ts", + "**/*.spec.ts", + "**/__tests__" + ] +} \ No newline at end of file diff --git a/packages/tus/tsup.config.ts b/packages/tus/tsup.config.ts new file mode 100644 index 0000000..1eacf7a --- /dev/null +++ b/packages/tus/tsup.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + entry: ['src/index.ts'], + format: ['cjs', 'esm'], + splitting: false, + sourcemap: true, + clean: false, + dts: true +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f53bc75..da40f57 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -372,6 +372,61 @@ importers: specifier: ^5.8.3 version: 5.8.3 + packages/tus: + dependencies: + '@aws-sdk/client-s3': + specifier: ^3.723.0 + version: 3.817.0 + '@shopify/semaphore': + specifier: ^3.1.0 + version: 3.1.0 + debug: + specifier: ^4.4.0 + version: 4.4.1 + lodash.throttle: + specifier: ^4.1.1 + version: 4.1.1 + multistream: + specifier: ^4.1.0 + version: 4.1.0 + devDependencies: + '@redis/client': + specifier: ^1.6.0 + version: 1.6.1 + '@types/debug': + specifier: ^4.1.12 + version: 4.1.12 + '@types/lodash.throttle': + specifier: ^4.1.9 + version: 4.1.9 + '@types/multistream': + specifier: ^4.1.3 + version: 4.1.3 + '@types/node': + specifier: ^20.3.1 + version: 20.17.50 + concurrently: + specifier: ^8.0.0 + version: 8.2.2 + ioredis: + specifier: ^5.4.1 + version: 5.4.1 + rimraf: + specifier: ^6.0.1 + version: 6.0.1 + should: + specifier: ^13.2.3 + version: 13.2.3 + ts-node: + specifier: ^10.9.1 + version: 10.9.2(@swc/core@1.11.29(@swc/helpers@0.5.17))(@types/node@20.17.50)(typescript@5.8.3) + tsup: + specifier: ^8.3.5 + version: 8.5.0(@swc/core@1.11.29(@swc/helpers@0.5.17))(jiti@2.4.2)(postcss@8.5.3)(tsx@4.19.4)(typescript@5.8.3)(yaml@2.8.0) + typescript: + specifier: ^5.5.4 + version: 5.8.3 + packages/typescript-config: {} packages/ui: @@ -450,6 +505,157 @@ packages: peerDependencies: zod: ^3.20.2 + '@aws-crypto/crc32@5.2.0': + resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/crc32c@5.2.0': + resolution: {integrity: sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==} + + '@aws-crypto/sha1-browser@5.2.0': + resolution: {integrity: sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==} + + '@aws-crypto/sha256-browser@5.2.0': + resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==} + + '@aws-crypto/sha256-js@5.2.0': + resolution: {integrity: sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/supports-web-crypto@5.2.0': + resolution: {integrity: sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==} + + '@aws-crypto/util@5.2.0': + resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} + + '@aws-sdk/client-s3@3.817.0': + resolution: {integrity: sha512-nZyjhlLMEXDs0ofWbpikI8tKoeKuuSgYcIb6eEZJk90Nt5HkkXn6nkWOs/kp2FdhpoGJyTILOVsDgdm7eutnLA==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/client-sso@3.817.0': + resolution: {integrity: sha512-fCh5rUHmWmWDvw70NNoWpE5+BRdtNi45kDnIoeoszqVg7UKF79SlG+qYooUT52HKCgDNHqgbWaXxMOSqd2I/OQ==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/core@3.816.0': + resolution: {integrity: sha512-Lx50wjtyarzKpMFV6V+gjbSZDgsA/71iyifbClGUSiNPoIQ4OCV0KVOmAAj7mQRVvGJqUMWKVM+WzK79CjbjWA==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-env@3.816.0': + resolution: {integrity: sha512-wUJZwRLe+SxPxRV9AENYBLrJZRrNIo+fva7ZzejsC83iz7hdfq6Rv6B/aHEdPwG/nQC4+q7UUvcRPlomyrpsBA==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-http@3.816.0': + resolution: {integrity: sha512-gcWGzMQ7yRIF+ljTkR8Vzp7727UY6cmeaPrFQrvcFB8PhOqWpf7g0JsgOf5BSaP8CkkSQcTQHc0C5ZYAzUFwPg==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-ini@3.817.0': + resolution: {integrity: sha512-kyEwbQyuXE+phWVzloMdkFv6qM6NOon+asMXY5W0fhDKwBz9zQLObDRWBrvQX9lmqq8BbDL1sCfZjOh82Y+RFw==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-node@3.817.0': + resolution: {integrity: sha512-b5mz7av0Lhavs1Bz3Zb+jrs0Pki93+8XNctnVO0drBW98x1fM4AR38cWvGbM/w9F9Q0/WEH3TinkmrMPrP4T/w==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-process@3.816.0': + resolution: {integrity: sha512-9Tm+AxMoV2Izvl5b9tyMQRbBwaex8JP06HN7ZeCXgC5sAsSN+o8dsThnEhf8jKN+uBpT6CLWKN1TXuUMrAmW1A==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-sso@3.817.0': + resolution: {integrity: sha512-gFUAW3VmGvdnueK1bh6TOcRX+j99Xm0men1+gz3cA4RE+rZGNy1Qjj8YHlv0hPwI9OnTPZquvPzA5fkviGREWg==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-web-identity@3.817.0': + resolution: {integrity: sha512-A2kgkS9g6NY0OMT2f2EdXHpL17Ym81NhbGnQ8bRXPqESIi7TFypFD2U6osB2VnsFv+MhwM+Ke4PKXSmLun22/A==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-bucket-endpoint@3.808.0': + resolution: {integrity: sha512-wEPlNcs8dir9lXbuviEGtSzYSxG/NRKQrJk5ybOc7OpPGHovsN+QhDOdY3lcjOFdwMTiMIG9foUkPz3zBpLB1A==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-expect-continue@3.804.0': + resolution: {integrity: sha512-YW1hySBolALMII6C8y7Z0CRG2UX1dGJjLEBNFeefhO/xP7ZuE1dvnmfJGaEuBMnvc3wkRS63VZ3aqX6sevM1CA==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-flexible-checksums@3.816.0': + resolution: {integrity: sha512-kftcwDxB/VoCBsUiRgkm5CIuKbTfCN1WLPbis9LRwX3kQhKgGVxG2gG78SHk4TBB0qviWVAd/t+i/KaUgwiAcA==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-host-header@3.804.0': + resolution: {integrity: sha512-bum1hLVBrn2lJCi423Z2fMUYtsbkGI2s4N+2RI2WSjvbaVyMSv/WcejIrjkqiiMR+2Y7m5exgoKeg4/TODLDPQ==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-location-constraint@3.804.0': + resolution: {integrity: sha512-AMtKnllIWKgoo7hiJfphLYotEwTERfjVMO2+cKAncz9w1g+bnYhHxiVhJJoR94y047c06X4PU5MsTxvdQ73Znw==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-logger@3.804.0': + resolution: {integrity: sha512-w/qLwL3iq0KOPQNat0Kb7sKndl9BtceigINwBU7SpkYWX9L/Lem6f8NPEKrC9Tl4wDBht3Yztub4oRTy/horJA==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-recursion-detection@3.804.0': + resolution: {integrity: sha512-zqHOrvLRdsUdN/ehYfZ9Tf8svhbiLLz5VaWUz22YndFv6m9qaAcijkpAOlKexsv3nLBMJdSdJ6GUTAeIy3BZzw==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-sdk-s3@3.816.0': + resolution: {integrity: sha512-jJ+EAXM7gnOwiCM6rrl4AUNY5urmtIsX7roTkxtb4DevJxcS+wFYRRg3/j33fQbuxQZrvk21HqxyZYx5UH70PA==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-ssec@3.804.0': + resolution: {integrity: sha512-Tk8jK0gOIUBvEPTz/wwSlP1V70zVQ3QYqsLPAjQRMO6zfOK9ax31dln3MgKvFDJxBydS2tS3wsn53v+brxDxTA==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-user-agent@3.816.0': + resolution: {integrity: sha512-bHRSlWZ0xDsFR8E2FwDb//0Ff6wMkVx4O+UKsfyNlAbtqCiiHRt5ANNfKPafr95cN2CCxLxiPvFTFVblQM5TsQ==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/nested-clients@3.817.0': + resolution: {integrity: sha512-vQ2E06A48STJFssueJQgxYD8lh1iGJoLJnHdshRDWOQb8gy1wVQR+a7MkPGhGR6lGoS0SCnF/Qp6CZhnwLsqsQ==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/region-config-resolver@3.808.0': + resolution: {integrity: sha512-9x2QWfphkARZY5OGkl9dJxZlSlYM2l5inFeo2bKntGuwg4A4YUe5h7d5yJ6sZbam9h43eBrkOdumx03DAkQF9A==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/signature-v4-multi-region@3.816.0': + resolution: {integrity: sha512-idcr9NW86sSIXASSej3423Selu6fxlhhJJtMgpAqoCH/HJh1eQrONJwNKuI9huiruPE8+02pwxuePvLW46X2mw==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/token-providers@3.817.0': + resolution: {integrity: sha512-CYN4/UO0VaqyHf46ogZzNrVX7jI3/CfiuktwKlwtpKA6hjf2+ivfgHSKzPpgPBcSEfiibA/26EeLuMnB6cpSrQ==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/types@3.804.0': + resolution: {integrity: sha512-A9qnsy9zQ8G89vrPPlNG9d1d8QcKRGqJKqwyGgS0dclJpwy6d1EWgQLIolKPl6vcFpLoe6avLOLxr+h8ur5wpg==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/util-arn-parser@3.804.0': + resolution: {integrity: sha512-wmBJqn1DRXnZu3b4EkE6CWnoWMo1ZMvlfkqU5zPz67xx1GMaXlDCchFvKAXMjk4jn/L1O3tKnoFDNsoLV1kgNQ==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/util-endpoints@3.808.0': + resolution: {integrity: sha512-N6Lic98uc4ADB7fLWlzx+1uVnq04VgVjngZvwHoujcRg9YDhIg9dUDiTzD5VZv13g1BrPYmvYP1HhsildpGV6w==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/util-locate-window@3.804.0': + resolution: {integrity: sha512-zVoRfpmBVPodYlnMjgVjfGoEZagyRF5IPn3Uo6ZvOZp24chnW/FRstH7ESDHDDRga4z3V+ElUQHKpFDXWyBW5A==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/util-user-agent-browser@3.804.0': + resolution: {integrity: sha512-KfW6T6nQHHM/vZBBdGn6fMyG/MgX5lq82TDdX4HRQRRuHKLgBWGpKXqqvBwqIaCdXwWHgDrg2VQups6GqOWW2A==} + + '@aws-sdk/util-user-agent-node@3.816.0': + resolution: {integrity: sha512-Q6dxmuj4hL7pudhrneWEQ7yVHIQRBFr0wqKLF1opwOi1cIePuoEbPyJ2jkel6PDEv1YMfvsAKaRshp6eNA8VHg==} + engines: {node: '>=18.0.0'} + peerDependencies: + aws-crt: '>=1.0.0' + peerDependenciesMeta: + aws-crt: + optional: true + + '@aws-sdk/xml-builder@3.804.0': + resolution: {integrity: sha512-JbGWp36IG9dgxtvC6+YXwt5WDZYfuamWFtVfK6fQpnmL96dx+GUPOXPKRWdw67WLKf2comHY28iX2d3z35I53Q==} + engines: {node: '>=18.0.0'} + '@babel/code-frame@7.27.1': resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} @@ -1457,6 +1663,10 @@ packages: '@radix-ui/rect@1.1.1': resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + '@redis/client@1.6.1': + resolution: {integrity: sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==} + engines: {node: '>=14'} + '@rollup/rollup-android-arm-eabi@4.41.0': resolution: {integrity: sha512-KxN+zCjOYHGwCl4UCtSfZ6jrq/qi88JDUtiEFk8LELEHq2Egfc/FgW+jItZiOLRuQfb/3xJSgFuNPC9jzggX+A==} cpu: [arm] @@ -1557,6 +1767,223 @@ packages: cpu: [x64] os: [win32] + '@shopify/semaphore@3.1.0': + resolution: {integrity: sha512-LxonkiWEu12FbZhuOMhsdocpxCqm7By8C/2U9QgNuEoXUx2iMrlXjJv3p93RwfNC6TrdlNRo17gRer1z1309VQ==} + engines: {node: '>=18.12.0'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + + '@smithy/abort-controller@4.0.3': + resolution: {integrity: sha512-AqXFf6DXnuRBXy4SoK/n1mfgHaKaq36bmkphmD1KO0nHq6xK/g9KHSW4HEsPQUBCGdIEfuJifGHwxFXPIFay9Q==} + engines: {node: '>=18.0.0'} + + '@smithy/chunked-blob-reader-native@4.0.0': + resolution: {integrity: sha512-R9wM2yPmfEMsUmlMlIgSzOyICs0x9uu7UTHoccMyt7BWw8shcGM8HqB355+BZCPBcySvbTYMs62EgEQkNxz2ig==} + engines: {node: '>=18.0.0'} + + '@smithy/chunked-blob-reader@5.0.0': + resolution: {integrity: sha512-+sKqDBQqb036hh4NPaUiEkYFkTUGYzRsn3EuFhyfQfMy6oGHEUJDurLP9Ufb5dasr/XiAmPNMr6wa9afjQB+Gw==} + engines: {node: '>=18.0.0'} + + '@smithy/config-resolver@4.1.3': + resolution: {integrity: sha512-N5e7ofiyYDmHxnPnqF8L4KtsbSDwyxFRfDK9bp1d9OyPO4ytRLd0/XxCqi5xVaaqB65v4woW8uey6jND6zxzxQ==} + engines: {node: '>=18.0.0'} + + '@smithy/core@3.4.0': + resolution: {integrity: sha512-dDYISQo7k0Ml/rXlFIjkTmTcQze/LxhtIRAEmZ6HJ/EI0inVxVEVnrUXJ7jPx6ZP0GHUhFm40iQcCgS5apXIXA==} + engines: {node: '>=18.0.0'} + + '@smithy/credential-provider-imds@4.0.5': + resolution: {integrity: sha512-saEAGwrIlkb9XxX/m5S5hOtzjoJPEK6Qw2f9pYTbIsMPOFyGSXBBTw95WbOyru8A1vIS2jVCCU1Qhz50QWG3IA==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-codec@4.0.3': + resolution: {integrity: sha512-V22KIPXZsE2mc4zEgYGANM/7UbL9jWlOACEolyGyMuTY+jjHJ2PQ0FdopOTS1CS7u6PlAkALmypkv2oQ4aftcg==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-serde-browser@4.0.3': + resolution: {integrity: sha512-oe1d/tfCGVZBMX8O6HApaM4G+fF9JNdyLP7tWXt00epuL/kLOdp/4o9VqheLFeJaXgao+9IaBgs/q/oM48hxzg==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-serde-config-resolver@4.1.1': + resolution: {integrity: sha512-XXCPGjRNwpFWHKQJMKIjGLfFKYULYckFnxGcWmBC2mBf3NsrvUKgqHax4NCqc0TfbDAimPDHOc6HOKtzsXK9Gw==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-serde-node@4.0.3': + resolution: {integrity: sha512-HOEbRmm9TrikCoFrypYu0J/gC4Lsk8gl5LtOz1G3laD2Jy44+ht2Pd2E9qjNQfhMJIzKDZ/gbuUH0s0v4kWQ0A==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-serde-universal@4.0.3': + resolution: {integrity: sha512-ShOP512CZrYI9n+h64PJ84udzoNHUQtPddyh1j175KNTKsSnMEDNscOWJWyEoLQiuhWWw51lSa+k6ea9ZGXcRg==} + engines: {node: '>=18.0.0'} + + '@smithy/fetch-http-handler@5.0.3': + resolution: {integrity: sha512-yBZwavI31roqTndNI7ONHqesfH01JmjJK6L3uUpZAhyAmr86LN5QiPzfyZGIxQmed8VEK2NRSQT3/JX5V1njfQ==} + engines: {node: '>=18.0.0'} + + '@smithy/hash-blob-browser@4.0.3': + resolution: {integrity: sha512-37wZYU/XI2cOF4hgNDNMzZNAuNtJTkZFWxcpagQrnf6PYU/6sJ6y5Ey9Bp4vzi9nteex/ImxAugfsF3XGLrqWA==} + engines: {node: '>=18.0.0'} + + '@smithy/hash-node@4.0.3': + resolution: {integrity: sha512-W5Uhy6v/aYrgtjh9y0YP332gIQcwccQ+EcfWhllL0B9rPae42JngTTUpb8W6wuxaNFzqps4xq5klHckSSOy5fw==} + engines: {node: '>=18.0.0'} + + '@smithy/hash-stream-node@4.0.3': + resolution: {integrity: sha512-CAwAvztwGYHHZGGcXtbinNxytaj5FNZChz8V+o7eNUAi5BgVqnF91Z3cJSmaE9O7FYUQVrIzGAB25Aok9T5KHQ==} + engines: {node: '>=18.0.0'} + + '@smithy/invalid-dependency@4.0.3': + resolution: {integrity: sha512-1Bo8Ur1ZGqxvwTqBmv6DZEn0rXtwJGeqiiO2/JFcCtz3nBakOqeXbJBElXJMMzd0ghe8+eB6Dkw98nMYctgizg==} + engines: {node: '>=18.0.0'} + + '@smithy/is-array-buffer@2.2.0': + resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} + engines: {node: '>=14.0.0'} + + '@smithy/is-array-buffer@4.0.0': + resolution: {integrity: sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw==} + engines: {node: '>=18.0.0'} + + '@smithy/md5-js@4.0.3': + resolution: {integrity: sha512-m95Z+1UJFPq4cv/R6TPMLYkoau7cNJYA5GLuuUJjfmF+Zrad4yaupIWeGGzIinf8pD1L+CIAxjh8eowPvyL7Dw==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-content-length@4.0.3': + resolution: {integrity: sha512-NE/Zph4BP5u16bzYq2csq9qD0T6UBLeg4AuNrwNJ7Gv9uLYaGEgelZUOdRndGdMGcUfSGvNlXGb2aA2hPCwJ6g==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-endpoint@4.1.7': + resolution: {integrity: sha512-KDzM7Iajo6K7eIWNNtukykRT4eWwlHjCEsULZUaSfi/SRSBK8BPRqG5FsVfp58lUxcvre8GT8AIPIqndA0ERKw==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-retry@4.1.8': + resolution: {integrity: sha512-e2OtQgFzzlSG0uCjcJmi02QuFSRTrpT11Eh2EcqqDFy7DYriteHZJkkf+4AsxsrGDugAtPFcWBz1aq06sSX5fQ==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-serde@4.0.6': + resolution: {integrity: sha512-YECyl7uNII+jCr/9qEmCu8xYL79cU0fqjo0qxpcVIU18dAPHam/iYwcknAu4Jiyw1uN+sAx7/SMf/Kmef/Jjsg==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-stack@4.0.3': + resolution: {integrity: sha512-baeV7t4jQfQtFxBADFmnhmqBmqR38dNU5cvEgHcMK/Kp3D3bEI0CouoX2Sr/rGuntR+Eg0IjXdxnGGTc6SbIkw==} + engines: {node: '>=18.0.0'} + + '@smithy/node-config-provider@4.1.2': + resolution: {integrity: sha512-SUvNup8iU1v7fmM8XPk+27m36udmGCfSz+VZP5Gb0aJ3Ne0X28K/25gnsrg3X1rWlhcnhzNUUysKW/Ied46ivQ==} + engines: {node: '>=18.0.0'} + + '@smithy/node-http-handler@4.0.5': + resolution: {integrity: sha512-T7QglZC1vS7SPT44/1qSIAQEx5bFKb3LfO6zw/o4Xzt1eC5HNoH1TkS4lMYA9cWFbacUhx4hRl/blLun4EOCkg==} + engines: {node: '>=18.0.0'} + + '@smithy/property-provider@4.0.3': + resolution: {integrity: sha512-Wcn17QNdawJZcZZPBuMuzyBENVi1AXl4TdE0jvzo4vWX2x5df/oMlmr/9M5XAAC6+yae4kWZlOYIsNsgDrMU9A==} + engines: {node: '>=18.0.0'} + + '@smithy/protocol-http@5.1.1': + resolution: {integrity: sha512-Vsay2mzq05DwNi9jK01yCFtfvu9HimmgC7a4HTs7lhX12Sx8aWsH0mfz6q/02yspSp+lOB+Q2HJwi4IV2GKz7A==} + engines: {node: '>=18.0.0'} + + '@smithy/querystring-builder@4.0.3': + resolution: {integrity: sha512-UUzIWMVfPmDZcOutk2/r1vURZqavvQW0OHvgsyNV0cKupChvqg+/NKPRMaMEe+i8tP96IthMFeZOZWpV+E4RAw==} + engines: {node: '>=18.0.0'} + + '@smithy/querystring-parser@4.0.3': + resolution: {integrity: sha512-K5M4ZJQpFCblOJ5Oyw7diICpFg1qhhR47m2/5Ef1PhGE19RaIZf50tjYFrxa6usqcuXyTiFPGo4d1geZdH4YcQ==} + engines: {node: '>=18.0.0'} + + '@smithy/service-error-classification@4.0.4': + resolution: {integrity: sha512-W5ScbQ1bTzgH91kNEE2CvOzM4gXlDOqdow4m8vMFSIXCel2scbHwjflpVNnC60Y3F1m5i7w2gQg9lSnR+JsJAA==} + engines: {node: '>=18.0.0'} + + '@smithy/shared-ini-file-loader@4.0.3': + resolution: {integrity: sha512-vHwlrqhZGIoLwaH8vvIjpHnloShqdJ7SUPNM2EQtEox+yEDFTVQ7E+DLZ+6OhnYEgFUwPByJyz6UZaOu2tny6A==} + engines: {node: '>=18.0.0'} + + '@smithy/signature-v4@5.1.1': + resolution: {integrity: sha512-zy8Repr5zvT0ja+Tf5wjV/Ba6vRrhdiDcp/ww6cvqYbSEudIkziDe3uppNRlFoCViyJXdPnLcwyZdDLA4CHzSg==} + engines: {node: '>=18.0.0'} + + '@smithy/smithy-client@4.3.0': + resolution: {integrity: sha512-DNsRA38pN6tYHUjebmwD9e4KcgqTLldYQb2gC6K+oxXYdCTxPn6wV9+FvOa6wrU2FQEnGJoi+3GULzOTKck/tg==} + engines: {node: '>=18.0.0'} + + '@smithy/types@4.3.0': + resolution: {integrity: sha512-+1iaIQHthDh9yaLhRzaoQxRk+l9xlk+JjMFxGRhNLz+m9vKOkjNeU8QuB4w3xvzHyVR/BVlp/4AXDHjoRIkfgQ==} + engines: {node: '>=18.0.0'} + + '@smithy/url-parser@4.0.3': + resolution: {integrity: sha512-n5/DnosDu/tweOqUUNtUbu7eRIR4J/Wz9nL7V5kFYQQVb8VYdj7a4G5NJHCw6o21ul7CvZoJkOpdTnsQDLT0tQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-base64@4.0.0': + resolution: {integrity: sha512-CvHfCmO2mchox9kjrtzoHkWHxjHZzaFojLc8quxXY7WAAMAg43nuxwv95tATVgQFNDwd4M9S1qFzj40Ul41Kmg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-body-length-browser@4.0.0': + resolution: {integrity: sha512-sNi3DL0/k64/LO3A256M+m3CDdG6V7WKWHdAiBBMUN8S3hK3aMPhwnPik2A/a2ONN+9doY9UxaLfgqsIRg69QA==} + engines: {node: '>=18.0.0'} + + '@smithy/util-body-length-node@4.0.0': + resolution: {integrity: sha512-q0iDP3VsZzqJyje8xJWEJCNIu3lktUGVoSy1KB0UWym2CL1siV3artm+u1DFYTLejpsrdGyCSWBdGNjJzfDPjg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-buffer-from@2.2.0': + resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} + engines: {node: '>=14.0.0'} + + '@smithy/util-buffer-from@4.0.0': + resolution: {integrity: sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug==} + engines: {node: '>=18.0.0'} + + '@smithy/util-config-provider@4.0.0': + resolution: {integrity: sha512-L1RBVzLyfE8OXH+1hsJ8p+acNUSirQnWQ6/EgpchV88G6zGBTDPdXiiExei6Z1wR2RxYvxY/XLw6AMNCCt8H3w==} + engines: {node: '>=18.0.0'} + + '@smithy/util-defaults-mode-browser@4.0.15': + resolution: {integrity: sha512-bJJ/B8owQbHAflatSq92f9OcV8858DJBQF1Y3GRjB8psLyUjbISywszYPFw16beREHO/C3I3taW4VGH+tOuwrQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-defaults-mode-node@4.0.15': + resolution: {integrity: sha512-8CUrEW2Ni5q+NmYkj8wsgkfqoP7l4ZquptFbq92yQE66xevc4SxqP2zH6tMtN158kgBqBDsZ+qlrRwXWOjCR8A==} + engines: {node: '>=18.0.0'} + + '@smithy/util-endpoints@3.0.5': + resolution: {integrity: sha512-PjDpqLk24/vAl340tmtCA++Q01GRRNH9cwL9qh46NspAX9S+IQVcK+GOzPt0GLJ6KYGyn8uOgo2kvJhiThclJw==} + engines: {node: '>=18.0.0'} + + '@smithy/util-hex-encoding@4.0.0': + resolution: {integrity: sha512-Yk5mLhHtfIgW2W2WQZWSg5kuMZCVbvhFmC7rV4IO2QqnZdbEFPmQnCcGMAX2z/8Qj3B9hYYNjZOhWym+RwhePw==} + engines: {node: '>=18.0.0'} + + '@smithy/util-middleware@4.0.3': + resolution: {integrity: sha512-iIsC6qZXxkD7V3BzTw3b1uK8RVC1M8WvwNxK1PKrH9FnxntCd30CSunXjL/8iJBE8Z0J14r2P69njwIpRG4FBQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-retry@4.0.4': + resolution: {integrity: sha512-Aoqr9W2jDYGrI6OxljN8VmLDQIGO4VdMAUKMf9RGqLG8hn6or+K41NEy1Y5dtum9q8F7e0obYAuKl2mt/GnpZg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-stream@4.2.1': + resolution: {integrity: sha512-W3IR0x5DY6iVtjj5p902oNhD+Bz7vs5S+p6tppbPa509rV9BdeXZjGuRSCtVEad9FA0Mba+tNUtUmtnSI1nwUw==} + engines: {node: '>=18.0.0'} + + '@smithy/util-uri-escape@4.0.0': + resolution: {integrity: sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-utf8@2.3.0': + resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} + engines: {node: '>=14.0.0'} + + '@smithy/util-utf8@4.0.0': + resolution: {integrity: sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==} + engines: {node: '>=18.0.0'} + + '@smithy/util-waiter@4.0.4': + resolution: {integrity: sha512-73aeIvHjtSB6fd9I08iFaQIGTICKpLrI3EtlWAkStVENGo1ARMq9qdoD4QwkY0RUp6A409xlgbD9NCCfCF5ieg==} + engines: {node: '>=18.0.0'} + '@svgr/babel-plugin-add-jsx-attribute@8.0.0': resolution: {integrity: sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==} engines: {node: '>=14'} @@ -1908,6 +2335,9 @@ packages: '@types/cookies@0.9.0': resolution: {integrity: sha512-40Zk8qR147RABiQ7NQnBzWzDcjKzNrntB5BAmeGCb2p/MIyOE+4BVvc17wumsUqUw00bJYqoXFHYygQnEFh4/Q==} + '@types/debug@4.1.12': + resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + '@types/estree@1.0.7': resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} @@ -1950,12 +2380,24 @@ packages: '@types/koa@2.15.0': resolution: {integrity: sha512-7QFsywoE5URbuVnG3loe03QXuGajrnotr3gQkXcEBShORai23MePfFYdhz90FEtBBpkyIYQbVD+evKtloCgX3g==} + '@types/lodash.throttle@4.1.9': + resolution: {integrity: sha512-PCPVfpfueguWZQB7pJQK890F2scYKoDUL3iM522AptHWn7d5NQmeS/LTEHIcLr5PaTzl3dK2Z0xSUHHTHwaL5g==} + + '@types/lodash@4.17.17': + resolution: {integrity: sha512-RRVJ+J3J+WmyOTqnz3PiBLA501eKwXl2noseKOrNo/6+XEHjTAxO4xHvxQB6QuNm+s4WRbn6rSiap8+EA+ykFQ==} + '@types/mime@1.3.5': resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} '@types/minimatch@5.1.2': resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==} + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + + '@types/multistream@4.1.3': + resolution: {integrity: sha512-t57vmDEJOZuC0M3IrZYfCd9wolTcr3ZTCGk1iwHNosvgBX+7/SMvCGcR8wP9lidpelBZQ12crSuINOxkk0azPA==} + '@types/node@20.17.50': resolution: {integrity: sha512-Mxiq0ULv/zo1OzOhwPqOA13I81CV/W3nvd3ChtQZRT5Cwz3cr0FKo/wMSsbTqL3EXpaBAEQhva2B8ByRkOIh9A==} @@ -2204,6 +2646,9 @@ packages: boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + bowser@2.11.0: + resolution: {integrity: sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==} + brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} @@ -2844,6 +3289,10 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-xml-parser@4.4.1: + resolution: {integrity: sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==} + hasBin: true + fast-xml-parser@4.5.3: resolution: {integrity: sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==} hasBin: true @@ -2971,6 +3420,10 @@ packages: functions-have-names@1.2.3: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + generic-pool@3.9.0: + resolution: {integrity: sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==} + engines: {node: '>= 4'} + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -3561,6 +4014,9 @@ packages: lodash.sortby@4.7.0: resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} + lodash.throttle@4.1.1: + resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==} + lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} @@ -3696,6 +4152,9 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + multistream@4.1.0: + resolution: {integrity: sha512-J1XDiAmmNpRCBfIWJv+n0ymC4ABcf/Pl+5YvC5B/D2f/2+8PtHvCNxMPKiQcZyi922Hq69J2YOpb1pTywfifyw==} + mute-stream@0.0.8: resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} @@ -4273,6 +4732,24 @@ packages: resolution: {integrity: sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==} engines: {node: '>= 0.4'} + should-equal@2.0.0: + resolution: {integrity: sha512-ZP36TMrK9euEuWQYBig9W55WPC7uo37qzAEmbjHz4gfyuXrEUgF8cUvQVO+w+d3OMfPvSRQJ22lSm8MQJ43LTA==} + + should-format@3.0.3: + resolution: {integrity: sha512-hZ58adtulAk0gKtua7QxevgUaXTTXxIi8t41L3zo9AHvjXO1/7sdLECuHeIN2SRtYXpNkmhoUP2pdeWgricQ+Q==} + + should-type-adaptors@1.1.0: + resolution: {integrity: sha512-JA4hdoLnN+kebEp2Vs8eBe9g7uy0zbRo+RMcU0EsNy+R+k049Ki+N5tT5Jagst2g7EAja+euFuoXFCa8vIklfA==} + + should-type@1.4.0: + resolution: {integrity: sha512-MdAsTu3n25yDbIe1NeN69G4n6mUnJGtSJHygX3+oN0ZbO3DTiATnf7XnYJdGT42JCXurTb1JI0qOBR65shvhPQ==} + + should-util@1.0.1: + resolution: {integrity: sha512-oXF8tfxx5cDk8r2kYqlkUJzZpDBqVY/II2WhvU0n9Y3XYvAYRmeaf1PvvIvTgPnv4KJ+ES5M0PyDq5Jp+Ygy2g==} + + should@13.2.3: + resolution: {integrity: sha512-ggLesLtu2xp+ZxI+ysJTmNjh2U0TsC+rQ/pfED9bUZZ4DKefP27D+7YJVVTvKsmjLpIi9jAa7itwDGkDDmt1GQ==} + side-channel-list@1.0.0: resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} engines: {node: '>= 0.4'} @@ -4778,6 +5255,10 @@ packages: util@0.12.5: resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} + uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + hasBin: true + v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} @@ -4870,6 +5351,9 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + yallist@5.0.0: resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} engines: {node: '>=18'} @@ -4916,6 +5400,465 @@ snapshots: openapi3-ts: 4.2.2 zod: 3.25.23 + '@aws-crypto/crc32@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.804.0 + tslib: 2.8.1 + + '@aws-crypto/crc32c@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.804.0 + tslib: 2.8.1 + + '@aws-crypto/sha1-browser@5.2.0': + dependencies: + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.804.0 + '@aws-sdk/util-locate-window': 3.804.0 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-crypto/sha256-browser@5.2.0': + dependencies: + '@aws-crypto/sha256-js': 5.2.0 + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.804.0 + '@aws-sdk/util-locate-window': 3.804.0 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-crypto/sha256-js@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.804.0 + tslib: 2.8.1 + + '@aws-crypto/supports-web-crypto@5.2.0': + dependencies: + tslib: 2.8.1 + + '@aws-crypto/util@5.2.0': + dependencies: + '@aws-sdk/types': 3.804.0 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-sdk/client-s3@3.817.0': + dependencies: + '@aws-crypto/sha1-browser': 5.2.0 + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.816.0 + '@aws-sdk/credential-provider-node': 3.817.0 + '@aws-sdk/middleware-bucket-endpoint': 3.808.0 + '@aws-sdk/middleware-expect-continue': 3.804.0 + '@aws-sdk/middleware-flexible-checksums': 3.816.0 + '@aws-sdk/middleware-host-header': 3.804.0 + '@aws-sdk/middleware-location-constraint': 3.804.0 + '@aws-sdk/middleware-logger': 3.804.0 + '@aws-sdk/middleware-recursion-detection': 3.804.0 + '@aws-sdk/middleware-sdk-s3': 3.816.0 + '@aws-sdk/middleware-ssec': 3.804.0 + '@aws-sdk/middleware-user-agent': 3.816.0 + '@aws-sdk/region-config-resolver': 3.808.0 + '@aws-sdk/signature-v4-multi-region': 3.816.0 + '@aws-sdk/types': 3.804.0 + '@aws-sdk/util-endpoints': 3.808.0 + '@aws-sdk/util-user-agent-browser': 3.804.0 + '@aws-sdk/util-user-agent-node': 3.816.0 + '@aws-sdk/xml-builder': 3.804.0 + '@smithy/config-resolver': 4.1.3 + '@smithy/core': 3.4.0 + '@smithy/eventstream-serde-browser': 4.0.3 + '@smithy/eventstream-serde-config-resolver': 4.1.1 + '@smithy/eventstream-serde-node': 4.0.3 + '@smithy/fetch-http-handler': 5.0.3 + '@smithy/hash-blob-browser': 4.0.3 + '@smithy/hash-node': 4.0.3 + '@smithy/hash-stream-node': 4.0.3 + '@smithy/invalid-dependency': 4.0.3 + '@smithy/md5-js': 4.0.3 + '@smithy/middleware-content-length': 4.0.3 + '@smithy/middleware-endpoint': 4.1.7 + '@smithy/middleware-retry': 4.1.8 + '@smithy/middleware-serde': 4.0.6 + '@smithy/middleware-stack': 4.0.3 + '@smithy/node-config-provider': 4.1.2 + '@smithy/node-http-handler': 4.0.5 + '@smithy/protocol-http': 5.1.1 + '@smithy/smithy-client': 4.3.0 + '@smithy/types': 4.3.0 + '@smithy/url-parser': 4.0.3 + '@smithy/util-base64': 4.0.0 + '@smithy/util-body-length-browser': 4.0.0 + '@smithy/util-body-length-node': 4.0.0 + '@smithy/util-defaults-mode-browser': 4.0.15 + '@smithy/util-defaults-mode-node': 4.0.15 + '@smithy/util-endpoints': 3.0.5 + '@smithy/util-middleware': 4.0.3 + '@smithy/util-retry': 4.0.4 + '@smithy/util-stream': 4.2.1 + '@smithy/util-utf8': 4.0.0 + '@smithy/util-waiter': 4.0.4 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/client-sso@3.817.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.816.0 + '@aws-sdk/middleware-host-header': 3.804.0 + '@aws-sdk/middleware-logger': 3.804.0 + '@aws-sdk/middleware-recursion-detection': 3.804.0 + '@aws-sdk/middleware-user-agent': 3.816.0 + '@aws-sdk/region-config-resolver': 3.808.0 + '@aws-sdk/types': 3.804.0 + '@aws-sdk/util-endpoints': 3.808.0 + '@aws-sdk/util-user-agent-browser': 3.804.0 + '@aws-sdk/util-user-agent-node': 3.816.0 + '@smithy/config-resolver': 4.1.3 + '@smithy/core': 3.4.0 + '@smithy/fetch-http-handler': 5.0.3 + '@smithy/hash-node': 4.0.3 + '@smithy/invalid-dependency': 4.0.3 + '@smithy/middleware-content-length': 4.0.3 + '@smithy/middleware-endpoint': 4.1.7 + '@smithy/middleware-retry': 4.1.8 + '@smithy/middleware-serde': 4.0.6 + '@smithy/middleware-stack': 4.0.3 + '@smithy/node-config-provider': 4.1.2 + '@smithy/node-http-handler': 4.0.5 + '@smithy/protocol-http': 5.1.1 + '@smithy/smithy-client': 4.3.0 + '@smithy/types': 4.3.0 + '@smithy/url-parser': 4.0.3 + '@smithy/util-base64': 4.0.0 + '@smithy/util-body-length-browser': 4.0.0 + '@smithy/util-body-length-node': 4.0.0 + '@smithy/util-defaults-mode-browser': 4.0.15 + '@smithy/util-defaults-mode-node': 4.0.15 + '@smithy/util-endpoints': 3.0.5 + '@smithy/util-middleware': 4.0.3 + '@smithy/util-retry': 4.0.4 + '@smithy/util-utf8': 4.0.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/core@3.816.0': + dependencies: + '@aws-sdk/types': 3.804.0 + '@smithy/core': 3.4.0 + '@smithy/node-config-provider': 4.1.2 + '@smithy/property-provider': 4.0.3 + '@smithy/protocol-http': 5.1.1 + '@smithy/signature-v4': 5.1.1 + '@smithy/smithy-client': 4.3.0 + '@smithy/types': 4.3.0 + '@smithy/util-middleware': 4.0.3 + fast-xml-parser: 4.4.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-env@3.816.0': + dependencies: + '@aws-sdk/core': 3.816.0 + '@aws-sdk/types': 3.804.0 + '@smithy/property-provider': 4.0.3 + '@smithy/types': 4.3.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-http@3.816.0': + dependencies: + '@aws-sdk/core': 3.816.0 + '@aws-sdk/types': 3.804.0 + '@smithy/fetch-http-handler': 5.0.3 + '@smithy/node-http-handler': 4.0.5 + '@smithy/property-provider': 4.0.3 + '@smithy/protocol-http': 5.1.1 + '@smithy/smithy-client': 4.3.0 + '@smithy/types': 4.3.0 + '@smithy/util-stream': 4.2.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-ini@3.817.0': + dependencies: + '@aws-sdk/core': 3.816.0 + '@aws-sdk/credential-provider-env': 3.816.0 + '@aws-sdk/credential-provider-http': 3.816.0 + '@aws-sdk/credential-provider-process': 3.816.0 + '@aws-sdk/credential-provider-sso': 3.817.0 + '@aws-sdk/credential-provider-web-identity': 3.817.0 + '@aws-sdk/nested-clients': 3.817.0 + '@aws-sdk/types': 3.804.0 + '@smithy/credential-provider-imds': 4.0.5 + '@smithy/property-provider': 4.0.3 + '@smithy/shared-ini-file-loader': 4.0.3 + '@smithy/types': 4.3.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-node@3.817.0': + dependencies: + '@aws-sdk/credential-provider-env': 3.816.0 + '@aws-sdk/credential-provider-http': 3.816.0 + '@aws-sdk/credential-provider-ini': 3.817.0 + '@aws-sdk/credential-provider-process': 3.816.0 + '@aws-sdk/credential-provider-sso': 3.817.0 + '@aws-sdk/credential-provider-web-identity': 3.817.0 + '@aws-sdk/types': 3.804.0 + '@smithy/credential-provider-imds': 4.0.5 + '@smithy/property-provider': 4.0.3 + '@smithy/shared-ini-file-loader': 4.0.3 + '@smithy/types': 4.3.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-process@3.816.0': + dependencies: + '@aws-sdk/core': 3.816.0 + '@aws-sdk/types': 3.804.0 + '@smithy/property-provider': 4.0.3 + '@smithy/shared-ini-file-loader': 4.0.3 + '@smithy/types': 4.3.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-sso@3.817.0': + dependencies: + '@aws-sdk/client-sso': 3.817.0 + '@aws-sdk/core': 3.816.0 + '@aws-sdk/token-providers': 3.817.0 + '@aws-sdk/types': 3.804.0 + '@smithy/property-provider': 4.0.3 + '@smithy/shared-ini-file-loader': 4.0.3 + '@smithy/types': 4.3.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-web-identity@3.817.0': + dependencies: + '@aws-sdk/core': 3.816.0 + '@aws-sdk/nested-clients': 3.817.0 + '@aws-sdk/types': 3.804.0 + '@smithy/property-provider': 4.0.3 + '@smithy/types': 4.3.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/middleware-bucket-endpoint@3.808.0': + dependencies: + '@aws-sdk/types': 3.804.0 + '@aws-sdk/util-arn-parser': 3.804.0 + '@smithy/node-config-provider': 4.1.2 + '@smithy/protocol-http': 5.1.1 + '@smithy/types': 4.3.0 + '@smithy/util-config-provider': 4.0.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-expect-continue@3.804.0': + dependencies: + '@aws-sdk/types': 3.804.0 + '@smithy/protocol-http': 5.1.1 + '@smithy/types': 4.3.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-flexible-checksums@3.816.0': + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@aws-crypto/crc32c': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/core': 3.816.0 + '@aws-sdk/types': 3.804.0 + '@smithy/is-array-buffer': 4.0.0 + '@smithy/node-config-provider': 4.1.2 + '@smithy/protocol-http': 5.1.1 + '@smithy/types': 4.3.0 + '@smithy/util-middleware': 4.0.3 + '@smithy/util-stream': 4.2.1 + '@smithy/util-utf8': 4.0.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-host-header@3.804.0': + dependencies: + '@aws-sdk/types': 3.804.0 + '@smithy/protocol-http': 5.1.1 + '@smithy/types': 4.3.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-location-constraint@3.804.0': + dependencies: + '@aws-sdk/types': 3.804.0 + '@smithy/types': 4.3.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-logger@3.804.0': + dependencies: + '@aws-sdk/types': 3.804.0 + '@smithy/types': 4.3.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-recursion-detection@3.804.0': + dependencies: + '@aws-sdk/types': 3.804.0 + '@smithy/protocol-http': 5.1.1 + '@smithy/types': 4.3.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-sdk-s3@3.816.0': + dependencies: + '@aws-sdk/core': 3.816.0 + '@aws-sdk/types': 3.804.0 + '@aws-sdk/util-arn-parser': 3.804.0 + '@smithy/core': 3.4.0 + '@smithy/node-config-provider': 4.1.2 + '@smithy/protocol-http': 5.1.1 + '@smithy/signature-v4': 5.1.1 + '@smithy/smithy-client': 4.3.0 + '@smithy/types': 4.3.0 + '@smithy/util-config-provider': 4.0.0 + '@smithy/util-middleware': 4.0.3 + '@smithy/util-stream': 4.2.1 + '@smithy/util-utf8': 4.0.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-ssec@3.804.0': + dependencies: + '@aws-sdk/types': 3.804.0 + '@smithy/types': 4.3.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-user-agent@3.816.0': + dependencies: + '@aws-sdk/core': 3.816.0 + '@aws-sdk/types': 3.804.0 + '@aws-sdk/util-endpoints': 3.808.0 + '@smithy/core': 3.4.0 + '@smithy/protocol-http': 5.1.1 + '@smithy/types': 4.3.0 + tslib: 2.8.1 + + '@aws-sdk/nested-clients@3.817.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.816.0 + '@aws-sdk/middleware-host-header': 3.804.0 + '@aws-sdk/middleware-logger': 3.804.0 + '@aws-sdk/middleware-recursion-detection': 3.804.0 + '@aws-sdk/middleware-user-agent': 3.816.0 + '@aws-sdk/region-config-resolver': 3.808.0 + '@aws-sdk/types': 3.804.0 + '@aws-sdk/util-endpoints': 3.808.0 + '@aws-sdk/util-user-agent-browser': 3.804.0 + '@aws-sdk/util-user-agent-node': 3.816.0 + '@smithy/config-resolver': 4.1.3 + '@smithy/core': 3.4.0 + '@smithy/fetch-http-handler': 5.0.3 + '@smithy/hash-node': 4.0.3 + '@smithy/invalid-dependency': 4.0.3 + '@smithy/middleware-content-length': 4.0.3 + '@smithy/middleware-endpoint': 4.1.7 + '@smithy/middleware-retry': 4.1.8 + '@smithy/middleware-serde': 4.0.6 + '@smithy/middleware-stack': 4.0.3 + '@smithy/node-config-provider': 4.1.2 + '@smithy/node-http-handler': 4.0.5 + '@smithy/protocol-http': 5.1.1 + '@smithy/smithy-client': 4.3.0 + '@smithy/types': 4.3.0 + '@smithy/url-parser': 4.0.3 + '@smithy/util-base64': 4.0.0 + '@smithy/util-body-length-browser': 4.0.0 + '@smithy/util-body-length-node': 4.0.0 + '@smithy/util-defaults-mode-browser': 4.0.15 + '@smithy/util-defaults-mode-node': 4.0.15 + '@smithy/util-endpoints': 3.0.5 + '@smithy/util-middleware': 4.0.3 + '@smithy/util-retry': 4.0.4 + '@smithy/util-utf8': 4.0.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/region-config-resolver@3.808.0': + dependencies: + '@aws-sdk/types': 3.804.0 + '@smithy/node-config-provider': 4.1.2 + '@smithy/types': 4.3.0 + '@smithy/util-config-provider': 4.0.0 + '@smithy/util-middleware': 4.0.3 + tslib: 2.8.1 + + '@aws-sdk/signature-v4-multi-region@3.816.0': + dependencies: + '@aws-sdk/middleware-sdk-s3': 3.816.0 + '@aws-sdk/types': 3.804.0 + '@smithy/protocol-http': 5.1.1 + '@smithy/signature-v4': 5.1.1 + '@smithy/types': 4.3.0 + tslib: 2.8.1 + + '@aws-sdk/token-providers@3.817.0': + dependencies: + '@aws-sdk/core': 3.816.0 + '@aws-sdk/nested-clients': 3.817.0 + '@aws-sdk/types': 3.804.0 + '@smithy/property-provider': 4.0.3 + '@smithy/shared-ini-file-loader': 4.0.3 + '@smithy/types': 4.3.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/types@3.804.0': + dependencies: + '@smithy/types': 4.3.0 + tslib: 2.8.1 + + '@aws-sdk/util-arn-parser@3.804.0': + dependencies: + tslib: 2.8.1 + + '@aws-sdk/util-endpoints@3.808.0': + dependencies: + '@aws-sdk/types': 3.804.0 + '@smithy/types': 4.3.0 + '@smithy/util-endpoints': 3.0.5 + tslib: 2.8.1 + + '@aws-sdk/util-locate-window@3.804.0': + dependencies: + tslib: 2.8.1 + + '@aws-sdk/util-user-agent-browser@3.804.0': + dependencies: + '@aws-sdk/types': 3.804.0 + '@smithy/types': 4.3.0 + bowser: 2.11.0 + tslib: 2.8.1 + + '@aws-sdk/util-user-agent-node@3.816.0': + dependencies: + '@aws-sdk/middleware-user-agent': 3.816.0 + '@aws-sdk/types': 3.804.0 + '@smithy/node-config-provider': 4.1.2 + '@smithy/types': 4.3.0 + tslib: 2.8.1 + + '@aws-sdk/xml-builder@3.804.0': + dependencies: + '@smithy/types': 4.3.0 + tslib: 2.8.1 + '@babel/code-frame@7.27.1': dependencies: '@babel/helper-validator-identifier': 7.27.1 @@ -5757,6 +6700,12 @@ snapshots: '@radix-ui/rect@1.1.1': {} + '@redis/client@1.6.1': + dependencies: + cluster-key-slot: 1.1.2 + generic-pool: 3.9.0 + yallist: 4.0.0 + '@rollup/rollup-android-arm-eabi@4.41.0': optional: true @@ -5817,6 +6766,340 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.41.0': optional: true + '@shopify/semaphore@3.1.0': {} + + '@smithy/abort-controller@4.0.3': + dependencies: + '@smithy/types': 4.3.0 + tslib: 2.8.1 + + '@smithy/chunked-blob-reader-native@4.0.0': + dependencies: + '@smithy/util-base64': 4.0.0 + tslib: 2.8.1 + + '@smithy/chunked-blob-reader@5.0.0': + dependencies: + tslib: 2.8.1 + + '@smithy/config-resolver@4.1.3': + dependencies: + '@smithy/node-config-provider': 4.1.2 + '@smithy/types': 4.3.0 + '@smithy/util-config-provider': 4.0.0 + '@smithy/util-middleware': 4.0.3 + tslib: 2.8.1 + + '@smithy/core@3.4.0': + dependencies: + '@smithy/middleware-serde': 4.0.6 + '@smithy/protocol-http': 5.1.1 + '@smithy/types': 4.3.0 + '@smithy/util-body-length-browser': 4.0.0 + '@smithy/util-middleware': 4.0.3 + '@smithy/util-stream': 4.2.1 + '@smithy/util-utf8': 4.0.0 + tslib: 2.8.1 + + '@smithy/credential-provider-imds@4.0.5': + dependencies: + '@smithy/node-config-provider': 4.1.2 + '@smithy/property-provider': 4.0.3 + '@smithy/types': 4.3.0 + '@smithy/url-parser': 4.0.3 + tslib: 2.8.1 + + '@smithy/eventstream-codec@4.0.3': + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@smithy/types': 4.3.0 + '@smithy/util-hex-encoding': 4.0.0 + tslib: 2.8.1 + + '@smithy/eventstream-serde-browser@4.0.3': + dependencies: + '@smithy/eventstream-serde-universal': 4.0.3 + '@smithy/types': 4.3.0 + tslib: 2.8.1 + + '@smithy/eventstream-serde-config-resolver@4.1.1': + dependencies: + '@smithy/types': 4.3.0 + tslib: 2.8.1 + + '@smithy/eventstream-serde-node@4.0.3': + dependencies: + '@smithy/eventstream-serde-universal': 4.0.3 + '@smithy/types': 4.3.0 + tslib: 2.8.1 + + '@smithy/eventstream-serde-universal@4.0.3': + dependencies: + '@smithy/eventstream-codec': 4.0.3 + '@smithy/types': 4.3.0 + tslib: 2.8.1 + + '@smithy/fetch-http-handler@5.0.3': + dependencies: + '@smithy/protocol-http': 5.1.1 + '@smithy/querystring-builder': 4.0.3 + '@smithy/types': 4.3.0 + '@smithy/util-base64': 4.0.0 + tslib: 2.8.1 + + '@smithy/hash-blob-browser@4.0.3': + dependencies: + '@smithy/chunked-blob-reader': 5.0.0 + '@smithy/chunked-blob-reader-native': 4.0.0 + '@smithy/types': 4.3.0 + tslib: 2.8.1 + + '@smithy/hash-node@4.0.3': + dependencies: + '@smithy/types': 4.3.0 + '@smithy/util-buffer-from': 4.0.0 + '@smithy/util-utf8': 4.0.0 + tslib: 2.8.1 + + '@smithy/hash-stream-node@4.0.3': + dependencies: + '@smithy/types': 4.3.0 + '@smithy/util-utf8': 4.0.0 + tslib: 2.8.1 + + '@smithy/invalid-dependency@4.0.3': + dependencies: + '@smithy/types': 4.3.0 + tslib: 2.8.1 + + '@smithy/is-array-buffer@2.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/is-array-buffer@4.0.0': + dependencies: + tslib: 2.8.1 + + '@smithy/md5-js@4.0.3': + dependencies: + '@smithy/types': 4.3.0 + '@smithy/util-utf8': 4.0.0 + tslib: 2.8.1 + + '@smithy/middleware-content-length@4.0.3': + dependencies: + '@smithy/protocol-http': 5.1.1 + '@smithy/types': 4.3.0 + tslib: 2.8.1 + + '@smithy/middleware-endpoint@4.1.7': + dependencies: + '@smithy/core': 3.4.0 + '@smithy/middleware-serde': 4.0.6 + '@smithy/node-config-provider': 4.1.2 + '@smithy/shared-ini-file-loader': 4.0.3 + '@smithy/types': 4.3.0 + '@smithy/url-parser': 4.0.3 + '@smithy/util-middleware': 4.0.3 + tslib: 2.8.1 + + '@smithy/middleware-retry@4.1.8': + dependencies: + '@smithy/node-config-provider': 4.1.2 + '@smithy/protocol-http': 5.1.1 + '@smithy/service-error-classification': 4.0.4 + '@smithy/smithy-client': 4.3.0 + '@smithy/types': 4.3.0 + '@smithy/util-middleware': 4.0.3 + '@smithy/util-retry': 4.0.4 + tslib: 2.8.1 + uuid: 9.0.1 + + '@smithy/middleware-serde@4.0.6': + dependencies: + '@smithy/protocol-http': 5.1.1 + '@smithy/types': 4.3.0 + tslib: 2.8.1 + + '@smithy/middleware-stack@4.0.3': + dependencies: + '@smithy/types': 4.3.0 + tslib: 2.8.1 + + '@smithy/node-config-provider@4.1.2': + dependencies: + '@smithy/property-provider': 4.0.3 + '@smithy/shared-ini-file-loader': 4.0.3 + '@smithy/types': 4.3.0 + tslib: 2.8.1 + + '@smithy/node-http-handler@4.0.5': + dependencies: + '@smithy/abort-controller': 4.0.3 + '@smithy/protocol-http': 5.1.1 + '@smithy/querystring-builder': 4.0.3 + '@smithy/types': 4.3.0 + tslib: 2.8.1 + + '@smithy/property-provider@4.0.3': + dependencies: + '@smithy/types': 4.3.0 + tslib: 2.8.1 + + '@smithy/protocol-http@5.1.1': + dependencies: + '@smithy/types': 4.3.0 + tslib: 2.8.1 + + '@smithy/querystring-builder@4.0.3': + dependencies: + '@smithy/types': 4.3.0 + '@smithy/util-uri-escape': 4.0.0 + tslib: 2.8.1 + + '@smithy/querystring-parser@4.0.3': + dependencies: + '@smithy/types': 4.3.0 + tslib: 2.8.1 + + '@smithy/service-error-classification@4.0.4': + dependencies: + '@smithy/types': 4.3.0 + + '@smithy/shared-ini-file-loader@4.0.3': + dependencies: + '@smithy/types': 4.3.0 + tslib: 2.8.1 + + '@smithy/signature-v4@5.1.1': + dependencies: + '@smithy/is-array-buffer': 4.0.0 + '@smithy/protocol-http': 5.1.1 + '@smithy/types': 4.3.0 + '@smithy/util-hex-encoding': 4.0.0 + '@smithy/util-middleware': 4.0.3 + '@smithy/util-uri-escape': 4.0.0 + '@smithy/util-utf8': 4.0.0 + tslib: 2.8.1 + + '@smithy/smithy-client@4.3.0': + dependencies: + '@smithy/core': 3.4.0 + '@smithy/middleware-endpoint': 4.1.7 + '@smithy/middleware-stack': 4.0.3 + '@smithy/protocol-http': 5.1.1 + '@smithy/types': 4.3.0 + '@smithy/util-stream': 4.2.1 + tslib: 2.8.1 + + '@smithy/types@4.3.0': + dependencies: + tslib: 2.8.1 + + '@smithy/url-parser@4.0.3': + dependencies: + '@smithy/querystring-parser': 4.0.3 + '@smithy/types': 4.3.0 + tslib: 2.8.1 + + '@smithy/util-base64@4.0.0': + dependencies: + '@smithy/util-buffer-from': 4.0.0 + '@smithy/util-utf8': 4.0.0 + tslib: 2.8.1 + + '@smithy/util-body-length-browser@4.0.0': + dependencies: + tslib: 2.8.1 + + '@smithy/util-body-length-node@4.0.0': + dependencies: + tslib: 2.8.1 + + '@smithy/util-buffer-from@2.2.0': + dependencies: + '@smithy/is-array-buffer': 2.2.0 + tslib: 2.8.1 + + '@smithy/util-buffer-from@4.0.0': + dependencies: + '@smithy/is-array-buffer': 4.0.0 + tslib: 2.8.1 + + '@smithy/util-config-provider@4.0.0': + dependencies: + tslib: 2.8.1 + + '@smithy/util-defaults-mode-browser@4.0.15': + dependencies: + '@smithy/property-provider': 4.0.3 + '@smithy/smithy-client': 4.3.0 + '@smithy/types': 4.3.0 + bowser: 2.11.0 + tslib: 2.8.1 + + '@smithy/util-defaults-mode-node@4.0.15': + dependencies: + '@smithy/config-resolver': 4.1.3 + '@smithy/credential-provider-imds': 4.0.5 + '@smithy/node-config-provider': 4.1.2 + '@smithy/property-provider': 4.0.3 + '@smithy/smithy-client': 4.3.0 + '@smithy/types': 4.3.0 + tslib: 2.8.1 + + '@smithy/util-endpoints@3.0.5': + dependencies: + '@smithy/node-config-provider': 4.1.2 + '@smithy/types': 4.3.0 + tslib: 2.8.1 + + '@smithy/util-hex-encoding@4.0.0': + dependencies: + tslib: 2.8.1 + + '@smithy/util-middleware@4.0.3': + dependencies: + '@smithy/types': 4.3.0 + tslib: 2.8.1 + + '@smithy/util-retry@4.0.4': + dependencies: + '@smithy/service-error-classification': 4.0.4 + '@smithy/types': 4.3.0 + tslib: 2.8.1 + + '@smithy/util-stream@4.2.1': + dependencies: + '@smithy/fetch-http-handler': 5.0.3 + '@smithy/node-http-handler': 4.0.5 + '@smithy/types': 4.3.0 + '@smithy/util-base64': 4.0.0 + '@smithy/util-buffer-from': 4.0.0 + '@smithy/util-hex-encoding': 4.0.0 + '@smithy/util-utf8': 4.0.0 + tslib: 2.8.1 + + '@smithy/util-uri-escape@4.0.0': + dependencies: + tslib: 2.8.1 + + '@smithy/util-utf8@2.3.0': + dependencies: + '@smithy/util-buffer-from': 2.2.0 + tslib: 2.8.1 + + '@smithy/util-utf8@4.0.0': + dependencies: + '@smithy/util-buffer-from': 4.0.0 + tslib: 2.8.1 + + '@smithy/util-waiter@4.0.4': + dependencies: + '@smithy/abort-controller': 4.0.3 + '@smithy/types': 4.3.0 + tslib: 2.8.1 + '@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 @@ -6161,6 +7444,10 @@ snapshots: '@types/keygrip': 1.0.6 '@types/node': 20.17.50 + '@types/debug@4.1.12': + dependencies: + '@types/ms': 2.1.0 + '@types/estree@1.0.7': {} '@types/express-serve-static-core@5.0.6': @@ -6220,10 +7507,22 @@ snapshots: '@types/koa-compose': 3.2.8 '@types/node': 20.17.50 + '@types/lodash.throttle@4.1.9': + dependencies: + '@types/lodash': 4.17.17 + + '@types/lodash@4.17.17': {} + '@types/mime@1.3.5': {} '@types/minimatch@5.1.2': {} + '@types/ms@2.1.0': {} + + '@types/multistream@4.1.3': + dependencies: + '@types/node': 20.17.50 + '@types/node@20.17.50': dependencies: undici-types: 6.19.8 @@ -6536,6 +7835,8 @@ snapshots: boolbase@1.0.0: {} + bowser@2.11.0: {} + brace-expansion@1.1.11: dependencies: balanced-match: 1.0.2 @@ -7380,6 +8681,10 @@ snapshots: fast-levenshtein@2.0.6: {} + fast-xml-parser@4.4.1: + dependencies: + strnum: 1.1.2 + fast-xml-parser@4.5.3: dependencies: strnum: 1.1.2 @@ -7501,6 +8806,8 @@ snapshots: functions-have-names@1.2.3: {} + generic-pool@3.9.0: {} + gensync@1.0.0-beta.2: {} get-caller-file@2.0.5: {} @@ -8129,6 +9436,8 @@ snapshots: lodash.sortby@4.7.0: {} + lodash.throttle@4.1.1: {} + lodash@4.17.21: {} log-symbols@3.0.0: @@ -8257,6 +9566,11 @@ snapshots: ms@2.1.3: {} + multistream@4.1.0: + dependencies: + once: 1.4.0 + readable-stream: 3.6.2 + mute-stream@0.0.8: {} mz@2.7.0: @@ -8916,6 +10230,32 @@ snapshots: shell-quote@1.8.2: {} + should-equal@2.0.0: + dependencies: + should-type: 1.4.0 + + should-format@3.0.3: + dependencies: + should-type: 1.4.0 + should-type-adaptors: 1.1.0 + + should-type-adaptors@1.1.0: + dependencies: + should-type: 1.4.0 + should-util: 1.0.1 + + should-type@1.4.0: {} + + should-util@1.0.1: {} + + should@13.2.3: + dependencies: + should-equal: 2.0.0 + should-format: 3.0.3 + should-type: 1.4.0 + should-type-adaptors: 1.1.0 + should-util: 1.0.1 + side-channel-list@1.0.0: dependencies: es-errors: 1.3.0 @@ -9460,6 +10800,8 @@ snapshots: is-typed-array: 1.1.15 which-typed-array: 1.1.19 + uuid@9.0.1: {} + v8-compile-cache-lib@3.0.1: {} validate-npm-package-name@5.0.1: {} @@ -9570,6 +10912,8 @@ snapshots: yallist@3.1.1: {} + yallist@4.0.0: {} + yallist@5.0.0: {} yaml@2.8.0: {} From 4dce2dc613140f8aee83dc34d68ec621d86488b9 Mon Sep 17 00:00:00 2001 From: ditiqi Date: Wed, 28 May 2025 08:23:15 +0800 Subject: [PATCH 9/9] add --- apps/backend/.env.example | 4 +- apps/backend/package.json | 4 + apps/backend/src/index.ts | 13 + apps/backend/src/oidc/config.ts | 190 +++++++------- apps/backend/src/socket.ts | 10 +- apps/backend/src/upload/README.md | 232 ++++++++++++++++++ apps/backend/src/upload/scheduler.ts | 40 +++ apps/backend/src/upload/storage.adapter.ts | 185 ++++++++++++++ apps/backend/src/upload/storage.utils.ts | 202 +++++++++++++++ apps/backend/src/upload/tus.ts | 153 ++++++++++++ apps/backend/src/upload/types.ts | 29 +++ apps/backend/src/upload/upload.index.ts | 116 +++++++++ apps/backend/src/upload/upload.rest.ts | 198 +++++++++++++++ apps/backend/src/upload/utils.ts | 4 + apps/backend/src/utils/file.ts | 67 +++++ apps/web/app/websocket/page.tsx | 19 +- apps/web/hooks/useTusUpload.ts | 122 +++++++++ apps/web/package.json | 76 +++--- packages/client/src/websocket/client.ts | 4 +- packages/client/tsconfig.json | 2 +- .../20250527111157_init/migration.sql | 28 +++ .../20250527115119_init/migration.sql | 2 + packages/db/prisma/schema.prisma | 22 ++ pnpm-lock.yaml | 155 ++++++++++++ turbo.json | 113 ++++----- 25 files changed, 1779 insertions(+), 211 deletions(-) create mode 100644 apps/backend/src/upload/README.md create mode 100644 apps/backend/src/upload/scheduler.ts create mode 100644 apps/backend/src/upload/storage.adapter.ts create mode 100644 apps/backend/src/upload/storage.utils.ts create mode 100644 apps/backend/src/upload/tus.ts create mode 100644 apps/backend/src/upload/types.ts create mode 100644 apps/backend/src/upload/upload.index.ts create mode 100644 apps/backend/src/upload/upload.rest.ts create mode 100644 apps/backend/src/upload/utils.ts create mode 100644 apps/backend/src/utils/file.ts create mode 100644 apps/web/hooks/useTusUpload.ts create mode 100644 packages/db/prisma/migrations/20250527111157_init/migration.sql create mode 100644 packages/db/prisma/migrations/20250527115119_init/migration.sql diff --git a/apps/backend/.env.example b/apps/backend/.env.example index df91ae3..589f11c 100644 --- a/apps/backend/.env.example +++ b/apps/backend/.env.example @@ -12,4 +12,6 @@ REDIS_PASSWORD=nice # OIDC_COOKIE_KEY= OIDC_CLIENT_ID=your-client-id OIDC_CLIENT_SECRET=your-client-secret -OIDC_REDIRECT_URI=https://your-frontend.com/callback \ No newline at end of file +OIDC_REDIRECT_URI=https://your-frontend.com/callback + +UPLOAD_DIR=/opt/projects/nice/uploads \ No newline at end of file diff --git a/apps/backend/package.json b/apps/backend/package.json index c08ed25..1472d63 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -16,9 +16,13 @@ "jose": "^6.0.11", "minio": "7.1.3", "nanoid": "^5.1.5", + "nanoid-cjs": "^0.0.7", + "transliteration": "^2.3.5", "node-cron": "^4.0.7", "oidc-provider": "^9.1.1", "superjson": "^2.2.2", + "dayjs": "^1.11.13", + "dotenv": "^16.4.7", "zod": "^3.25.23" }, "devDependencies": { diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts index f2f5b74..8962108 100644 --- a/apps/backend/src/index.ts +++ b/apps/backend/src/index.ts @@ -15,6 +15,11 @@ import { appRouter } from './trpc'; import { createBunWebSocket } from 'hono/bun'; import { wsHandler, wsConfig } from './socket'; +// 导入新的路由 +import userRest from './user/user.rest'; +import uploadRest from './upload/upload.rest'; +import { startCleanupScheduler } from './upload/scheduler'; + type Env = { Variables: { redis: Redis; @@ -51,6 +56,10 @@ app.use( }), ); +// 添加 REST API 路由 +app.route('/api/users', userRest); +app.route('/api/upload', uploadRest); + app.use('/oidc/*', async (c, next) => { // @ts-ignore await oidc.callback(c.req.raw, c.res.raw); @@ -60,6 +69,10 @@ app.use('/oidc/*', async (c, next) => { // 添加 WebSocket 路由 app.get('/ws', wsHandler); + +// 启动上传清理定时任务 +startCleanupScheduler(); + const bunServerConfig = { port: 3000, fetch: app.fetch, diff --git a/apps/backend/src/oidc/config.ts b/apps/backend/src/oidc/config.ts index 09c71f6..e518af0 100644 --- a/apps/backend/src/oidc/config.ts +++ b/apps/backend/src/oidc/config.ts @@ -3,107 +3,109 @@ import { RedisAdapter } from './redis-adapter'; import { prisma } from '@repo/db'; async function getClients() { - const dbClients = await prisma.oidcClient.findMany?.(); - const dbClientList = (dbClients && dbClients.length > 0) - ? dbClients.map(c => ({ - client_id: c.clientId, - client_secret: c.clientSecret, - grant_types: JSON.parse(c.grantTypes), // string -> string[] - redirect_uris: JSON.parse(c.redirectUris), // string -> string[] - response_types: JSON.parse(c.responseTypes), // string -> string[] - scope: c.scope, - })) - : []; + const dbClients = await prisma.oidcClient.findMany?.(); + const dbClientList = + dbClients && dbClients.length > 0 + ? dbClients.map((c) => ({ + client_id: c.clientId, + client_secret: c.clientSecret, + grant_types: JSON.parse(c.grantTypes), // string -> string[] + redirect_uris: JSON.parse(c.redirectUris), // string -> string[] + response_types: JSON.parse(c.responseTypes), // string -> string[] + scope: c.scope, + })) + : []; - // 管理后台client,通过环境变量读取 - const defaultClient = { - client_id: process.env.OIDC_CLIENT_ID || 'admin-client', - client_secret: process.env.OIDC_CLIENT_SECRET || 'admin-secret', - grant_types: ['authorization_code', 'refresh_token'], - redirect_uris: [process.env.OIDC_REDIRECT_URI || 'http://localhost:3000/callback'], - response_types: ['code'], - scope: 'openid email profile', - }; + // 管理后台client,通过环境变量读取 + const defaultClient = { + client_id: process.env.OIDC_CLIENT_ID || 'admin-client', + client_secret: process.env.OIDC_CLIENT_SECRET || 'admin-secret', + grant_types: ['authorization_code', 'refresh_token'], + redirect_uris: [process.env.OIDC_REDIRECT_URI || 'http://localhost:3000/callback'], + response_types: ['code'], + scope: 'openid email profile', + }; - // 检查是否与数据库client_id重复 - const allClients = [defaultClient, ...dbClientList.filter(c => c.client_id !== defaultClient.client_id)]; + // 检查是否与数据库client_id重复 + const allClients = [defaultClient, ...dbClientList.filter((c) => c.client_id !== defaultClient.client_id)]; - return allClients; + return allClients; } const OIDC_COOKIE_KEY = process.env.OIDC_COOKIE_KEY || 'HrbEPlzByV0CcjFJhl2pjKV2iG8FgQIc'; const config: Configuration = { - adapter: RedisAdapter, - // 注意:clients字段现在是Promise,需在Provider初始化时await - clients: await getClients(), - pkce: { - required: () => true, - }, - features: { - devInteractions: { enabled: false }, - resourceIndicators: { enabled: true }, - revocation: { enabled: true }, - userinfo: { enabled: true }, - registration: { enabled: true }, - }, - cookies: { - keys: [OIDC_COOKIE_KEY], - }, - jwks: { - keys: [ - { - d: 'VEZOsY07JTFzGTqv6cC2Y32vsfChind2I_TTuvV225_-0zrSej3XLRg8iE_u0-3GSgiGi4WImmTwmEgLo4Qp3uEcxCYbt4NMJC7fwT2i3dfRZjtZ4yJwFl0SIj8TgfQ8ptwZbFZUlcHGXZIr4nL8GXyQT0CK8wy4COfmymHrrUoyfZA154ql_OsoiupSUCRcKVvZj2JHL2KILsq_sh_l7g2dqAN8D7jYfJ58MkqlknBMa2-zi5I0-1JUOwztVNml_zGrp27UbEU60RqV3GHjoqwI6m01U7K0a8Q_SQAKYGqgepbAYOA-P4_TLl5KC4-WWBZu_rVfwgSENwWNEhw8oQ', - dp: 'E1Y-SN4bQqX7kP-bNgZ_gEv-pixJ5F_EGocHKfS56jtzRqQdTurrk4jIVpI-ZITA88lWAHxjD-OaoJUh9Jupd_lwD5Si80PyVxOMI2xaGQiF0lbKJfD38Sh8frRpgelZVaK_gm834B6SLfxKdNsP04DsJqGKktODF_fZeaGFPH0', - dq: 'F90JPxevQYOlAgEH0TUt1-3_hyxY6cfPRU2HQBaahyWrtCWpaOzenKZnvGFZdg-BuLVKjCchq3G_70OLE-XDP_ol0UTJmDTT-WyuJQdEMpt_WFF9yJGoeIu8yohfeLatU-67ukjghJ0s9CBzNE_LrGEV6Cup3FXywpSYZAV3iqc', - e: 'AQAB', - kty: 'RSA', - n: 'xwQ72P9z9OYshiQ-ntDYaPnnfwG6u9JAdLMZ5o0dmjlcyrvwQRdoFIKPnO65Q8mh6F_LDSxjxa2Yzo_wdjhbPZLjfUJXgCzm54cClXzT5twzo7lzoAfaJlkTsoZc2HFWqmcri0BuzmTFLZx2Q7wYBm0pXHmQKF0V-C1O6NWfd4mfBhbM-I1tHYSpAMgarSm22WDMDx-WWI7TEzy2QhaBVaENW9BKaKkJklocAZCxk18WhR0fckIGiWiSM5FcU1PY2jfGsTmX505Ub7P5Dz75Ygqrutd5tFrcqyPAtPTFDk8X1InxkkUwpP3nFU5o50DGhwQolGYKPGtQ-ZtmbOfcWQ', - p: '5wC6nY6Ev5FqcLPCqn9fC6R9KUuBej6NaAVOKW7GXiOJAq2WrileGKfMc9kIny20zW3uWkRLm-O-3Yzze1zFpxmqvsvCxZ5ERVZ6leiNXSu3tez71ZZwp0O9gys4knjrI-9w46l_vFuRtjL6XEeFfHEZFaNJpz-lcnb3w0okrbM', - q: '3I1qeEDslZFB8iNfpKAdWtz_Wzm6-jayT_V6aIvhvMj5mnU-Xpj75zLPQSGa9wunMlOoZW9w1wDO1FVuDhwzeOJaTm-Ds0MezeC4U6nVGyyDHb4CUA3ml2tzt4yLrqGYMT7XbADSvuWYADHw79OFjEi4T3s3tJymhaBvy1ulv8M', - qi: 'wSbXte9PcPtr788e713KHQ4waE26CzoXx-JNOgN0iqJMN6C4_XJEX-cSvCZDf4rh7xpXN6SGLVd5ibIyDJi7bbi5EQ5AXjazPbLBjRthcGXsIuZ3AtQyR0CEWNSdM7EyM5TRdyZQ9kftfz9nI03guW3iKKASETqX2vh0Z8XRjyU', - use: 'sig', - }, { - crv: 'P-256', - d: 'K9xfPv773dZR22TVUB80xouzdF7qCg5cWjPjkHyv7Ws', - kty: 'EC', - use: 'sig', - x: 'FWZ9rSkLt6Dx9E3pxLybhdM6xgR5obGsj5_pqmnz5J4', - y: '_n8G69C-A2Xl4xUW2lF0i8ZGZnk_KPYrhv4GbTGu5G4', - }, - ], - }, - ttl: { - AccessToken: 3600, - AuthorizationCode: 600, - IdToken: 3600, - RefreshToken: 1209600, - BackchannelAuthenticationRequest: 600, - ClientCredentials: 600, - DeviceCode: 600, - Grant: 1209600, - Interaction: 3600, - Session: 1209600, - RegistrationAccessToken: 3600, - DPoPProof: 300, - PushedAuthorizationRequest: 600, - ReplayDetection: 3600, - LogoutToken: 600, - }, - findAccount: async (ctx, id) => { - const user = await prisma.user.findUnique({ where: { id } }); - if (!user) return undefined; - return { - accountId: user.id, - async claims() { - return { - sub: user.id, - email: user.email, - name: user.name, - }; - }, - }; - }, + adapter: RedisAdapter, + // 注意:clients字段现在是Promise,需在Provider初始化时await + clients: await getClients(), + pkce: { + required: () => true, + }, + features: { + devInteractions: { enabled: false }, + resourceIndicators: { enabled: true }, + revocation: { enabled: true }, + userinfo: { enabled: true }, + registration: { enabled: true }, + }, + cookies: { + keys: [OIDC_COOKIE_KEY], + }, + jwks: { + keys: [ + { + d: 'VEZOsY07JTFzGTqv6cC2Y32vsfChind2I_TTuvV225_-0zrSej3XLRg8iE_u0-3GSgiGi4WImmTwmEgLo4Qp3uEcxCYbt4NMJC7fwT2i3dfRZjtZ4yJwFl0SIj8TgfQ8ptwZbFZUlcHGXZIr4nL8GXyQT0CK8wy4COfmymHrrUoyfZA154ql_OsoiupSUCRcKVvZj2JHL2KILsq_sh_l7g2dqAN8D7jYfJ58MkqlknBMa2-zi5I0-1JUOwztVNml_zGrp27UbEU60RqV3GHjoqwI6m01U7K0a8Q_SQAKYGqgepbAYOA-P4_TLl5KC4-WWBZu_rVfwgSENwWNEhw8oQ', + dp: 'E1Y-SN4bQqX7kP-bNgZ_gEv-pixJ5F_EGocHKfS56jtzRqQdTurrk4jIVpI-ZITA88lWAHxjD-OaoJUh9Jupd_lwD5Si80PyVxOMI2xaGQiF0lbKJfD38Sh8frRpgelZVaK_gm834B6SLfxKdNsP04DsJqGKktODF_fZeaGFPH0', + dq: 'F90JPxevQYOlAgEH0TUt1-3_hyxY6cfPRU2HQBaahyWrtCWpaOzenKZnvGFZdg-BuLVKjCchq3G_70OLE-XDP_ol0UTJmDTT-WyuJQdEMpt_WFF9yJGoeIu8yohfeLatU-67ukjghJ0s9CBzNE_LrGEV6Cup3FXywpSYZAV3iqc', + e: 'AQAB', + kty: 'RSA', + n: 'xwQ72P9z9OYshiQ-ntDYaPnnfwG6u9JAdLMZ5o0dmjlcyrvwQRdoFIKPnO65Q8mh6F_LDSxjxa2Yzo_wdjhbPZLjfUJXgCzm54cClXzT5twzo7lzoAfaJlkTsoZc2HFWqmcri0BuzmTFLZx2Q7wYBm0pXHmQKF0V-C1O6NWfd4mfBhbM-I1tHYSpAMgarSm22WDMDx-WWI7TEzy2QhaBVaENW9BKaKkJklocAZCxk18WhR0fckIGiWiSM5FcU1PY2jfGsTmX505Ub7P5Dz75Ygqrutd5tFrcqyPAtPTFDk8X1InxkkUwpP3nFU5o50DGhwQolGYKPGtQ-ZtmbOfcWQ', + p: '5wC6nY6Ev5FqcLPCqn9fC6R9KUuBej6NaAVOKW7GXiOJAq2WrileGKfMc9kIny20zW3uWkRLm-O-3Yzze1zFpxmqvsvCxZ5ERVZ6leiNXSu3tez71ZZwp0O9gys4knjrI-9w46l_vFuRtjL6XEeFfHEZFaNJpz-lcnb3w0okrbM', + q: '3I1qeEDslZFB8iNfpKAdWtz_Wzm6-jayT_V6aIvhvMj5mnU-Xpj75zLPQSGa9wunMlOoZW9w1wDO1FVuDhwzeOJaTm-Ds0MezeC4U6nVGyyDHb4CUA3ml2tzt4yLrqGYMT7XbADSvuWYADHw79OFjEi4T3s3tJymhaBvy1ulv8M', + qi: 'wSbXte9PcPtr788e713KHQ4waE26CzoXx-JNOgN0iqJMN6C4_XJEX-cSvCZDf4rh7xpXN6SGLVd5ibIyDJi7bbi5EQ5AXjazPbLBjRthcGXsIuZ3AtQyR0CEWNSdM7EyM5TRdyZQ9kftfz9nI03guW3iKKASETqX2vh0Z8XRjyU', + use: 'sig', + }, + { + crv: 'P-256', + d: 'K9xfPv773dZR22TVUB80xouzdF7qCg5cWjPjkHyv7Ws', + kty: 'EC', + use: 'sig', + x: 'FWZ9rSkLt6Dx9E3pxLybhdM6xgR5obGsj5_pqmnz5J4', + y: '_n8G69C-A2Xl4xUW2lF0i8ZGZnk_KPYrhv4GbTGu5G4', + }, + ], + }, + ttl: { + AccessToken: 3600, + AuthorizationCode: 600, + IdToken: 3600, + RefreshToken: 1209600, + BackchannelAuthenticationRequest: 600, + ClientCredentials: 600, + DeviceCode: 600, + Grant: 1209600, + Interaction: 3600, + Session: 1209600, + RegistrationAccessToken: 3600, + DPoPProof: 300, + PushedAuthorizationRequest: 600, + ReplayDetection: 3600, + LogoutToken: 600, + }, + findAccount: async (ctx, id) => { + const user = await prisma.user.findUnique({ where: { id } }); + if (!user) return undefined; + return { + accountId: user.id, + async claims() { + return { + sub: user.id, + email: user.email, + name: user.name, + }; + }, + }; + }, }; -export default config; \ No newline at end of file +export default config; diff --git a/apps/backend/src/socket.ts b/apps/backend/src/socket.ts index 27c5269..95e78b6 100644 --- a/apps/backend/src/socket.ts +++ b/apps/backend/src/socket.ts @@ -14,7 +14,7 @@ interface WSMessageParams { } // 定义消息类型接口 interface WSMessage { - type: 'join' | 'leave' | 'message'; + action: 'join' | 'leave' | 'message'; roomId: string; data: WSMessageParams; } @@ -39,7 +39,7 @@ const wsHandler = upgradeWebSocket((c) => { const parsedMessage: WSMessage = JSON.parse(message.data as any); console.log('收到消息:', parsedMessage); - switch (parsedMessage.type) { + switch (parsedMessage.action) { case 'join': // 加入房间 if (!rooms.has(parsedMessage.roomId)) { @@ -51,7 +51,7 @@ const wsHandler = upgradeWebSocket((c) => { // 发送加入成功消息 ws.send( JSON.stringify({ - type: 'system', + action: 'system', data: { text: `成功加入房间 ${parsedMessage.roomId}`, type: MessageType.TEXT, @@ -75,7 +75,7 @@ const wsHandler = upgradeWebSocket((c) => { const room = rooms.get(parsedMessage.roomId); if (room) { const messageToSend = { - type: 'message', + action: 'message', data: parsedMessage.data, roomId: parsedMessage.roomId, }; @@ -92,7 +92,7 @@ const wsHandler = upgradeWebSocket((c) => { console.error('处理消息时出错:', error); ws.send( JSON.stringify({ - type: 'error', + action: 'error', data: { text: '消息处理失败', type: MessageType.TEXT, diff --git a/apps/backend/src/upload/README.md b/apps/backend/src/upload/README.md new file mode 100644 index 0000000..38aac07 --- /dev/null +++ b/apps/backend/src/upload/README.md @@ -0,0 +1,232 @@ +# 上传模块架构改造 + +本模块已从 NestJS 架构成功改造为 Hono + Bun 架构,并支持多种存储后端的无感切换。 + +## 文件结构 + +``` +src/upload/ +├── tus.ts # TUS 协议服务核心实现 +├── upload.index.ts # 资源管理相关函数 +├── upload.rest.ts # Hono REST API 路由 +├── storage.adapter.ts # 存储适配器系统 🆕 +├── storage.utils.ts # 存储工具类 🆕 +├── scheduler.ts # 定时清理任务 +├── utils.ts # 工具函数 +├── types.ts # 类型定义 +└── README.md # 本文档 +``` + +## 存储适配器系统 + +### 支持的存储类型 + +1. **本地存储 (Local)** - 文件存储在本地文件系统 +2. **S3 存储 (S3)** - 文件存储在 AWS S3 或兼容的对象存储服务 + +### 环境变量配置 + +#### 本地存储配置 + +```bash +STORAGE_TYPE=local +UPLOAD_DIR=./uploads +UPLOAD_EXPIRATION_MS=0 # 0 表示不自动过期(推荐设置) +``` + +#### S3 存储配置 + +```bash +STORAGE_TYPE=s3 +S3_BUCKET=your-bucket-name +S3_REGION=us-east-1 +S3_ACCESS_KEY_ID=your-access-key +S3_SECRET_ACCESS_KEY=your-secret-key +S3_ENDPOINT=https://s3.amazonaws.com # 可选,支持其他 S3 兼容服务 +S3_FORCE_PATH_STYLE=false # 可选,路径风格 +S3_PART_SIZE=8388608 # 可选,分片大小 (8MB) +S3_MAX_CONCURRENT_UPLOADS=60 # 可选,最大并发上传数 +UPLOAD_EXPIRATION_MS=0 # 0 表示不自动过期(推荐设置) +``` + +### 存储类型记录 + +- **数据库支持**: 每个资源记录都包含 `storageType` 字段,标识文件使用的存储后端 +- **自动记录**: 上传时自动记录当前的存储类型 +- **迁移支持**: 支持批量更新现有资源的存储类型标记 + +### 不过期设置 + +- **默认行为**: 过期时间默认设为 0,表示文件不会自动过期 +- **手动清理**: 提供多种手动清理选项 +- **灵活控制**: 可根据需要设置过期时间,或完全禁用自动清理 + +### 无感切换机制 + +1. **单例模式管理**: `StorageManager` 使用单例模式确保全局一致性 +2. **自动配置检测**: 启动时根据环境变量自动选择存储类型 +3. **统一接口**: 所有存储类型都实现相同的 TUS `DataStore` 接口 +4. **运行时切换**: 支持运行时切换存储配置(需要重启生效) + +## API 端点 + +### 资源管理 + +- `GET /api/upload/resource/:fileId` - 获取文件资源信息 +- `GET /api/upload/resources` - 获取所有资源 +- `GET /api/upload/resources/storage/:storageType` - 🆕 根据存储类型获取资源 +- `GET /api/upload/resources/status/:status` - 🆕 根据状态获取资源 +- `GET /api/upload/resources/uploading` - 🆕 获取正在上传的资源 +- `GET /api/upload/stats` - 🆕 获取资源统计信息 +- `DELETE /api/upload/resource/:id` - 删除资源 +- `PATCH /api/upload/resource/:id` - 更新资源 +- `POST /api/upload/cleanup` - 手动触发清理 +- `POST /api/upload/cleanup/by-status` - 🆕 根据状态清理资源 +- `POST /api/upload/migrate-storage` - 🆕 迁移资源存储类型标记 + +### 存储管理 + +- `GET /api/upload/storage/info` - 获取当前存储配置信息 +- `POST /api/upload/storage/switch` - 切换存储类型 +- `POST /api/upload/storage/validate` - 验证存储配置 + +### TUS 协议 + +- `OPTIONS /api/upload/*` - TUS 协议选项请求 +- `HEAD /api/upload/*` - TUS 协议头部请求 +- `POST /api/upload/*` - TUS 协议创建上传 +- `PATCH /api/upload/*` - TUS 协议上传数据 +- `GET /api/upload/*` - TUS 协议获取状态 + +## 新增 API 使用示例 + +### 获取存储类型统计 + +```javascript +const response = await fetch('/api/upload/stats'); +const stats = await response.json(); +// { +// total: 150, +// byStatus: { "UPLOADED": 120, "UPLOADING": 5, "PROCESSED": 25 }, +// byStorageType: { "local": 80, "s3": 70 } +// } +``` + +### 查询特定存储类型的资源 + +```javascript +const response = await fetch('/api/upload/resources/storage/s3'); +const s3Resources = await response.json(); +``` + +### 迁移存储类型标记 + +```javascript +const response = await fetch('/api/upload/migrate-storage', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ from: 'local', to: 's3' }), +}); +// { success: true, message: "Migrated 50 resources from local to s3", count: 50 } +``` + +### 手动清理特定状态的资源 + +```javascript +const response = await fetch('/api/upload/cleanup/by-status', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + status: 'UPLOADING', + olderThanDays: 7, + }), +}); +``` + +## 🆕 存储管理示例 + +### 获取存储信息 + +```javascript +const response = await fetch('/api/upload/storage/info'); +const storageInfo = await response.json(); +// { type: 'local', config: { directory: './uploads' } } +``` + +### 切换到 S3 存储 + +```javascript +const newConfig = { + type: 's3', + s3: { + bucket: 'my-bucket', + region: 'us-west-2', + accessKeyId: 'YOUR_ACCESS_KEY', + secretAccessKey: 'YOUR_SECRET_KEY', + }, +}; + +const response = await fetch('/api/upload/storage/switch', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(newConfig), +}); +``` + +### 验证存储配置 + +```javascript +const response = await fetch('/api/upload/storage/validate', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(newConfig), +}); +const validation = await response.json(); +// { valid: true, message: 'Storage configuration is valid' } +``` + +## 特性保留 + +1. **TUS 协议支持** - 完全保留原有的断点续传功能 +2. **文件命名** - 保留安全的文件命名策略 +3. **资源状态管理** - 保留完整的上传状态跟踪 +4. **自动清理** - 保留过期文件清理功能(默认禁用) +5. **数据库集成** - 保留 Prisma ORM 数据库操作 + +## 🆕 新增特性 + +1. **多存储后端支持** - 支持本地存储和 S3 存储 +2. **无感切换** - 运行时可切换存储类型 +3. **配置验证** - 提供存储配置验证功能 +4. **存储信息查询** - 可查询当前存储配置 +5. **统一日志** - 存储操作统一日志记录 +6. **🆕 存储类型记录** - 数据库记录每个资源的存储类型 +7. **🆕 灵活清理** - 支持按状态、时间等条件清理 +8. **🆕 统计分析** - 提供详细的资源统计信息 +9. **🆕 不过期设置** - 默认不自动过期,避免意外删除 + +## 运行 + +服务启动时会自动: + +1. 根据环境变量初始化存储适配器 +2. 初始化 TUS 服务器 +3. 注册 REST API 路由 +4. 启动定时清理任务(如果启用) + +支持的存储切换场景: + +- 开发环境使用本地存储 +- 生产环境使用 S3 存储 +- 混合云部署灵活切换 +- 存储迁移时批量更新资源标记 + +## 💡 最佳实践 + +1. **过期设置**: 推荐设置 `UPLOAD_EXPIRATION_MS=0` 避免文件意外过期 +2. **存储记录**: 利用数据库中的 `storageType` 字段追踪文件位置 +3. **定期清理**: 使用手动清理 API 定期清理不需要的资源 +4. **监控统计**: 使用统计 API 监控存储使用情况 +5. **迁移策略**: 在存储迁移时先更新环境变量,再使用迁移 API 更新数据库标记 + +无需代码修改,仅通过环境变量即可实现存储后端的无感切换。 diff --git a/apps/backend/src/upload/scheduler.ts b/apps/backend/src/upload/scheduler.ts new file mode 100644 index 0000000..fe4d09f --- /dev/null +++ b/apps/backend/src/upload/scheduler.ts @@ -0,0 +1,40 @@ +import { cleanupExpiredUploads } from './tus'; + +// 设置定时清理任务 - 每天凌晨执行 +export function startCleanupScheduler() { + // 每 24 小时执行一次清理任务 + const CLEANUP_INTERVAL = 24 * 60 * 60 * 1000; // 24 hours in milliseconds + + // 立即执行一次清理 + setTimeout(async () => { + console.log('Starting initial cleanup...'); + try { + await cleanupExpiredUploads(); + } catch (error) { + console.error('Initial cleanup failed:', error); + } + }, 5000); // 启动 5 秒后执行 + + // 设置定期清理 + setInterval(async () => { + console.log('Starting scheduled cleanup...'); + try { + await cleanupExpiredUploads(); + } catch (error) { + console.error('Scheduled cleanup failed:', error); + } + }, CLEANUP_INTERVAL); + + console.log('Upload cleanup scheduler started - will run every 24 hours'); +} + +// 手动触发清理(可用于 API 调用) +export async function triggerCleanup() { + console.log('Manual cleanup triggered...'); + try { + return await cleanupExpiredUploads(); + } catch (error) { + console.error('Manual cleanup failed:', error); + throw error; + } +} diff --git a/apps/backend/src/upload/storage.adapter.ts b/apps/backend/src/upload/storage.adapter.ts new file mode 100644 index 0000000..6ba69fd --- /dev/null +++ b/apps/backend/src/upload/storage.adapter.ts @@ -0,0 +1,185 @@ +import { FileStore, S3Store } from '@repo/tus'; +import type { DataStore } from '@repo/tus'; + +// 存储类型枚举 +export enum StorageType { + LOCAL = 'local', + S3 = 's3', +} + +// 存储配置接口 +export interface StorageConfig { + type: StorageType; + // 本地存储配置 + local?: { + directory: string; + expirationPeriodInMilliseconds?: number; + }; + // S3 存储配置 + s3?: { + bucket: string; + region: string; + accessKeyId: string; + secretAccessKey: string; + endpoint?: string; // 用于兼容其他 S3 兼容服务 + forcePathStyle?: boolean; + partSize?: number; + maxConcurrentPartUploads?: number; + expirationPeriodInMilliseconds?: number; + }; +} + +// 从环境变量获取存储配置 +export function getStorageConfig(): StorageConfig { + const storageType = (process.env.STORAGE_TYPE || 'local') as StorageType; + + const config: StorageConfig = { + type: storageType, + }; + + if (storageType === StorageType.LOCAL) { + config.local = { + directory: process.env.UPLOAD_DIR || './uploads', + expirationPeriodInMilliseconds: parseInt(process.env.UPLOAD_EXPIRATION_MS || '0'), // 默认不过期 + }; + } else if (storageType === StorageType.S3) { + config.s3 = { + bucket: process.env.S3_BUCKET || '', + region: process.env.S3_REGION || 'us-east-1', + accessKeyId: process.env.S3_ACCESS_KEY_ID || '', + secretAccessKey: process.env.S3_SECRET_ACCESS_KEY || '', + endpoint: process.env.S3_ENDPOINT, + forcePathStyle: process.env.S3_FORCE_PATH_STYLE === 'true', + partSize: parseInt(process.env.S3_PART_SIZE || '8388608'), // 8MB + maxConcurrentPartUploads: parseInt(process.env.S3_MAX_CONCURRENT_UPLOADS || '60'), + expirationPeriodInMilliseconds: parseInt(process.env.UPLOAD_EXPIRATION_MS || '0'), // 默认不过期 + }; + } + + return config; +} + +// 验证存储配置 +export function validateStorageConfig(config: StorageConfig): string[] { + const errors: string[] = []; + + if (config.type === StorageType.LOCAL) { + if (!config.local?.directory) { + errors.push('Local storage directory is required'); + } + } else if (config.type === StorageType.S3) { + const s3Config = config.s3; + if (!s3Config?.bucket) errors.push('S3 bucket is required'); + if (!s3Config?.region) errors.push('S3 region is required'); + if (!s3Config?.accessKeyId) errors.push('S3 access key ID is required'); + if (!s3Config?.secretAccessKey) errors.push('S3 secret access key is required'); + } else { + errors.push(`Unsupported storage type: ${config.type}`); + } + + return errors; +} + +// 创建存储实例 +export function createStorageInstance(config: StorageConfig): DataStore { + // 验证配置 + const errors = validateStorageConfig(config); + if (errors.length > 0) { + throw new Error(`Storage configuration errors: ${errors.join(', ')}`); + } + + switch (config.type) { + case StorageType.LOCAL: + return new FileStore({ + directory: config.local!.directory, + expirationPeriodInMilliseconds: config.local!.expirationPeriodInMilliseconds, + }); + + case StorageType.S3: + const s3Config = config.s3!; + return new S3Store({ + partSize: s3Config.partSize, + maxConcurrentPartUploads: s3Config.maxConcurrentPartUploads, + expirationPeriodInMilliseconds: s3Config.expirationPeriodInMilliseconds, + s3ClientConfig: { + bucket: s3Config.bucket, + region: s3Config.region, + credentials: { + accessKeyId: s3Config.accessKeyId, + secretAccessKey: s3Config.secretAccessKey, + }, + endpoint: s3Config.endpoint, + forcePathStyle: s3Config.forcePathStyle, + }, + }); + + default: + throw new Error(`Unsupported storage type: ${config.type}`); + } +} + +// 存储管理器类 +export class StorageManager { + private static instance: StorageManager; + private storageConfig: StorageConfig; + private dataStore: DataStore; + + private constructor() { + this.storageConfig = getStorageConfig(); + this.dataStore = createStorageInstance(this.storageConfig); + + console.log(`Storage initialized: ${this.storageConfig.type}`); + if (this.storageConfig.type === StorageType.LOCAL) { + console.log(`Local directory: ${this.storageConfig.local?.directory}`); + } else if (this.storageConfig.type === StorageType.S3) { + console.log(`S3 bucket: ${this.storageConfig.s3?.bucket} in region: ${this.storageConfig.s3?.region}`); + } + } + + public static getInstance(): StorageManager { + if (!StorageManager.instance) { + StorageManager.instance = new StorageManager(); + } + return StorageManager.instance; + } + + public getDataStore(): DataStore { + return this.dataStore; + } + + public getStorageConfig(): StorageConfig { + return this.storageConfig; + } + + public getStorageType(): StorageType { + return this.storageConfig.type; + } + + // 切换存储类型(需要重启应用) + public async switchStorage(newConfig: StorageConfig): Promise { + const errors = validateStorageConfig(newConfig); + if (errors.length > 0) { + throw new Error(`Invalid storage configuration: ${errors.join(', ')}`); + } + + this.storageConfig = newConfig; + this.dataStore = createStorageInstance(newConfig); + + console.log(`Storage switched to: ${newConfig.type}`); + } + + // 获取存储统计信息 + public getStorageInfo() { + return { + type: this.storageConfig.type, + config: + this.storageConfig.type === StorageType.LOCAL + ? { directory: this.storageConfig.local?.directory } + : { + bucket: this.storageConfig.s3?.bucket, + region: this.storageConfig.s3?.region, + endpoint: this.storageConfig.s3?.endpoint, + }, + }; + } +} diff --git a/apps/backend/src/upload/storage.utils.ts b/apps/backend/src/upload/storage.utils.ts new file mode 100644 index 0000000..1c37c8d --- /dev/null +++ b/apps/backend/src/upload/storage.utils.ts @@ -0,0 +1,202 @@ +import { StorageManager, StorageType } from './storage.adapter'; +import path from 'path'; + +/** + * 存储工具类 - 处理不同存储类型的文件操作 + */ +export class StorageUtils { + private static instance: StorageUtils; + private storageManager: StorageManager; + + private constructor() { + this.storageManager = StorageManager.getInstance(); + } + + public static getInstance(): StorageUtils { + if (!StorageUtils.instance) { + StorageUtils.instance = new StorageUtils(); + } + return StorageUtils.instance; + } + + /** + * 生成文件访问URL + * @param fileId 文件ID + * @param isPublic 是否为公开访问链接 + * @returns 文件访问URL + */ + public generateFileUrl(fileId: string, isPublic: boolean = false): string { + const storageType = this.storageManager.getStorageType(); + const config = this.storageManager.getStorageConfig(); + + switch (storageType) { + case StorageType.LOCAL: + // 本地存储返回相对路径或服务器路径 + if (isPublic) { + // 假设有一个静态文件服务 + return `/uploads/${fileId}`; + } + return path.join(config.local?.directory || './uploads', fileId); + + case StorageType.S3: + // S3 存储返回对象存储路径 + const s3Config = config.s3!; + if (s3Config.endpoint && s3Config.endpoint !== 'https://s3.amazonaws.com') { + // 自定义 S3 兼容服务 + return `${s3Config.endpoint}/${s3Config.bucket}/${fileId}`; + } + // AWS S3 + return `https://${s3Config.bucket}.s3.${s3Config.region}.amazonaws.com/${fileId}`; + + default: + throw new Error(`Unsupported storage type: ${storageType}`); + } + } + + /** + * 生成预签名URL(仅支持S3) + * @param fileId 文件ID + * @param expiresIn 过期时间(秒) + * @returns 预签名URL + */ + public async generatePresignedUrl(fileId: string, expiresIn: number = 3600): Promise { + const storageType = this.storageManager.getStorageType(); + + if (storageType !== StorageType.S3) { + throw new Error('Presigned URLs are only supported for S3 storage'); + } + + // TODO: 实现 S3 预签名 URL 生成 + // 这需要使用 AWS SDK 的 getSignedUrl 方法 + // const s3Client = this.storageManager.getS3Client(); + // return await getSignedUrl(s3Client, new GetObjectCommand({ + // Bucket: config.s3!.bucket, + // Key: fileId + // }), { expiresIn }); + + throw new Error('Presigned URL generation not implemented yet'); + } + + /** + * 获取文件物理路径(仅本地存储) + * @param fileId 文件ID + * @returns 文件物理路径 + */ + public getFilePath(fileId: string): string { + const storageType = this.storageManager.getStorageType(); + + if (storageType !== StorageType.LOCAL) { + throw new Error('File path is only available for local storage'); + } + + const config = this.storageManager.getStorageConfig(); + return path.join(config.local?.directory || './uploads', fileId); + } + + /** + * 检查文件是否存在 + * @param fileId 文件ID + * @returns 是否存在 + */ + public async fileExists(fileId: string): Promise { + const storageType = this.storageManager.getStorageType(); + const dataStore = this.storageManager.getDataStore(); + + try { + await dataStore.getUpload(fileId); + return true; + } catch (error) { + return false; + } + } + + /** + * 删除文件 + * @param fileId 文件ID + */ + public async deleteFile(fileId: string): Promise { + const dataStore = this.storageManager.getDataStore(); + await dataStore.remove(fileId); + } + + /** + * 获取文件流(用于下载) + * @param fileId 文件ID + * @returns 文件流 + */ + public async getFileStream(fileId: string): Promise { + const storageType = this.storageManager.getStorageType(); + const dataStore = this.storageManager.getDataStore(); + + if (storageType === StorageType.LOCAL) { + // 本地存储直接返回文件流 + return (dataStore as any).read(fileId); + } else if (storageType === StorageType.S3) { + // S3 存储返回对象流 + return (dataStore as any).read(fileId); + } + + throw new Error(`File stream not supported for storage type: ${storageType}`); + } + + /** + * 复制文件到另一个位置 + * @param sourceFileId 源文件ID + * @param targetFileId 目标文件ID + */ + public async copyFile(sourceFileId: string, targetFileId: string): Promise { + const storageType = this.storageManager.getStorageType(); + + if (storageType === StorageType.LOCAL) { + // 本地存储使用文件系统复制 + const fs = await import('fs/promises'); + const sourcePath = this.getFilePath(sourceFileId); + const targetPath = this.getFilePath(targetFileId); + await fs.copyFile(sourcePath, targetPath); + } else if (storageType === StorageType.S3) { + // S3 存储使用对象复制 + // TODO: 实现 S3 对象复制 + throw new Error('S3 file copy not implemented yet'); + } + } + + /** + * 获取存储统计信息 + */ + public async getStorageStats(): Promise<{ + storageType: StorageType; + totalFiles?: number; + totalSize?: number; + availableSpace?: number; + }> { + const storageType = this.storageManager.getStorageType(); + const config = this.storageManager.getStorageConfig(); + + const stats = { + storageType, + totalFiles: undefined as number | undefined, + totalSize: undefined as number | undefined, + availableSpace: undefined as number | undefined, + }; + + if (storageType === StorageType.LOCAL) { + // 本地存储可以计算磁盘使用情况 + try { + const fs = await import('fs/promises'); + const path = await import('path'); + const uploadDir = config.local?.directory || './uploads'; + + // 计算文件总数和大小(这里简化实现) + // 实际应用中可能需要递归遍历目录 + const stat = await fs.stat(uploadDir).catch(() => null); + if (stat) { + // TODO: 实现详细的目录统计 + } + } catch (error) { + console.error('Failed to get local storage stats:', error); + } + } + + return stats; + } +} diff --git a/apps/backend/src/upload/tus.ts b/apps/backend/src/upload/tus.ts new file mode 100644 index 0000000..55d8950 --- /dev/null +++ b/apps/backend/src/upload/tus.ts @@ -0,0 +1,153 @@ +import { Server, Upload } from '@repo/tus'; +import { prisma } from '@repo/db'; +import { getFilenameWithoutExt } from '../utils/file'; +import { nanoid } from 'nanoid-cjs'; +import { slugify } from 'transliteration'; +import { StorageManager } from './storage.adapter'; + +const FILE_UPLOAD_CONFIG = { + maxSizeBytes: 20_000_000_000, // 20GB +}; + +export enum QueueJobType { + UPDATE_STATS = 'update_stats', + FILE_PROCESS = 'file_process', + UPDATE_POST_VISIT_COUNT = 'updatePostVisitCount', + UPDATE_POST_STATE = 'updatePostState', +} + +export enum ResourceStatus { + UPLOADING = 'UPLOADING', + UPLOADED = 'UPLOADED', + PROCESS_PENDING = 'PROCESS_PENDING', + PROCESSING = 'PROCESSING', + PROCESSED = 'PROCESSED', + PROCESS_FAILED = 'PROCESS_FAILED', +} + +// 全局 TUS 服务器实例 +let tusServer: Server | null = null; + +function getFileId(uploadId: string) { + return uploadId.replace(/\/[^/]+$/, ''); +} + +async function handleUploadCreate(req: any, res: any, upload: Upload, url: string) { + try { + const fileId = getFileId(upload.id); + const storageManager = StorageManager.getInstance(); + + await prisma.resource.create({ + data: { + title: getFilenameWithoutExt(upload.metadata?.filename || 'untitled'), + fileId, // 移除最后的文件名 + url: upload.id, + meta: upload.metadata, + status: ResourceStatus.UPLOADING, + storageType: storageManager.getStorageType(), // 记录存储类型 + }, + }); + + console.log(`Resource created for ${upload.id} using ${storageManager.getStorageType()} storage`); + } catch (error) { + console.error('Failed to create resource during upload', error); + } +} + +async function handleUploadFinish(req: any, res: any, upload: Upload) { + try { + const resource = await prisma.resource.update({ + where: { fileId: getFileId(upload.id) }, + data: { status: ResourceStatus.UPLOADED }, + }); + + // TODO: 这里可以添加队列处理逻辑 + // fileQueue.add(QueueJobType.FILE_PROCESS, { resource }, { jobId: resource.id }); + console.log(`Upload finished ${resource.url} using ${StorageManager.getInstance().getStorageType()} storage`); + } catch (error) { + console.error('Failed to update resource after upload', error); + } +} + +function initializeTusServer() { + if (tusServer) { + return tusServer; + } + + // 获取存储管理器实例 + const storageManager = StorageManager.getInstance(); + const dataStore = storageManager.getDataStore(); + + tusServer = new Server({ + namingFunction(req, metadata) { + const safeFilename = slugify(metadata?.filename || 'untitled'); + const now = new Date(); + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, '0'); + const day = String(now.getDate()).padStart(2, '0'); + const uniqueId = nanoid(10); + return `${year}/${month}/${day}/${uniqueId}/${safeFilename}`; + }, + path: '/upload', + datastore: dataStore, // 使用存储适配器 + maxSize: FILE_UPLOAD_CONFIG.maxSizeBytes, + postReceiveInterval: 1000, + getFileIdFromRequest: (req, lastPath) => { + const match = req.url?.match(/\/upload\/(.+)/); + return match ? match[1] : lastPath; + }, + }); + + // 设置事件处理器 + tusServer.on('POST_CREATE', handleUploadCreate); + tusServer.on('POST_FINISH', handleUploadFinish); + + console.log(`TUS server initialized with ${storageManager.getStorageType()} storage`); + return tusServer; +} + +export function getTusServer() { + return initializeTusServer(); +} + +export async function handleTusRequest(req: any, res: any) { + const server = getTusServer(); + return server.handle(req, res); +} + +export async function cleanupExpiredUploads() { + try { + const storageManager = StorageManager.getInstance(); + + // 获取过期时间配置,如果设置为 0 则不自动清理 + const expirationPeriod: number = 24 * 60 * 60 * 1000; + + // Delete incomplete uploads older than expiration period + const deletedResources = await prisma.resource.deleteMany({ + where: { + createdAt: { + lt: new Date(Date.now() - expirationPeriod), + }, + status: ResourceStatus.UPLOADING, + }, + }); + + const server = getTusServer(); + const expiredUploadCount = await server.cleanUpExpiredUploads(); + + console.log( + `Cleanup complete: ${deletedResources.count} resources and ${expiredUploadCount} uploads removed from ${storageManager.getStorageType()} storage`, + ); + + return { deletedResources: deletedResources.count, expiredUploads: expiredUploadCount }; + } catch (error) { + console.error('Expired uploads cleanup failed', error); + throw error; + } +} + +// 获取存储信息 +export function getStorageInfo() { + const storageManager = StorageManager.getInstance(); + return storageManager.getStorageInfo(); +} diff --git a/apps/backend/src/upload/types.ts b/apps/backend/src/upload/types.ts new file mode 100644 index 0000000..2140ebc --- /dev/null +++ b/apps/backend/src/upload/types.ts @@ -0,0 +1,29 @@ +export interface UploadCompleteEvent { + identifier: string; + filename: string; + size: number; + hash: string; + integrityVerified: boolean; +} + +export type UploadEvent = { + uploadStart: { + identifier: string; + filename: string; + totalSize: number; + resuming?: boolean; + }; + uploadComplete: UploadCompleteEvent; + uploadError: { identifier: string; error: string; filename: string }; +}; +export interface UploadLock { + clientId: string; + timestamp: number; +} +// 添加重试机制,处理临时网络问题 +// 实现定期清理过期的临时文件 +// 添加文件完整性校验 +// 实现上传进度持久化,支持服务重启后恢复 +// 添加并发限制,防止系统资源耗尽 +// 实现文件去重功能,避免重复上传 +// 添加日志记录和监控机制 diff --git a/apps/backend/src/upload/upload.index.ts b/apps/backend/src/upload/upload.index.ts new file mode 100644 index 0000000..83066fc --- /dev/null +++ b/apps/backend/src/upload/upload.index.ts @@ -0,0 +1,116 @@ +import { prisma } from '@repo/db'; +import type { Resource } from '@repo/db'; +import { StorageType } from './storage.adapter'; + +export async function getResourceByFileId(fileId: string): Promise<{ status: string; resource?: Resource }> { + const resource = await prisma.resource.findFirst({ + where: { fileId }, + }); + + if (!resource) { + return { status: 'pending' }; + } + + return { status: 'ready', resource }; +} + +export async function getAllResources(): Promise { + return prisma.resource.findMany({ + orderBy: { createdAt: 'desc' }, + }); +} + +export async function getResourcesByStorageType(storageType: StorageType): Promise { + return prisma.resource.findMany({ + where: { + storageType: storageType, + }, + orderBy: { createdAt: 'desc' }, + }); +} + +export async function getResourcesByStatus(status: string): Promise { + return prisma.resource.findMany({ + where: { status }, + orderBy: { createdAt: 'desc' }, + }); +} + +export async function getUploadingResources(): Promise { + return prisma.resource.findMany({ + where: { + status: 'UPLOADING', + }, + orderBy: { createdAt: 'desc' }, + }); +} + +export async function getResourceStats(): Promise<{ + total: number; + byStatus: Record; + byStorageType: Record; +}> { + const [total, statusStats, storageStats] = await Promise.all([ + prisma.resource.count(), + prisma.resource.groupBy({ + by: ['status'], + _count: true, + }), + prisma.resource.groupBy({ + by: ['storageType'], + _count: true, + }), + ]); + + const byStatus = statusStats.reduce( + (acc, item) => { + acc[item.status || 'unknown'] = item._count || 0; + return acc; + }, + {} as Record, + ); + + const byStorageType = storageStats.reduce( + (acc, item) => { + const key = (item.storageType as string) || 'unknown'; + acc[key] = item._count; + return acc; + }, + {} as Record, + ); + + return { + total, + byStatus, + byStorageType, + }; +} + +export async function deleteResource(id: string): Promise { + return prisma.resource.delete({ + where: { id }, + }); +} + +export async function updateResource(id: string, data: any): Promise { + return prisma.resource.update({ + where: { id }, + data, + }); +} + +export async function migrateResourcesStorageType( + fromStorageType: StorageType, + toStorageType: StorageType, +): Promise<{ count: number }> { + const result = await prisma.resource.updateMany({ + where: { + storageType: fromStorageType, + }, + data: { + storageType: toStorageType, + }, + }); + + return { count: result.count }; +} diff --git a/apps/backend/src/upload/upload.rest.ts b/apps/backend/src/upload/upload.rest.ts new file mode 100644 index 0000000..440e457 --- /dev/null +++ b/apps/backend/src/upload/upload.rest.ts @@ -0,0 +1,198 @@ +import { Hono } from 'hono'; +import { handleTusRequest, cleanupExpiredUploads, getStorageInfo } from './tus'; +import { + getResourceByFileId, + getAllResources, + deleteResource, + updateResource, + getResourcesByStorageType, + getResourcesByStatus, + getUploadingResources, + getResourceStats, + migrateResourcesStorageType, +} from './upload.index'; +import { StorageManager, StorageType, type StorageConfig } from './storage.adapter'; +import { prisma } from '@repo/db'; + +const uploadRest = new Hono(); + +// 获取文件资源信息 +uploadRest.get('/resource/:fileId', async (c) => { + const fileId = c.req.param('fileId'); + const result = await getResourceByFileId(fileId); + return c.json(result); +}); + +// 获取所有资源 +uploadRest.get('/resources', async (c) => { + const resources = await getAllResources(); + return c.json(resources); +}); + +// 根据存储类型获取资源 +uploadRest.get('/resources/storage/:storageType', async (c) => { + const storageType = c.req.param('storageType') as StorageType; + const resources = await getResourcesByStorageType(storageType); + return c.json(resources); +}); + +// 根据状态获取资源 +uploadRest.get('/resources/status/:status', async (c) => { + const status = c.req.param('status'); + const resources = await getResourcesByStatus(status); + return c.json(resources); +}); + +// 获取正在上传的资源 +uploadRest.get('/resources/uploading', async (c) => { + const resources = await getUploadingResources(); + return c.json(resources); +}); + +// 获取资源统计信息 +uploadRest.get('/stats', async (c) => { + const stats = await getResourceStats(); + return c.json(stats); +}); + +// 删除资源 +uploadRest.delete('/resource/:id', async (c) => { + const id = c.req.param('id'); + const result = await deleteResource(id); + return c.json(result); +}); + +// 更新资源 +uploadRest.patch('/resource/:id', async (c) => { + const id = c.req.param('id'); + const data = await c.req.json(); + const result = await updateResource(id, data); + return c.json(result); +}); + +// 迁移资源存储类型(批量更新数据库中的存储类型标记) +uploadRest.post('/migrate-storage', async (c) => { + try { + const { from, to } = await c.req.json(); + const result = await migrateResourcesStorageType(from as StorageType, to as StorageType); + return c.json({ + success: true, + message: `Migrated ${result.count} resources from ${from} to ${to}`, + count: result.count, + }); + } catch (error) { + console.error('Failed to migrate storage type:', error); + return c.json( + { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + }, + 400, + ); + } +}); + +// 清理过期上传 +uploadRest.post('/cleanup', async (c) => { + const result = await cleanupExpiredUploads(); + return c.json(result); +}); + +// 手动清理指定状态的资源 +uploadRest.post('/cleanup/by-status', async (c) => { + try { + const { status, olderThanDays } = await c.req.json(); + const cutoffDate = new Date(); + cutoffDate.setDate(cutoffDate.getDate() - (olderThanDays || 30)); + + const deletedResources = await prisma.resource.deleteMany({ + where: { + status, + createdAt: { + lt: cutoffDate, + }, + }, + }); + + return c.json({ + success: true, + message: `Deleted ${deletedResources.count} resources with status ${status}`, + count: deletedResources.count, + }); + } catch (error) { + console.error('Failed to cleanup by status:', error); + return c.json( + { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + }, + 400, + ); + } +}); + +// 获取存储信息 +uploadRest.get('/storage/info', async (c) => { + const storageInfo = getStorageInfo(); + return c.json(storageInfo); +}); + +// 切换存储类型(需要重启应用) +uploadRest.post('/storage/switch', async (c) => { + try { + const newConfig = (await c.req.json()) as StorageConfig; + const storageManager = StorageManager.getInstance(); + await storageManager.switchStorage(newConfig); + + return c.json({ + success: true, + message: 'Storage configuration updated. Please restart the application for changes to take effect.', + newType: newConfig.type, + }); + } catch (error) { + console.error('Failed to switch storage:', error); + return c.json( + { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + }, + 400, + ); + } +}); + +// 验证存储配置 +uploadRest.post('/storage/validate', async (c) => { + try { + const config = (await c.req.json()) as StorageConfig; + const { validateStorageConfig } = await import('./storage.adapter'); + const errors = validateStorageConfig(config); + + if (errors.length > 0) { + return c.json({ valid: false, errors }, 400); + } + + return c.json({ valid: true, message: 'Storage configuration is valid' }); + } catch (error) { + return c.json( + { + valid: false, + errors: [error instanceof Error ? error.message : 'Invalid JSON'], + }, + 400, + ); + } +}); + +// TUS 协议处理 - 使用通用处理器 +uploadRest.all('/*', async (c) => { + try { + await handleTusRequest(c.req.raw, c.res); + return new Response(null); + } catch (error) { + console.error('TUS request error:', error); + return c.json({ error: 'Upload request failed' }, 500); + } +}); + +export default uploadRest; diff --git a/apps/backend/src/upload/utils.ts b/apps/backend/src/upload/utils.ts new file mode 100644 index 0000000..a7c189f --- /dev/null +++ b/apps/backend/src/upload/utils.ts @@ -0,0 +1,4 @@ +export function extractFileIdFromNginxUrl(url: string) { + const match = url.match(/uploads\/(\d{4}\/\d{2}\/\d{2}\/[^/]+)/); + return match ? match[1] : ''; +} diff --git a/apps/backend/src/utils/file.ts b/apps/backend/src/utils/file.ts new file mode 100644 index 0000000..812af7e --- /dev/null +++ b/apps/backend/src/utils/file.ts @@ -0,0 +1,67 @@ +import { createHash } from 'crypto'; +import { createReadStream } from 'fs'; +import path from 'path'; +import * as dotenv from 'dotenv'; +import dayjs from 'dayjs'; +dotenv.config(); +export function getFilenameWithoutExt(filename: string | null | undefined) { + return filename ? filename.replace(/\.[^/.]+$/, '') : filename || dayjs().format('YYYYMMDDHHmmss'); +} +/** + * 计算文件的 SHA-256 哈希值 + * @param filePath 文件路径 + * @returns Promise 返回文件的哈希值(十六进制字符串) + */ +export async function calculateFileHash(filePath: string): Promise { + return new Promise((resolve, reject) => { + // 创建一个 SHA-256 哈希对象 + const hash = createHash('sha256'); + // 创建文件读取流 + const readStream = createReadStream(filePath); + // 处理读取错误 + readStream.on('error', (error) => { + reject(new Error(`Failed to read file: ${error.message}`)); + }); + // 处理哈希计算错误 + hash.on('error', (error) => { + reject(new Error(`Failed to calculate hash: ${error.message}`)); + }); + // 流式处理文件内容 + readStream + .pipe(hash) + .on('finish', () => { + // 获取最终的哈希值(十六进制格式) + const fileHash = hash.digest('hex'); + resolve(fileHash); + }) + .on('error', (error) => { + reject(new Error(`Hash calculation failed: ${error.message}`)); + }); + }); +} + +/** + * 计算 Buffer 的 SHA-256 哈希值 + * @param buffer 要计算哈希的 Buffer + * @returns string 返回 Buffer 的哈希值(十六进制字符串) + */ +export function calculateBufferHash(buffer: Buffer): string { + const hash = createHash('sha256'); + hash.update(buffer); + return hash.digest('hex'); +} + +/** + * 计算字符串的 SHA-256 哈希值 + * @param content 要计算哈希的字符串 + * @returns string 返回字符串的哈希值(十六进制字符串) + */ +export function calculateStringHash(content: string): string { + const hash = createHash('sha256'); + hash.update(content); + return hash.digest('hex'); +} +export const getUploadFilePath = (fileId: string): string => { + const uploadDirectory = process.env.UPLOAD_DIR || ''; + return path.join(uploadDirectory, fileId); +}; diff --git a/apps/web/app/websocket/page.tsx b/apps/web/app/websocket/page.tsx index b044baa..d7f600e 100644 --- a/apps/web/app/websocket/page.tsx +++ b/apps/web/app/websocket/page.tsx @@ -1,11 +1,10 @@ 'use client'; import { useHello, useTRPC, useWebSocket, MessageType } from '@repo/client'; import { useQuery } from '@tanstack/react-query'; -import { useRef, useState } from 'react'; +import { useRef, useState, useEffect } from 'react'; export default function WebSocketPage() { const trpc = useTRPC(); - const { data, isLoading } = useQuery(trpc.user.getUser.queryOptions()); const [message, setMessage] = useState(''); const [roomId, setRoomId] = useState(''); const messagesEndRef = useRef(null); @@ -16,9 +15,13 @@ export default function WebSocketPage() { // 滚动到底部 const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); - setTimeout(scrollToBottom, 100); }; + // 当消息更新时自动滚动到底部 + useEffect(() => { + scrollToBottom(); + }, [messages]); + const handleJoinRoom = async () => { const success = await joinRoom(roomId.trim()); if (success) { @@ -48,6 +51,14 @@ export default function WebSocketPage() { } }; + // 处理房间ID输入框的回车事件 + const handleRoomKeyPress = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleJoinRoom(); + } + }; + return (

WebSocket 房间测试

@@ -61,7 +72,7 @@ export default function WebSocketPage() { type="text" value={roomId} onChange={(e) => setRoomId(e.target.value)} - onKeyPress={handleKeyPress} + onKeyPress={handleRoomKeyPress} disabled={connecting} className="border border-gray-300 rounded px-3 py-2 flex-1" placeholder="输入房间ID..." diff --git a/apps/web/hooks/useTusUpload.ts b/apps/web/hooks/useTusUpload.ts new file mode 100644 index 0000000..85c3d31 --- /dev/null +++ b/apps/web/hooks/useTusUpload.ts @@ -0,0 +1,122 @@ +import { useState } from "react"; +import * as tus from "tus-js-client"; +import { env } from "../env"; +import { getCompressedImageUrl } from "@nice/utils"; + +interface UploadResult { + compressedUrl: string; + url: string; + fileId: string; + fileName: string; +} + +export function useTusUpload() { + const [uploadProgress, setUploadProgress] = useState< + Record + >({}); + const [isUploading, setIsUploading] = useState(false); + const [uploadError, setUploadError] = useState(null); + + const getFileId = (url: string) => { + const parts = url.split("/"); + const uploadIndex = parts.findIndex((part) => part === "upload"); + if (uploadIndex === -1 || uploadIndex + 4 >= parts.length) { + throw new Error("Invalid upload URL format"); + } + return parts.slice(uploadIndex + 1, uploadIndex + 5).join("/"); + }; + const getResourceUrl = (url: string) => { + const parts = url.split("/"); + const uploadIndex = parts.findIndex((part) => part === "upload"); + if (uploadIndex === -1 || uploadIndex + 4 >= parts.length) { + throw new Error("Invalid upload URL format"); + } + const resUrl = `http://${env.SERVER_IP}:${env.FILE_PORT}/uploads/${parts.slice(uploadIndex + 1, uploadIndex + 6).join("/")}`; + return resUrl; + }; + const handleFileUpload = async ( + file: File | Blob, + onSuccess: (result: UploadResult) => void, + onError: (error: Error) => void, + fileKey: string // 添加文件唯一标识 + ) => { + // console.log() + setIsUploading(true); + setUploadProgress((prev) => ({ ...prev, [fileKey]: 0 })); + setUploadError(null); + + try { + // 如果是Blob,需要转换为File + let fileName = "uploaded-file"; + if (file instanceof Blob && !(file instanceof File)) { + // 根据MIME类型设置文件扩展名 + const extension = file.type.split('/')[1]; + fileName = `uploaded-file.${extension}`; + } + const uploadFile = file instanceof Blob && !(file instanceof File) + ? new File([file], fileName, { type: file.type }) + : file as File; + console.log(`http://${env.SERVER_IP}:${env.SERVER_PORT}/upload`); + const upload = new tus.Upload(uploadFile, { + endpoint: `http://${env.SERVER_IP}:${env.SERVER_PORT}/upload`, + retryDelays: [0, 1000, 3000, 5000], + metadata: { + filename: uploadFile.name, + filetype: uploadFile.type, + size: uploadFile.size as any, + }, + onProgress: (bytesUploaded, bytesTotal) => { + const progress = Number( + ((bytesUploaded / bytesTotal) * 100).toFixed(2) + ); + setUploadProgress((prev) => ({ + ...prev, + [fileKey]: progress, + })); + }, + onSuccess: async (payload) => { + if (upload.url) { + const fileId = getFileId(upload.url); + //console.log(fileId) + const url = getResourceUrl(upload.url); + setIsUploading(false); + setUploadProgress((prev) => ({ + ...prev, + [fileKey]: 100, + })); + onSuccess({ + compressedUrl: getCompressedImageUrl(url), + url, + fileId, + fileName: uploadFile.name, + }); + } + }, + onError: (error) => { + const err = + error instanceof Error + ? error + : new Error("Unknown error"); + setIsUploading(false); + setUploadError(error.message); + console.log(error); + onError(err); + }, + }); + upload.start(); + } catch (error) { + const err = + error instanceof Error ? error : new Error("Upload failed"); + setIsUploading(false); + setUploadError(err.message); + onError(err); + } + }; + + return { + uploadProgress, + isUploading, + uploadError, + handleFileUpload, + }; +} diff --git a/apps/web/package.json b/apps/web/package.json index cf560aa..0cbeb59 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,40 +1,40 @@ { - "name": "web", - "version": "0.0.1", - "type": "module", - "private": true, - "scripts": { - "dev": "next dev --turbopack", - "build": "next build", - "start": "next start", - "lint": "next lint" - }, - "dependencies": { - "@repo/client": "workspace:*", - "@repo/db": "workspace:*", - "@repo/ui": "workspace:*", - "@tanstack/react-query": "^5.51.21", - "@trpc/client": "11.1.2", - "@trpc/react-query": "11.1.2", - "@trpc/server": "11.1.2", - "@trpc/tanstack-react-query": "11.1.2", - "axios": "^1.7.2", - "dayjs": "^1.11.12", - "lucide-react": "0.511.0", - "next": "15.3.2", - "next-themes": "^0.4.6", - "react": "^19.1.0", - "react-dom": "^19.1.0", - "superjson": "^2.2.2" - }, - "devDependencies": { - "@repo/eslint-config": "workspace:*", - "@repo/typescript-config": "workspace:*", - "@tailwindcss/postcss": "^4", - - "@types/react": "^19.1.4", - "@types/react-dom": "^19.1.5", - "tailwindcss": "^4", - "typescript": "^5" - } + "name": "web", + "version": "0.0.1", + "type": "module", + "private": true, + "scripts": { + "dev": "next dev --turbopack -p 3001", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "@repo/client": "workspace:*", + "@repo/db": "workspace:*", + "@repo/ui": "workspace:*", + "@tanstack/react-query": "^5.51.21", + "@trpc/client": "11.1.2", + "@trpc/react-query": "11.1.2", + "@trpc/server": "11.1.2", + "@trpc/tanstack-react-query": "11.1.2", + "axios": "^1.7.2", + "dayjs": "^1.11.12", + "lucide-react": "0.511.0", + "next": "15.3.2", + "next-themes": "^0.4.6", + "react": "^19.1.0", + "tus-js-client": "^4.1.0", + "react-dom": "^19.1.0", + "superjson": "^2.2.2" + }, + "devDependencies": { + "@repo/eslint-config": "workspace:*", + "@repo/typescript-config": "workspace:*", + "@tailwindcss/postcss": "^4", + "@types/react": "^19.1.4", + "@types/react-dom": "^19.1.5", + "tailwindcss": "^4", + "typescript": "^5" + } } diff --git a/packages/client/src/websocket/client.ts b/packages/client/src/websocket/client.ts index 01d29b1..b206ec9 100644 --- a/packages/client/src/websocket/client.ts +++ b/packages/client/src/websocket/client.ts @@ -37,11 +37,11 @@ export class WebSocketClient { try { const message = JSON.parse(event.data) as WSMessage; - // 只处理系统消息、错误消息,或者当前房间的消息 + // 处理所有系统消息、错误消息,以及当前房间的所有消息 if ( message.action === 'system' || message.action === 'error' || - (message.roomId && message.roomId === this.currentRoom) + (message.action === 'message' && message.roomId === this.currentRoom) ) { console.log('收到消息:', message); // 触发消息处理器 diff --git a/packages/client/tsconfig.json b/packages/client/tsconfig.json index 91b3139..4bbd830 100644 --- a/packages/client/tsconfig.json +++ b/packages/client/tsconfig.json @@ -20,6 +20,6 @@ "@repo/backend/*": ["../../apps/backend/src/*"] } }, - "include": ["src"], + "include": ["src", "../../apps/web/hooks/useTusUpload.ts"], "exclude": ["node_modules", "dist"] } diff --git a/packages/db/prisma/migrations/20250527111157_init/migration.sql b/packages/db/prisma/migrations/20250527111157_init/migration.sql new file mode 100644 index 0000000..1e4adf9 --- /dev/null +++ b/packages/db/prisma/migrations/20250527111157_init/migration.sql @@ -0,0 +1,28 @@ +-- CreateTable +CREATE TABLE "resource" ( + "id" TEXT NOT NULL, + "title" TEXT, + "description" TEXT, + "type" TEXT, + "fileId" TEXT, + "url" TEXT, + "meta" JSONB, + "status" TEXT, + "created_at" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3), + "created_by" TEXT, + "updated_by" TEXT, + "deleted_at" TIMESTAMP(3), + "is_public" BOOLEAN DEFAULT true, + + CONSTRAINT "resource_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "resource_fileId_key" ON "resource"("fileId"); + +-- CreateIndex +CREATE INDEX "resource_type_idx" ON "resource"("type"); + +-- CreateIndex +CREATE INDEX "resource_created_at_idx" ON "resource"("created_at"); diff --git a/packages/db/prisma/migrations/20250527115119_init/migration.sql b/packages/db/prisma/migrations/20250527115119_init/migration.sql new file mode 100644 index 0000000..2a11431 --- /dev/null +++ b/packages/db/prisma/migrations/20250527115119_init/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "resource" ADD COLUMN "storage_type" TEXT; diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index 6c5b6c2..4f925dd 100755 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -112,3 +112,25 @@ model OidcClient { @@map("oidc_clients") } +model Resource { + id String @id @default(cuid()) @map("id") + title String? @map("title") + description String? @map("description") + type String? @map("type") + fileId String? @unique + url String? + meta Json? @map("meta") + status String? + createdAt DateTime? @default(now()) @map("created_at") + updatedAt DateTime? @updatedAt @map("updated_at") + createdBy String? @map("created_by") + updatedBy String? @map("updated_by") + deletedAt DateTime? @map("deleted_at") + isPublic Boolean? @default(true) @map("is_public") + storageType String? @map("storage_type") + + // 索引 + @@index([type]) + @@index([createdAt]) + @@map("resource") +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index da40f57..90a6a98 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,12 +41,21 @@ importers: '@repo/db': specifier: workspace:* version: link:../../packages/db + '@repo/tus': + specifier: workspace:* + version: link:../../packages/tus '@trpc/server': specifier: 11.1.2 version: 11.1.2(typescript@5.8.3) '@types/oidc-provider': specifier: ^9.1.0 version: 9.1.0 + dayjs: + specifier: ^1.11.13 + version: 1.11.13 + dotenv: + specifier: ^16.4.7 + version: 16.5.0 hono: specifier: ^4.7.10 version: 4.7.10 @@ -62,6 +71,9 @@ importers: nanoid: specifier: ^5.1.5 version: 5.1.5 + nanoid-cjs: + specifier: ^0.0.7 + version: 0.0.7 node-cron: specifier: ^4.0.7 version: 4.0.7 @@ -71,6 +83,9 @@ importers: superjson: specifier: ^2.2.2 version: 2.2.2 + transliteration: + specifier: ^2.3.5 + version: 2.3.5 zod: specifier: ^3.25.23 version: 3.25.23 @@ -132,6 +147,9 @@ importers: superjson: specifier: ^2.2.2 version: 2.2.2 + tus-js-client: + specifier: ^4.1.0 + version: 4.3.1 devDependencies: '@repo/eslint-config': specifier: workspace:* @@ -2670,6 +2688,9 @@ packages: buffer-crc32@0.2.13: resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} @@ -2826,6 +2847,9 @@ packages: resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} engines: {node: '>=12.5.0'} + combine-errors@3.0.3: + resolution: {integrity: sha512-C8ikRNRMygCwaTx+Ek3Yr+OuZzgZjduCOfSQBjbM8V3MfgcjSTeto/GXP6PAwKvJz/v15b7GHZvx5rOlczFw/Q==} + combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} @@ -2933,6 +2957,9 @@ packages: csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + custom-error-instance@2.1.1: + resolution: {integrity: sha512-p6JFxJc3M4OTD2li2qaHkDCw9SfMw82Ldr6OC9Je1aXiGfhx2W8p3GaoeaGrPJTUN9NirTM/KTxHWMUdR1rsUg==} + data-uri-to-buffer@6.0.2: resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==} engines: {node: '>= 14'} @@ -3075,6 +3102,10 @@ packages: resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} engines: {node: '>=12'} + dotenv@16.5.0: + resolution: {integrity: sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==} + engines: {node: '>=12'} + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -3849,6 +3880,9 @@ packages: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} + js-base64@3.7.7: + resolution: {integrity: sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -3995,6 +4029,24 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash._baseiteratee@4.7.0: + resolution: {integrity: sha512-nqB9M+wITz0BX/Q2xg6fQ8mLkyfF7MU7eE+MNBNjTHFKeKaZAPEzEg+E8LWxKWf1DQVflNEn9N49yAuqKh2mWQ==} + + lodash._basetostring@4.12.0: + resolution: {integrity: sha512-SwcRIbyxnN6CFEEK4K1y+zuApvWdpQdBHM/swxP962s8HIxPO3alBH5t3m/dl+f4CMUug6sJb7Pww8d13/9WSw==} + + lodash._baseuniq@4.6.0: + resolution: {integrity: sha512-Ja1YevpHZctlI5beLA7oc5KNDhGcPixFhcqSiORHNsp/1QTv7amAXzw+gu4YOvErqVlMVyIJGgtzeepCnnur0A==} + + lodash._createset@4.0.3: + resolution: {integrity: sha512-GTkC6YMprrJZCYU3zcqZj+jkXkrXzq3IPBcF/fIPpNEAB4hZEtXU8zp/RwKOvZl43NUmwDbyRk3+ZTbeRdEBXA==} + + lodash._root@3.0.1: + resolution: {integrity: sha512-O0pWuFSK6x4EXhM1dhZ8gchNtG7JMqBtrHdoUFUWXD7dJnNSUze1GuyQr5sOs0aCvgGeI3o/OJW8f4ca7FDxmQ==} + + lodash._stringtopath@4.8.0: + resolution: {integrity: sha512-SXL66C731p0xPDC5LZg4wI5H+dJo/EO4KTqOMwLYCH3+FmmfAKJEZCm6ohGpI+T1xwsDsJCfL4OnhorllvlTPQ==} + lodash.camelcase@4.3.0: resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} @@ -4017,6 +4069,9 @@ packages: lodash.throttle@4.1.1: resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==} + lodash.uniqby@4.5.0: + resolution: {integrity: sha512-IRt7cfTtHy6f1aRVA5n7kT8rgN3N1nH6MOWLcHfpWG2SH19E3JksLK38MktLxZDhlAjCP9jpIXkOnRXlu6oByQ==} + lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} @@ -4161,6 +4216,9 @@ packages: mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + nanoid-cjs@0.0.7: + resolution: {integrity: sha512-z72crZ0JcTb5s40Pm9Vk99qfEw9Oe1qyVjK/kpelCKyZDH8YTX4HejSfp54PMJT8F5rmsiBpG6wfVAGAhLEFhA==} + nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -4484,6 +4542,9 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + proper-lockfile@4.1.2: + resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} + proxy-agent@6.5.0: resolution: {integrity: sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==} engines: {node: '>= 14'} @@ -4499,6 +4560,9 @@ packages: resolution: {integrity: sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==} engines: {node: '>=6'} + querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -4595,6 +4659,9 @@ packages: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -4619,6 +4686,10 @@ packages: resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} engines: {node: '>=8'} + retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -5010,6 +5081,11 @@ packages: tr46@1.0.1: resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} + transliteration@2.3.5: + resolution: {integrity: sha512-HAGI4Lq4Q9dZ3Utu2phaWgtm3vB6PkLUFqWAScg/UW+1eZ/Tg6Exo4oC0/3VUol/w4BlefLhUUSVBr/9/ZGQOw==} + engines: {node: '>=6.0.0'} + hasBin: true + tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true @@ -5122,6 +5198,10 @@ packages: resolution: {integrity: sha512-iHuaNcq5GZZnr3XDZNuu2LSyCzAOPwDuo5Qt+q64DfsTP1i3T2bKfxJhni2ZQxsvAoxRbuUK5QetJki4qc5aYA==} hasBin: true + tus-js-client@4.3.1: + resolution: {integrity: sha512-ZLeYmjrkaU1fUsKbIi8JML52uAocjEZtBx4DKjRrqzrZa0O4MYwT6db+oqePlspV+FxXJAyFBc/L5gwUi2OFsg==} + engines: {node: '>=18'} + tw-animate-css@1.3.0: resolution: {integrity: sha512-jrJ0XenzS9KVuDThJDvnhalbl4IYiMQ/XvpA0a2FL8KmlK+6CSMviO7ROY/I7z1NnUs5NnDhlM6fXmF40xPxzw==} @@ -5229,6 +5309,9 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + use-callback-ref@1.3.3: resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} engines: {node: '>=10'} @@ -7861,6 +7944,8 @@ snapshots: buffer-crc32@0.2.13: {} + buffer-from@1.1.2: {} + buffer@5.7.1: dependencies: base64-js: 1.5.1 @@ -8036,6 +8121,11 @@ snapshots: color-string: 1.9.1 optional: true + combine-errors@3.0.3: + dependencies: + custom-error-instance: 2.1.1 + lodash.uniqby: 4.5.0 + combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 @@ -8145,6 +8235,8 @@ snapshots: csstype@3.1.3: {} + custom-error-instance@2.1.1: {} + data-uri-to-buffer@6.0.2: {} data-view-buffer@1.0.2: @@ -8279,6 +8371,8 @@ snapshots: dotenv@16.4.5: {} + dotenv@16.5.0: {} + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -9289,6 +9383,8 @@ snapshots: joycon@3.1.1: {} + js-base64@3.7.7: {} + js-tokens@4.0.0: {} js-yaml@4.1.0: @@ -9424,6 +9520,25 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash._baseiteratee@4.7.0: + dependencies: + lodash._stringtopath: 4.8.0 + + lodash._basetostring@4.12.0: {} + + lodash._baseuniq@4.6.0: + dependencies: + lodash._createset: 4.0.3 + lodash._root: 3.0.1 + + lodash._createset@4.0.3: {} + + lodash._root@3.0.1: {} + + lodash._stringtopath@4.8.0: + dependencies: + lodash._basetostring: 4.12.0 + lodash.camelcase@4.3.0: {} lodash.defaults@4.2.0: {} @@ -9438,6 +9553,11 @@ snapshots: lodash.throttle@4.1.1: {} + lodash.uniqby@4.5.0: + dependencies: + lodash._baseiteratee: 4.7.0 + lodash._baseuniq: 4.6.0 + lodash@4.17.21: {} log-symbols@3.0.0: @@ -9579,6 +9699,10 @@ snapshots: object-assign: 4.1.1 thenify-all: 1.6.0 + nanoid-cjs@0.0.7: + dependencies: + nanoid: 5.1.5 + nanoid@3.3.11: {} nanoid@5.1.5: {} @@ -9924,6 +10048,12 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 + proper-lockfile@4.1.2: + dependencies: + graceful-fs: 4.2.11 + retry: 0.12.0 + signal-exit: 3.0.7 + proxy-agent@6.5.0: dependencies: agent-base: 7.1.3 @@ -9948,6 +10078,8 @@ snapshots: split-on-first: 1.1.0 strict-uri-encode: 2.0.0 + querystringify@2.2.0: {} + queue-microtask@1.2.3: {} quick-lru@7.0.1: {} @@ -10051,6 +10183,8 @@ snapshots: require-directory@2.1.1: {} + requires-port@1.0.0: {} + resolve-from@4.0.0: {} resolve-from@5.0.0: {} @@ -10074,6 +10208,8 @@ snapshots: onetime: 5.1.2 signal-exit: 3.0.7 + retry@0.12.0: {} + reusify@1.1.0: {} rimraf@3.0.2: @@ -10544,6 +10680,10 @@ snapshots: dependencies: punycode: 2.3.1 + transliteration@2.3.5: + dependencies: + yargs: 17.7.2 + tree-kill@1.2.2: {} ts-api-utils@2.1.0(typescript@5.8.3): @@ -10665,6 +10805,16 @@ snapshots: turbo-windows-64: 2.5.3 turbo-windows-arm64: 2.5.3 + tus-js-client@4.3.1: + dependencies: + buffer-from: 1.1.2 + combine-errors: 3.0.3 + is-stream: 2.0.1 + js-base64: 3.7.7 + lodash.throttle: 4.1.1 + proper-lockfile: 4.1.2 + url-parse: 1.5.10 + tw-animate-css@1.3.0: {} type-check@0.4.0: @@ -10775,6 +10925,11 @@ snapshots: dependencies: punycode: 2.3.1 + url-parse@1.5.10: + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + use-callback-ref@1.3.3(@types/react@19.1.5)(react@19.1.0): dependencies: react: 19.1.0 diff --git a/turbo.json b/turbo.json index bed41db..10717c6 100644 --- a/turbo.json +++ b/turbo.json @@ -1,67 +1,48 @@ { - "$schema": "https://turbo.build/schema.json", - "globalDependencies": [ - "**/.env.*local" - ], - "tasks": { - "dev": { - "dependsOn": ["^db:generate"], - "cache": false, - "persistent": true - }, - "build": { - "dependsOn": ["^build", "^db:generate"], - "inputs": [ - "$TURBO_DEFAULT$", - ".env*" - ], - "outputs": [ - "dist/**", - ".next/**", - "!.next/cache/**" - ] - }, - "lint": { - "dependsOn": [ - "^lint" - ] - }, - "check-types": { - "dependsOn": [ - "^check-types" - ] - }, - "db:generate": { - "cache": false - }, - "db:migrate": { - "cache": false, - "persistent": true - }, - "db:deploy": { - "cache": false - }, - "db:push": { - "cache": false - }, - "db:seed": { - "cache": false - }, - "generate": { - "dependsOn": [ - "^generate" - ], - "cache": false - }, - "test": { - "outputs": [ - "coverage/**" - ] - }, - "test:e2e": { - "outputs": [ - "coverage-e2e/**" - ] - } - } -} \ No newline at end of file + "$schema": "https://turbo.build/schema.json", + "globalDependencies": ["**/.env.*local"], + "tasks": { + "dev": { + "dependsOn": ["^db:generate"], + "cache": false, + "persistent": true + }, + "build": { + "dependsOn": ["^build", "^db:generate"], + "inputs": ["$TURBO_DEFAULT$", ".env*"], + "outputs": ["dist/**", ".next/**", "!.next/cache/**"] + }, + "lint": { + "dependsOn": ["^lint"] + }, + "check-types": { + "dependsOn": ["^check-types"] + }, + "db:generate": { + "cache": false + }, + "db:migrate": { + "cache": false, + "persistent": true + }, + "db:deploy": { + "cache": false + }, + "db:push": { + "cache": false + }, + "db:seed": { + "cache": false + }, + "generate": { + "dependsOn": ["^generate"], + "cache": false + }, + "test": { + "outputs": ["coverage/**"] + }, + "test:e2e": { + "outputs": ["coverage-e2e/**"] + } + } +}