diff --git a/packages/sdk-socket-server/.env.sample b/packages/sdk-socket-server/.env.sample index bd39df4b5..2aecb46e5 100644 --- a/packages/sdk-socket-server/.env.sample +++ b/packages/sdk-socket-server/.env.sample @@ -1,2 +1,3 @@ SEGMENT_API_KEY_PRODUCTION=123456789 SEGMENT_API_KEY_DEBUG=123456789 +REDIS_URL=redis://redis:6379/0 diff --git a/packages/sdk-socket-server/Dockerfile b/packages/sdk-socket-server/Dockerfile index 2561b64de..ac2aadac4 100644 --- a/packages/sdk-socket-server/Dockerfile +++ b/packages/sdk-socket-server/Dockerfile @@ -1,7 +1,12 @@ # Build stage FROM node:18-alpine AS builder -# Install build dependencies and build the project +# Accept NODE_ENV as a build argument +ARG NODE_ENV=production + +# Set the NODE_ENV environment variable +ENV NODE_ENV=${NODE_ENV} + WORKDIR /app COPY package.json ./ RUN yarn install @@ -11,17 +16,14 @@ RUN yarn build # Runtime stage FROM node:18-alpine -# Install runtime dependencies WORKDIR /app COPY --from=builder /app/package.json ./ -RUN yarn install --production -# Copy built project and .env file from the build stage +# If NODE_ENV is development, install all dependencies, else only production dependencies +RUN if [ "$NODE_ENV" = "development" ]; then yarn install; else yarn install --production; fi + COPY --from=builder /app/dist ./dist COPY .env ./ -# Expose the server port EXPOSE 4000 - -# Start the server CMD ["node", "dist/index.js"] diff --git a/packages/sdk-socket-server/README.md b/packages/sdk-socket-server/README.md index 79674555b..01547e2d2 100644 --- a/packages/sdk-socket-server/README.md +++ b/packages/sdk-socket-server/README.md @@ -1,12 +1,12 @@ # Debug SDK Socket Server Locally -This guide provides instructions for setting up and debugging the SDK socket server locally, as well as using Docker and Ngrok for broader testing, including integration with MetaMask Mobile app. +This guide provides instructions for setting up and debugging the SDK socket server locally, as well as using Docker Compose for broader testing, including integration with MetaMask Mobile app. ## Prerequisites - Node.js and Yarn installed -- Docker installed (for Docker-based setup) -- Ngrok account and CLI tool installed +- Docker and Docker Compose installed (for Docker-based setup) +- Ngrok account and CLI tool installed (for external access testing) ## Local Setup @@ -37,25 +37,30 @@ To expose your local server to the internet, particularly for testing with mobil 3. **Configure Your DApp**: - Set the `communicationServerUrl` in your DApp's SDK options to your local IP or `localhost` with port 4000. For example: `communicationServerUrl: "http://{yourLocalIP | localhost}:4000"` -## Debugging with Docker and Ngrok +## Debugging with Docker Compose -For a more isolated environment, you can debug the SDK socket server using Docker and Ngrok. +You can use Docker Compose to run the SDK socket server in either a development or production environment. -### Docker Setup +### Running in Development Mode -1. **Build the Docker Image**: +1. **Start in Development Mode**: + - Use the command: `yarn start:docker:dev` + - This command sets up the environment for development and starts the server along with any other necessary services, like Redis. - - Build the image for socket.io: `docker build -t socket-test .` +### Running in Production Mode -2. **Run the Docker Container**: - - Start the container: `docker run -dp 4000:4000 socket-test` +1. **Start in Production Mode**: + - Use the command: `yarn start:docker` + - This command sets up the environment for production. It's optimized for performance and stability. ### Ngrok Configuration -Follow the same Ngrok setup as mentioned in the Local Setup section above to expose your Docker-based server. +Follow the same Ngrok setup as mentioned in the Local Setup section above to expose your Docker Compose-based server. ## Additional Notes -- **Logs and Monitoring**: Monitor the logs for any error messages or warnings during startup or operation. This can provide valuable insights into the behavior of the server. -- **Security Considerations**: When exposing your local server using Ngrok, be aware that it is accessible publicly. Ensure that you do not expose sensitive data or endpoints. -- **Troubleshooting**: If you encounter issues, verify your environment settings, check for common networking issues, and ensure that all required services are running. +- **Environment-Specific Configuration**: The development mode includes additional debugging tools and settings, while the production mode is streamlined for performance. +- **Redis Setup**: Ensure that Redis is properly configured and running when using Docker Compose. +- **Logs and Monitoring**: Monitor the logs for any error messages or warnings during startup or operation of the server. +- **Security Considerations**: When using Ngrok, be aware that your server is publicly accessible. Ensure that you do not expose sensitive data or endpoints. +- **Troubleshooting**: If you encounter issues, verify your Docker Compose and Ngrok configurations. Check for network connectivity issues and ensure that all containers are running as expected. diff --git a/packages/sdk-socket-server/api-config.ts b/packages/sdk-socket-server/api-config.ts index 215c69e21..bb1e3aa02 100644 --- a/packages/sdk-socket-server/api-config.ts +++ b/packages/sdk-socket-server/api-config.ts @@ -4,16 +4,15 @@ import express from 'express'; import bodyParser from 'body-parser'; import cors from 'cors'; import helmet from 'helmet'; -import { LRUCache } from 'lru-cache'; +import Redis from 'ioredis'; import Analytics from 'analytics-node'; +import { isDevelopment, isDevelopmentServer } from '.'; -const isDevelopment: boolean = process.env.NODE_ENV === 'development'; -const isDevelopmentServer: boolean = process.env.ENVIRONMENT === 'development'; - -const userIdHashCache = new LRUCache({ - max: 5000, - ttl: 1000 * 60 * 60 * 24, -}); +// Initialize Redis client +// Provide a default URL if REDIS_URL is not set +const redisUrl = process.env.REDIS_URL || 'redis://localhost:6379'; +export const redis = new Redis(redisUrl); +const THIRTY_DAYS_IN_SECONDS = 30 * 24 * 60 * 60; // expiration time of entries in Redis const app = express(); @@ -24,6 +23,15 @@ app.options('*', cors()); app.use(helmet()); app.disable('x-powered-by'); +async function inspectRedis(key?: string) { + if (key) { + const value = await redis.get(key); + console.log(`DEBUG> Redis Update - Key: ${key}, Value: ${value}`); + return; + } + return; +} + const analytics = new Analytics( isDevelopment || isDevelopmentServer ? process.env.SEGMENT_API_KEY_DEBUG || '' @@ -40,7 +48,7 @@ app.get('/', (_req, res) => { res.json({ success: true }); }); -app.post('/debug', (_req, res) => { +app.post('/debug', async (_req, res) => { try { const { body } = _req; @@ -53,30 +61,72 @@ app.post('/debug', (_req, res) => { } const id: string = body.id || 'socket.io-server'; - let userIdHash: string | undefined = userIdHashCache.get(id); + let userIdHash = await redis.get(id); if (!userIdHash) { userIdHash = crypto.createHash('sha1').update(id).digest('hex'); - userIdHashCache.set(id, userIdHash); + await redis.set(id, userIdHash, 'EX', THIRTY_DAYS_IN_SECONDS); } - const event: { - userId: string; - event: string; - properties: { - userId: string; - [key: string]: string | undefined; // This line adds an index signature + if (isDevelopment) { + inspectRedis(id); + } + + let userInfo; + const cachedUserInfo = await redis.get(userIdHash); + + if (cachedUserInfo) { + userInfo = JSON.parse(cachedUserInfo); + } else { + // Initial userInfo setup + userInfo = { + url: '', + title: '', + platform: '', + source: '', + sdkVersion: '', }; - } = { + } + + // If 'sdk_connect_request_started', update userInfo in Redis + if (body.event === 'sdk_connect_request_started') { + userInfo = { + url: body.url || '', + title: body.title || '', + platform: body.platform || '', + source: body.source || '', + sdkVersion: body.sdkVersion || '', + }; + await redis.set( + userIdHash, + JSON.stringify(userInfo), + 'EX', + THIRTY_DAYS_IN_SECONDS, + ); + } + + if (isDevelopment) { + inspectRedis(userIdHash); + } + + const event = { userId: userIdHash, event: body.event, properties: { userId: userIdHash, + ...body.properties, + // Apply stored user info if available + url: userInfo.url || body.originationInfo?.url, + title: userInfo.title || body.originationInfo?.title, + platform: userInfo.platform || body.originationInfo?.platform, + sdkVersion: + userInfo.sdkVersion || body.originationInfo?.sdkVersion || '', + source: userInfo.source || body.originationInfo?.source || '', }, }; // Define properties to be excluded - const propertiesToExclude: string[] = ['icon']; + const propertiesToExclude: string[] = ['icon', 'originationInfo', 'id']; for (const property in body) { if ( diff --git a/packages/sdk-socket-server/docker-compose.yml b/packages/sdk-socket-server/docker-compose.yml new file mode 100644 index 000000000..aa427f36d --- /dev/null +++ b/packages/sdk-socket-server/docker-compose.yml @@ -0,0 +1,21 @@ +version: '3.8' + +services: + app: + build: + context: . + dockerfile: Dockerfile + args: + - NODE_ENV=${NODE_ENV:-production} + ports: + - '4000:4000' + environment: + - REDIS_URL=redis://cache:6379 + depends_on: + - cache + + cache: + image: redis:latest + command: redis-server --maxmemory 100mb --maxmemory-policy volatile-lru + ports: + - '6379:6379' diff --git a/packages/sdk-socket-server/index.ts b/packages/sdk-socket-server/index.ts index 6cd4b556c..70ecec229 100644 --- a/packages/sdk-socket-server/index.ts +++ b/packages/sdk-socket-server/index.ts @@ -6,7 +6,9 @@ import { app, analytics } from './api-config'; import configureSocketIO from './socket-config'; import { cleanupAndExit } from './utils'; -const isDevelopment: boolean = process.env.NODE_ENV === 'development'; +export const isDevelopment: boolean = process.env.NODE_ENV === 'development'; +export const isDevelopmentServer: boolean = + process.env.ENVIRONMENT === 'development'; const server = http.createServer(app); configureSocketIO(server); // configure socket.io server diff --git a/packages/sdk-socket-server/package.json b/packages/sdk-socket-server/package.json index 87daa7619..54017e047 100644 --- a/packages/sdk-socket-server/package.json +++ b/packages/sdk-socket-server/package.json @@ -26,7 +26,8 @@ "lint:misc": "prettier '**/*.json' '**/*.md' '!CHANGELOG.md' --ignore-path ../../.gitignore", "reset": "yarn clean && rimraf ./node_modules/", "start": "yarn build && node dist/index.js", - "start:docker": "docker build -t socket-test . && docker run -p 4000:4000 -d socket-test", + "start:docker": "docker-compose up --build", + "start:docker:dev": "NODE_ENV=development docker-compose up --build", "test": "jest", "test:ci": "jest --coverage --passWithNoTests" }, @@ -37,11 +38,12 @@ "dotenv": "^16.0.3", "express": "^4.18.2", "helmet": "^5.1.1", + "ioredis": "^5.3.2", "lru-cache": "^10.0.0", "rate-limiter-flexible": "^2.3.8", "rimraf": "^4.4.0", "socket.io": "^4.4.1", - "uuid": "^8.3.2" + "uuid": "^9.0.1" }, "devDependencies": { "@lavamoat/allow-scripts": "^2.3.1", diff --git a/packages/sdk-socket-server/rate-limiter.ts b/packages/sdk-socket-server/rate-limiter.ts index 672da60d5..e0e192f27 100644 --- a/packages/sdk-socket-server/rate-limiter.ts +++ b/packages/sdk-socket-server/rate-limiter.ts @@ -1,5 +1,6 @@ import os from 'os'; import { RateLimiterMemory } from 'rate-limiter-flexible'; +import { isDevelopment } from '.'; let rateLimitPoints = 10; let rateLimitMessagePoints = 100; @@ -37,9 +38,11 @@ const resetRateLimits = (): void => { rateLimitMessagePoints = initialRateLimitMessagePoints; } - console.log( - `INFO> RL points: ${rateLimitPoints} - RL message points: ${rateLimitMessagePoints}`, - ); + if (isDevelopment) { + console.log( + `DEBUG> RL points: ${rateLimitPoints} - RL message points: ${rateLimitMessagePoints}`, + ); + } }; const increaseRateLimits = (cpuUsagePercentMin: number): void => { @@ -86,9 +89,11 @@ const increaseRateLimits = (cpuUsagePercentMin: number): void => { duration: 1, }); - console.log( - `INFO> RL points: ${rateLimitPoints} - RL message points: ${rateLimitMessagePoints}`, - ); + if (isDevelopment) { + console.log( + `DEBUG> RL points: ${rateLimitPoints} - RL message points: ${rateLimitMessagePoints}`, + ); + } }; export { diff --git a/packages/sdk-socket-server/yarn.lock b/packages/sdk-socket-server/yarn.lock index ca4c44440..a1d8b18db 100644 --- a/packages/sdk-socket-server/yarn.lock +++ b/packages/sdk-socket-server/yarn.lock @@ -504,6 +504,13 @@ __metadata: languageName: node linkType: hard +"@ioredis/commands@npm:^1.1.1": + version: 1.2.0 + resolution: "@ioredis/commands@npm:1.2.0" + checksum: 9b20225ba36ef3e5caf69b3c0720597c3016cc9b1e157f519ea388f621dd9037177f84cfe7e25c4c32dad7dd90c70ff9123cd411f747e053cf292193c9c461e2 + languageName: node + linkType: hard + "@isaacs/cliui@npm:^8.0.2": version: 8.0.2 resolution: "@isaacs/cliui@npm:8.0.2" @@ -892,6 +899,7 @@ __metadata: eslint-plugin-prettier: ^3.4.0 express: ^4.18.2 helmet: ^5.1.1 + ioredis: ^5.3.2 jest: ^29.6.4 lru-cache: ^10.0.0 nodemon: ^2.0.20 @@ -905,7 +913,7 @@ __metadata: ts-node: ^10.9.1 ts-node-dev: ^2.0.0 typescript: ^5.2.2 - uuid: ^8.3.2 + uuid: ^9.0.1 languageName: unknown linkType: soft @@ -2396,6 +2404,13 @@ __metadata: languageName: node linkType: hard +"cluster-key-slot@npm:^1.1.0": + version: 1.1.2 + resolution: "cluster-key-slot@npm:1.1.2" + checksum: be0ad2d262502adc998597e83f9ded1b80f827f0452127c5a37b22dfca36bab8edf393f7b25bb626006fb9fb2436106939ede6d2d6ecf4229b96a47f27edd681 + languageName: node + linkType: hard + "cmd-shim@npm:^6.0.0": version: 6.0.2 resolution: "cmd-shim@npm:6.0.2" @@ -2697,6 +2712,13 @@ __metadata: languageName: node linkType: hard +"denque@npm:^2.1.0": + version: 2.1.0 + resolution: "denque@npm:2.1.0" + checksum: 1d4ae1d05e59ac3a3481e7b478293f4b4c813819342273f3d5b826c7ffa9753c520919ba264f377e09108d24ec6cf0ec0ac729a5686cbb8f32d797126c5dae74 + languageName: node + linkType: hard + "depd@npm:2.0.0": version: 2.0.0 resolution: "depd@npm:2.0.0" @@ -4166,6 +4188,23 @@ __metadata: languageName: node linkType: hard +"ioredis@npm:^5.3.2": + version: 5.3.2 + resolution: "ioredis@npm:5.3.2" + dependencies: + "@ioredis/commands": ^1.1.1 + cluster-key-slot: ^1.1.0 + debug: ^4.3.4 + denque: ^2.1.0 + lodash.defaults: ^4.2.0 + lodash.isarguments: ^3.1.0 + redis-errors: ^1.2.0 + redis-parser: ^3.0.0 + standard-as-callback: ^2.1.0 + checksum: 9a23559133e862a768778301efb68ae8c2af3c33562174b54a4c2d6574b976e85c75a4c34857991af733e35c48faf4c356e7daa8fb0a3543d85ff1768c8754bc + languageName: node + linkType: hard + "ip@npm:^2.0.0": version: 2.0.0 resolution: "ip@npm:2.0.0" @@ -5078,6 +5117,20 @@ __metadata: languageName: node linkType: hard +"lodash.defaults@npm:^4.2.0": + version: 4.2.0 + resolution: "lodash.defaults@npm:4.2.0" + checksum: 84923258235592c8886e29de5491946ff8c2ae5c82a7ac5cddd2e3cb697e6fbdfbbb6efcca015795c86eec2bb953a5a2ee4016e3735a3f02720428a40efbb8f1 + languageName: node + linkType: hard + +"lodash.isarguments@npm:^3.1.0": + version: 3.1.0 + resolution: "lodash.isarguments@npm:3.1.0" + checksum: ae1526f3eb5c61c77944b101b1f655f846ecbedcb9e6b073526eba6890dc0f13f09f72e11ffbf6540b602caee319af9ac363d6cdd6be41f4ee453436f04f13b5 + languageName: node + linkType: hard + "lodash.isstring@npm:^4.0.1": version: 4.0.1 resolution: "lodash.isstring@npm:4.0.1" @@ -6115,6 +6168,22 @@ __metadata: languageName: node linkType: hard +"redis-errors@npm:^1.0.0, redis-errors@npm:^1.2.0": + version: 1.2.0 + resolution: "redis-errors@npm:1.2.0" + checksum: f28ac2692113f6f9c222670735aa58aeae413464fd58ccf3fce3f700cae7262606300840c802c64f2b53f19f65993da24dc918afc277e9e33ac1ff09edb394f4 + languageName: node + linkType: hard + +"redis-parser@npm:^3.0.0": + version: 3.0.0 + resolution: "redis-parser@npm:3.0.0" + dependencies: + redis-errors: ^1.0.0 + checksum: 89290ae530332f2ae37577647fa18208d10308a1a6ba750b9d9a093e7398f5e5253f19855b64c98757f7129cccce958e4af2573fdc33bad41405f87f1943459a + languageName: node + linkType: hard + "regexp.prototype.flags@npm:^1.5.1": version: 1.5.1 resolution: "regexp.prototype.flags@npm:1.5.1" @@ -6656,6 +6725,13 @@ __metadata: languageName: node linkType: hard +"standard-as-callback@npm:^2.1.0": + version: 2.1.0 + resolution: "standard-as-callback@npm:2.1.0" + checksum: 88bec83ee220687c72d94fd86a98d5272c91d37ec64b66d830dbc0d79b62bfa6e47f53b71646011835fc9ce7fae62739545d13124262b53be4fbb3e2ebad551c + languageName: node + linkType: hard + "statuses@npm:2.0.1": version: 2.0.1 resolution: "statuses@npm:2.0.1" @@ -7318,6 +7394,15 @@ __metadata: languageName: node linkType: hard +"uuid@npm:^9.0.1": + version: 9.0.1 + resolution: "uuid@npm:9.0.1" + bin: + uuid: dist/bin/uuid + checksum: 39931f6da74e307f51c0fb463dc2462807531dc80760a9bff1e35af4316131b4fc3203d16da60ae33f07fdca5b56f3f1dd662da0c99fea9aaeab2004780cc5f4 + languageName: node + linkType: hard + "v8-compile-cache-lib@npm:^3.0.1": version: 3.0.1 resolution: "v8-compile-cache-lib@npm:3.0.1" diff --git a/yarn.lock b/yarn.lock index 2ce5f4536..f0dcbe286 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5933,6 +5933,13 @@ __metadata: languageName: node linkType: hard +"@ioredis/commands@npm:^1.1.1": + version: 1.2.0 + resolution: "@ioredis/commands@npm:1.2.0" + checksum: 9b20225ba36ef3e5caf69b3c0720597c3016cc9b1e157f519ea388f621dd9037177f84cfe7e25c4c32dad7dd90c70ff9123cd411f747e053cf292193c9c461e2 + languageName: node + linkType: hard + "@isaacs/cliui@npm:^8.0.2": version: 8.0.2 resolution: "@isaacs/cliui@npm:8.0.2" @@ -7823,6 +7830,7 @@ __metadata: eslint-plugin-prettier: ^3.4.0 express: ^4.18.2 helmet: ^5.1.1 + ioredis: ^5.3.2 jest: ^29.6.4 lru-cache: ^10.0.0 nodemon: ^2.0.20 @@ -7836,7 +7844,7 @@ __metadata: ts-node: ^10.9.1 ts-node-dev: ^2.0.0 typescript: ^5.2.2 - uuid: ^8.3.2 + uuid: ^9.0.1 languageName: unknown linkType: soft @@ -18382,6 +18390,13 @@ __metadata: languageName: node linkType: hard +"cluster-key-slot@npm:^1.1.0": + version: 1.1.2 + resolution: "cluster-key-slot@npm:1.1.2" + checksum: be0ad2d262502adc998597e83f9ded1b80f827f0452127c5a37b22dfca36bab8edf393f7b25bb626006fb9fb2436106939ede6d2d6ecf4229b96a47f27edd681 + languageName: node + linkType: hard + "cmd-shim@npm:^6.0.0": version: 6.0.1 resolution: "cmd-shim@npm:6.0.1" @@ -19873,6 +19888,13 @@ __metadata: languageName: node linkType: hard +"denque@npm:^2.1.0": + version: 2.1.0 + resolution: "denque@npm:2.1.0" + checksum: 1d4ae1d05e59ac3a3481e7b478293f4b4c813819342273f3d5b826c7ffa9753c520919ba264f377e09108d24ec6cf0ec0ac729a5686cbb8f32d797126c5dae74 + languageName: node + linkType: hard + "depd@npm:2.0.0, depd@npm:^2.0.0": version: 2.0.0 resolution: "depd@npm:2.0.0" @@ -25268,6 +25290,23 @@ __metadata: languageName: node linkType: hard +"ioredis@npm:^5.3.2": + version: 5.3.2 + resolution: "ioredis@npm:5.3.2" + dependencies: + "@ioredis/commands": ^1.1.1 + cluster-key-slot: ^1.1.0 + debug: ^4.3.4 + denque: ^2.1.0 + lodash.defaults: ^4.2.0 + lodash.isarguments: ^3.1.0 + redis-errors: ^1.2.0 + redis-parser: ^3.0.0 + standard-as-callback: ^2.1.0 + checksum: 9a23559133e862a768778301efb68ae8c2af3c33562174b54a4c2d6574b976e85c75a4c34857991af733e35c48faf4c356e7daa8fb0a3543d85ff1768c8754bc + languageName: node + linkType: hard + "ip-regex@npm:^2.1.0": version: 2.1.0 resolution: "ip-regex@npm:2.1.0" @@ -29043,6 +29082,13 @@ __metadata: languageName: node linkType: hard +"lodash.isarguments@npm:^3.1.0": + version: 3.1.0 + resolution: "lodash.isarguments@npm:3.1.0" + checksum: ae1526f3eb5c61c77944b101b1f655f846ecbedcb9e6b073526eba6890dc0f13f09f72e11ffbf6540b602caee319af9ac363d6cdd6be41f4ee453436f04f13b5 + languageName: node + linkType: hard + "lodash.isequal@npm:4.5.0, lodash.isequal@npm:^4.5.0": version: 4.5.0 resolution: "lodash.isequal@npm:4.5.0" @@ -35016,6 +35062,22 @@ __metadata: languageName: node linkType: hard +"redis-errors@npm:^1.0.0, redis-errors@npm:^1.2.0": + version: 1.2.0 + resolution: "redis-errors@npm:1.2.0" + checksum: f28ac2692113f6f9c222670735aa58aeae413464fd58ccf3fce3f700cae7262606300840c802c64f2b53f19f65993da24dc918afc277e9e33ac1ff09edb394f4 + languageName: node + linkType: hard + +"redis-parser@npm:^3.0.0": + version: 3.0.0 + resolution: "redis-parser@npm:3.0.0" + dependencies: + redis-errors: ^1.0.0 + checksum: 89290ae530332f2ae37577647fa18208d10308a1a6ba750b9d9a093e7398f5e5253f19855b64c98757f7129cccce958e4af2573fdc33bad41405f87f1943459a + languageName: node + linkType: hard + "reflect.getprototypeof@npm:^1.0.4": version: 1.0.4 resolution: "reflect.getprototypeof@npm:1.0.4" @@ -37094,6 +37156,13 @@ __metadata: languageName: node linkType: hard +"standard-as-callback@npm:^2.1.0": + version: 2.1.0 + resolution: "standard-as-callback@npm:2.1.0" + checksum: 88bec83ee220687c72d94fd86a98d5272c91d37ec64b66d830dbc0d79b62bfa6e47f53b71646011835fc9ce7fae62739545d13124262b53be4fbb3e2ebad551c + languageName: node + linkType: hard + "statuses@npm:2.0.1": version: 2.0.1 resolution: "statuses@npm:2.0.1" @@ -39569,7 +39638,7 @@ __metadata: languageName: node linkType: hard -"uuid@npm:^9.0.0": +"uuid@npm:^9.0.0, uuid@npm:^9.0.1": version: 9.0.1 resolution: "uuid@npm:9.0.1" bin: