diff --git a/.yarnrc.yml b/.yarnrc.yml index 8b757b2..ccdea23 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -1 +1,2 @@ -nodeLinker: node-modules \ No newline at end of file +nodeLinker: node-modules +compressionLevel: 0 diff --git a/CLAUDE.md b/CLAUDE.md index 154ec4c..a5a9715 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -52,12 +52,11 @@ The transfer server is a Socket.IO-based real-time communication server with end - `utils/` - Utility functions for crypto, buffers, caching, etc. **Socket.IO Events:** -- Client→Server: `create-room`, `join-room`, `send-encrypted-data`, `leave-room`, `get-room-status`, `get-room-list` -- Server→Client: `room-created`, `room-joined`, `user-joined`, `user-left`, `encrypted-data`, `room-error`, `room-status` +- Client→Server: `e2ee-request`, `e2ee-c2c-request`, `e2ee-c2c-response` +- Server→Client: `e2ee-response`, `e2ee-c2c-request`, `e2ee-c2c-response`, `user-joined`, `user-left`, `room-full`, `start-transfer` **Configuration (via environment variables):** - `PORT` (default: 3868) -- `CORS_ORIGINS` (comma-separated list) - `MAX_USERS_PER_ROOM` (default: 2) - `ROOM_TIMEOUT` (default: 3600000ms) - `MAX_MESSAGE_SIZE` (default: 10485760 bytes) @@ -80,7 +79,8 @@ A Midway.js-based component for OneKey Prime synchronization functionality. ## Testing Approach -- Both packages use Jest for testing +- transfer-server uses ts-node smoke/crash scripts and the Node.js test runner +- cloud-sync-server uses Jest - Test files are located in `test/` directories - Mock application available in `examples/mock-app/` for integration testing - Run individual package tests with `yarn workspace @onekeyhq/ test` @@ -88,9 +88,9 @@ A Midway.js-based component for OneKey Prime synchronization functionality. ## Code Style and Linting - TypeScript is used throughout the project -- ESLint configuration with TypeScript plugin +- ESLint 10 with the shared root `eslint.config.cjs` and typescript-eslint 8 - Prettier integration for code formatting -- Each package has its own `tsconfig.json` and `.eslintrc.js` +- Each package has its own `tsconfig.json`; source and tests share the root flat lint configuration - Node.js version requirement: >= 24 ## Important Implementation Notes @@ -100,7 +100,11 @@ A Midway.js-based component for OneKey Prime synchronization functionality. 2. **Error Handling**: The transfer-server includes custom error codes (see `errors.ts`). The cloud-sync-server uses Midway.js error handling patterns. 3. **Security**: - - CORS is configured but currently allows all origins in development + - CORS is intentionally permissive; the `Origin` header is not an auth + boundary here (native/desktop clients send no usable Origin, and there are + no cookie credentials to protect). Access control is the out-of-band + pairing code plus the room membership check on the c2c relay. See the + comment on `corsOptions` in `server.ts`. - Message size limits are enforced - Room timeouts prevent resource exhaustion @@ -115,4 +119,4 @@ A Midway.js-based component for OneKey Prime synchronization functionality. 6. **Dependency Injection**: The cloud-sync-server heavily uses Midway.js DI patterns. When adding new services, follow the existing pattern with decorators. -7. **Real-time Communication**: Socket.IO is configured with specific ping/pong intervals and buffer sizes. Be mindful of these settings when debugging connection issues. \ No newline at end of file +7. **Real-time Communication**: Socket.IO is configured with specific ping/pong intervals and buffer sizes. Be mindful of these settings when debugging connection issues. diff --git a/README.md b/README.md index 7b80023..34ae9c7 100644 --- a/README.md +++ b/README.md @@ -133,7 +133,6 @@ Each package can be configured using environment variables. Create `.env` files #### transfer-server ```env PORT=3868 -CORS_ORIGINS=http://localhost:3000 MAX_USERS_PER_ROOM=2 ROOM_TIMEOUT=3600000 MAX_MESSAGE_SIZE=10485760 @@ -300,7 +299,8 @@ chore: Update dependencies - All sensitive configuration should use environment variables - Never commit `.env` files - Use HTTPS in production -- Configure CORS appropriately +- Do not treat CORS as access control in transfer-server: it is deliberately + permissive (see `corsOptions` in `packages/transfer-server/src/server.ts`) - Implement rate limiting - Regular dependency updates diff --git a/eslint.config.cjs b/eslint.config.cjs new file mode 100644 index 0000000..1e14244 --- /dev/null +++ b/eslint.config.cjs @@ -0,0 +1,28 @@ +const js = require('@eslint/js'); +const { defineConfig } = require('eslint/config'); +const prettier = require('eslint-config-prettier'); +const globals = require('globals'); +const tseslint = require('typescript-eslint'); + +module.exports = defineConfig([ + { ignores: ['**/dist/**', '**/coverage/**', '**/node_modules/**'] }, + { + files: ['packages/*/{src,test}/**/*.ts', 'examples/*/{src,test}/**/*.ts'], + extends: [js.configs.recommended, tseslint.configs.recommended, prettier], + languageOptions: { + ecmaVersion: 2021, + sourceType: 'module', + globals: globals.node, + }, + rules: { + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-unused-vars': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-empty-function': 'off', + }, + }, + { + files: ['**/test/**/*.ts'], + languageOptions: { globals: globals.jest }, + }, +]); diff --git a/examples/mock-app/.eslintrc.js b/examples/mock-app/.eslintrc.js deleted file mode 100644 index 1fb4c3f..0000000 --- a/examples/mock-app/.eslintrc.js +++ /dev/null @@ -1,22 +0,0 @@ -module.exports = { - root: true, - parser: '@typescript-eslint/parser', - extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'], - plugins: ['@typescript-eslint'], - parserOptions: { - ecmaVersion: 2021, - sourceType: 'module', - project: './tsconfig.eslint.json', - tsconfigRootDir: __dirname, - }, - env: { - node: true, - jest: true, - }, - rules: { - '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/no-unused-vars': 'off', - '@typescript-eslint/explicit-module-boundary-types': 'off', - '@typescript-eslint/no-empty-function': 'off', - }, -}; \ No newline at end of file diff --git a/examples/mock-app/package.json b/examples/mock-app/package.json index 3da7ebf..574f96b 100644 --- a/examples/mock-app/package.json +++ b/examples/mock-app/package.json @@ -9,7 +9,7 @@ "start": "cross-env NODE_ENV=production node ./bootstrap.js", "test": "midway-bin test --ts", "cov": "midway-bin cov --ts", - "lint": "eslint src --ext .ts" + "lint": "eslint src test --max-warnings 0" }, "dependencies": { "@midwayjs/bootstrap": "^3.20.0", @@ -25,10 +25,8 @@ "@midwayjs/mock": "^3.20.11", "@types/jest": "^29.0.0", "@types/node": "^20.0.0", - "@typescript-eslint/eslint-plugin": "^5.0.0", - "@typescript-eslint/parser": "^5.0.0", "cross-env": "^10.0.0", - "eslint": "^7.32.0", + "eslint": "^10.10.0", "jest": "^29.0.0", "nodemon": "^3.0.0", "ts-jest": "^29.0.0", diff --git a/examples/mock-app/tsconfig.eslint.json b/examples/mock-app/tsconfig.eslint.json deleted file mode 100644 index e4831be..0000000 --- a/examples/mock-app/tsconfig.eslint.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "rootDir": "." - }, - "include": [ - "src/**/*", - "test/**/*" - ], - "exclude": [ - "node_modules", - "dist" - ] -} \ No newline at end of file diff --git a/package.json b/package.json index 172e39e..3feca49 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "@onekeyhq/monorepo", "version": "1.0.0", "private": true, + "packageManager": "yarn@4.14.1", "description": "OneKey Monorepo", "workspaces": [ "packages/*", @@ -19,12 +20,18 @@ "test": "yarn workspaces foreach --all run test", "test:sync": "yarn workspace @onekeyhq/cloud-sync-server test", "test:mock": "yarn workspace @onekeyhq/mock-app test", - "lint": "yarn workspaces foreach --all run lint", + "lint": "eslint packages/*/src packages/*/test examples/*/src examples/*/test --max-warnings 0", "lint:sync": "yarn workspace @onekeyhq/cloud-sync-server lint", "clean": "yarn workspaces foreach --all run clean" }, "devDependencies": { - "rimraf": "^5.0.5" + "@eslint/js": "^10.0.1", + "eslint": "^10.10.0", + "eslint-config-prettier": "^10.1.8", + "globals": "^17.12.0", + "rimraf": "^5.0.5", + "typescript": "^5.0.0", + "typescript-eslint": "^8.70.0" }, "engines": { "node": ">=24" diff --git a/packages/cloud-sync-server/.eslintrc.js b/packages/cloud-sync-server/.eslintrc.js deleted file mode 100644 index abeb1e7..0000000 --- a/packages/cloud-sync-server/.eslintrc.js +++ /dev/null @@ -1,23 +0,0 @@ -module.exports = { - root: true, - parser: '@typescript-eslint/parser', - extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'], - plugins: ['@typescript-eslint', 'prettier'], - parserOptions: { - ecmaVersion: 2021, - sourceType: 'module', - project: './tsconfig.eslint.json', - tsconfigRootDir: __dirname, - }, - env: { - node: true, - jest: true, - }, - rules: { - '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/no-unused-vars': 'off', - '@typescript-eslint/explicit-module-boundary-types': 'off', - '@typescript-eslint/no-empty-function': 'off', - 'node/no-extraneous-import': 'off', - }, -}; \ No newline at end of file diff --git a/packages/cloud-sync-server/package.json b/packages/cloud-sync-server/package.json index bcfad2d..60ece7f 100644 --- a/packages/cloud-sync-server/package.json +++ b/packages/cloud-sync-server/package.json @@ -8,14 +8,14 @@ "build": "tsc", "dev": "tsc -w", "watch": "tsc -w", - "lint": "eslint src test --ext .ts", - "lint:fix": "eslint src test --ext .ts --fix", + "lint": "eslint src test --max-warnings 0", + "lint:fix": "eslint src test --fix --max-warnings 0", "test": "jest", "test:watch": "jest --watch", "test:cov": "jest --coverage" }, "dependencies": { - "lodash": "^4.17.21" + "lodash": "^4.18.1" }, "peerDependencies": { "@midwayjs/core": "^3.0.0", @@ -27,11 +27,7 @@ "devDependencies": { "@types/jest": "^29.0.0", "@types/lodash": "^4.14.200", - "@typescript-eslint/eslint-plugin": "^5.0.0", - "@typescript-eslint/parser": "^5.0.0", - "eslint": "^7.32.0", - "eslint-config-prettier": "^8.5.0", - "eslint-plugin-prettier": "^4.0.0", + "eslint": "^10.10.0", "jest": "^29.0.0", "prettier": "^2.8.8", "ts-jest": "^29.0.0", diff --git a/packages/cloud-sync-server/tsconfig.eslint.json b/packages/cloud-sync-server/tsconfig.eslint.json deleted file mode 100644 index 29f9624..0000000 --- a/packages/cloud-sync-server/tsconfig.eslint.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "noEmit": true - }, - "include": ["src/**/*", "test/**/*"], - "exclude": ["dist", "node_modules"] -} \ No newline at end of file diff --git a/packages/transfer-server/README.md b/packages/transfer-server/README.md index 98f5c9b..ec5fa7c 100644 --- a/packages/transfer-server/README.md +++ b/packages/transfer-server/README.md @@ -49,7 +49,6 @@ The server can be configured using environment variables: | Variable | Default | Description | |----------|---------|-------------| | `PORT` | `3868` | Server listening port | -| `CORS_ORIGINS` | `*` | Comma-separated list of allowed CORS origins | | `MAX_USERS_PER_ROOM` | `2` | Maximum users allowed per room | | `ROOM_TIMEOUT` | `3600000` | Room timeout in milliseconds (1 hour) | | `MAX_MESSAGE_SIZE` | `10485760` | Maximum message size in bytes (10MB) | @@ -57,7 +56,6 @@ The server can be configured using environment variables: Example `.env` file: ```env PORT=3868 -CORS_ORIGINS=http://localhost:3000,https://app.onekey.so MAX_USERS_PER_ROOM=2 ROOM_TIMEOUT=3600000 MAX_MESSAGE_SIZE=10485760 @@ -67,37 +65,114 @@ MAX_MESSAGE_SIZE=10485760 ### Socket.IO Events -#### Client → Server Events - -| Event | Description | Payload | -|-------|-------------|---------| -| `create-room` | Create a new room | `{ roomId?: string, metadata?: object }` | -| `join-room` | Join an existing room | `{ roomId: string, userId?: string }` | -| `send-encrypted-data` | Send encrypted data to room members | `{ data: any, targetUserId?: string }` | -| `leave-room` | Leave the current room | `{ roomId: string }` | -| `get-room-status` | Get room information | `{ roomId: string }` | -| `get-room-list` | Get list of available rooms | `{}` | - -#### Server → Client Events - -| Event | Description | Payload | -|-------|-------------|---------| -| `room-created` | Room successfully created | `{ roomId: string, creatorId: string }` | -| `room-joined` | Successfully joined room | `{ roomId: string, userId: string, users: string[] }` | -| `user-joined` | Another user joined the room | `{ userId: string, users: string[] }` | -| `user-left` | User left the room | `{ userId: string, users: string[] }` | -| `encrypted-data` | Received encrypted data | `{ data: any, senderId: string }` | -| `room-error` | Error occurred | `{ code: string, message: string }` | -| `room-status` | Room status information | `{ roomId: string, users: User[], createdAt: number }` | +Room methods use the bridge request event, not separate `create-room` or +`join-room` events. A request payload contains `id`, `type: "REQUEST"`, and +`data: { module, method, params }`. Responses preserve correlation fields and +contain `type: "RESPONSE"` with `data` or `error: { name, message, code }`. +Optional string metadata (`origin`, `peerOrigin`, `scope`) is limited to 1024 characters. +`remoteId` accepts null, finite numbers, or strings up to 1024 characters. +Request IDs, when supplied, and response IDs are safe integers. + +| Direction | Event | Payload | +| --- | --- | --- | +| Client → server | `e2ee-request` | Bridge request; module `roomManager` | +| Server → client | `e2ee-response` | Bridge response to the server API | +| Client → server | `e2ee-c2c-request` | `{ roomId, payload: }` | +| Client → server | `e2ee-c2c-response` | `{ roomId, payload: }` | +| Server → peer | `e2ee-c2c-request` | The unwrapped bridge REQUEST | +| Server → peer | `e2ee-c2c-response` | The unwrapped bridge RESPONSE, or a relay rejection | +| Server → existing members | `user-joined` | `{ roomId, userId, userCount }`; excludes the joiner | +| Server → remaining members | `user-left` | `{ roomId, userId, userCount }` | +| Server → room members | `room-full` | `{ roomId, userCount }` | +| Server → room members | `start-transfer` | `{ roomId, fromUserId, toUserId, randomNumber }` | + +The event and bridge type must match. Both relay directions require membership +in Socket.IO and RoomManager. Authorized, accepted relay traffic renews room +activity; rejected traffic does not. Rooms expire after `ROOM_TIMEOUT` of +inactivity, checked every five minutes. Expiry emits the existing `user-left` +event with `userCount: 0` and removes Socket.IO membership only for that room; +other rooms on the connection remain usable. Leaving an already removed room is idempotent. + +### Room RPCs + +Use `data.module = "roomManager"`; `params` is an array of method arguments. + +| Method | First argument | Result | +| --- | --- | --- | +| `createRoom` | No arguments | `{ roomId, encryptionKey }` | +| `joinRoom`, `joinRoomAfterCreate` | `{ roomId, appPlatform, appPlatformName, appVersion, appBuildNumber, appDeviceName }` | `{ success, userId, roomId, userCount, roomKey, chunkedTransferVersion: 1, maxMessageSize }` | +| `getRoomUsers` | `{ roomId }` | User records ordered by join time, without socket IDs | +| `leaveRoom` | `{ roomId, userId }` | `{ success, userCount, roomDestroyed }` | +| `startTransfer` | `{ roomId, fromUserId, toUserId }` | Transfer direction, or undefined when cleared | + +`getRoomUsers` returns `[]` for both nonexistent rooms and non-members. This +preserves legacy missing-room behavior without exposing room existence. It uses +a per-connection token bucket of 10 requests, replenishing 5 requests/second. +Joins reserve capacity while the adapter is pending and publish membership only +after it succeeds; disconnects and failed joins release that reservation. +Supplied join metadata must be strings: platform/version/build fields allow 64 +characters, platform display name 256, and device name 512. Missing legacy fields +remain accepted. `maxMessageSize` advertises the deployment limit to new senders. + +### Chunk protocol v1 and relay limits + +Chunking requires the relay join result and the peer's `getTransferType` result +to advertise `chunkedTransferVersion: 1`. Older clients/relays retain +`sendTransferData` single-message transfers. A new sender must fall back when an +old peer does not implement the capability method. + +| Limit | Value | +| --- | --- | +| Chunk data | 64 KiB Base64 ASCII (`params[0].data`) | +| Complete chunk envelope | 72 KiB JSON UTF-8 bytes, including room ID, all extra fields and bridge metadata | +| Transfer total | 64 MiB of encrypted Base64 wire data; not the original wallet data size | +| Chunk indices | 0–1023 inclusive, derived from total bytes / chunk size | +| Chunk requests | At most 512 per connection per one-second window | +| Complete response envelope | 256 KiB JSON UTF-8 bytes | +| Responses | At most 1024 per connection per one-second window | +| Requests + responses | At most 1600 relay messages and 48 MiB per connection per one-second window | +| Legacy single request | Existing `MAX_MESSAGE_SIZE`, 10 MiB by default | + +The byte budget covers 512 complete 72 KiB chunk envelopes (36 MiB), plus +12 MiB for normal ACKs/control messages. Aggregate counts reserve control-message +headroom in addition to chunk and response counts. The combined byte budget is at least `MAX_MESSAGE_SIZE` when a deployment +explicitly raises that setting. Rejected relay traffic also consumes the shared +budget. Malformed/binary/overly complex JSON is not forwarded (maximum depth 64, +16384 visited values). Crossing the shared traffic or response-rate budget disconnects the abusive +socket. Within the transport packet limit, a single oversized or overly complex +response with valid metadata and membership becomes a small same-ID error for the original caller; its body is +never relayed, and the responder is not sent another response. Rejected requests +with a valid ID and membership receive `1001`; one-way requests receive no reply. Limits are per connection; +production ingress must also bound connection counts and aggregate traffic. + +Chunk RPCs use module `api`: `beginChunkedTransfer({ transferId, totalBytes })`, +`sendTransferChunk({ transferId, index, data })`, and +`finishChunkedTransfer({ transferId })`. A chunk acknowledgement contains +`{ transferId, index, receivedBytes }`. The relay validates chunk shape and packet +size but does not assemble, decrypt, or maintain the transfer manifest. The App +checks total size before starting and the receiver checks it again on begin. +In a valid bridge request, invalid chunk parameters or packet size return `1001`; actual chunk throttling +returns `1100`, on `e2ee-c2c-response`. The relay performs no automatic retries. +Malformed bridge envelopes are discarded; response IDs are required. + +New senders check the 64 MiB total before beginning a chunk transfer. Legacy +fallbacks check the complete encoded Socket.IO message before emitting wallet +data, using the advertised `maxMessageSize` or the historical 10 MiB default +when connected to an older relay. These total/message checks are independent +of throughput limits; increasing `MAX_MESSAGE_SIZE` does not raise the chunked-transfer total. + +CORS reflects the requesting origin and retains `credentials: true` for browser +clients using credentialed HTTP polling, even though this service does not use +cookie authentication. CORS is not an authorization boundary. ### REST API Endpoints | Endpoint | Method | Description | -|----------|--------|-------------| -| `/health` | GET | Health check endpoint | -| `/stats` | GET | Server statistics | -| `/rooms` | GET | List all active rooms | -| `/rooms/:roomId` | GET | Get room details | +| --- | --- | --- | +| `/health` | GET | `{ message: "Health check OK: " }` | + +There are no `/stats`, `/rooms`, or `/rooms/:roomId` handlers. Room operations +use the RPC interface above. ## Architecture @@ -127,16 +202,18 @@ MAX_MESSAGE_SIZE=10485760 ### Built-in Security Features -1. **Message Size Limits**: Prevents DoS attacks by limiting message sizes +1. **Message and Traffic Limits**: Bounds per-connection relay size and throughput 2. **Room Timeouts**: Automatic cleanup of inactive rooms 3. **User Limits**: Configurable maximum users per room -4. **CORS Protection**: Configurable CORS origins -5. **Input Validation**: Automatic validation of all API inputs +4. **Room Membership Enforcement**: Client-to-client messages are relayed only + for a sender that has actually joined the target room +5. **Input Validation**: Bridge type, metadata, membership, and chunk validation ### Best Practices - Always use HTTPS in production -- Configure CORS origins appropriately +- Do not treat CORS as access control: it is deliberately permissive and + `Origin` is not the auth boundary here (see `corsOptions` in `src/server.ts`) - Implement rate limiting with a reverse proxy - Monitor room creation patterns for abuse - Use environment variables for sensitive configuration @@ -189,25 +266,32 @@ yarn start # Start production server ### Testing ```bash -# Run tests (when implemented) +# Build and run TCP smoke, crash logging, relay policy, and lifecycle tests yarn test -# Run tests with coverage -yarn test:coverage +# Run the relay compatibility and lifecycle suite +yarn test:compatibility ``` ## Error Handling -The server implements a comprehensive error handling system with specific error codes: +Server-generated errors omit stack traces on the wire; server logs retain them. +Peer errors are relayed as peer data. + +| Code | Meaning | +| --- | --- | +| `1001` | Invalid parameter or chunk packet size; retrying unchanged input does not help | +| `1002` | Operation failed, including a closed connection during join | +| `1100` | Per-method or chunk rate limit | +| `1700` | Missing server-side socket context | +| `1701` | Invalid room ID | +| `1702` | Room not found for room operations other than the privacy-preserving user query | +| `1703` | Connection rejected because the room has no available slot | +| `1704` | User not found | +| `1705` | Socket is not in the room for an operation requiring membership | +| `1706` | Transfer participants are not both room members | -| Error Code | Description | -|------------|-------------| -| `ROOM_NOT_FOUND` | Requested room does not exist | -| `ROOM_FULL` | Room has reached maximum capacity | -| `UNAUTHORIZED` | User not authorized for this operation | -| `INVALID_DATA` | Invalid data format or content | -| `TIMEOUT` | Operation timed out | -| `INTERNAL_ERROR` | Internal server error | +See `src/errors.ts` for the complete code list. ## Performance Optimization @@ -257,11 +341,10 @@ module.exports = { curl http://localhost:3868/health ``` -### Server Statistics +### Room Lifecycle -```bash -curl http://localhost:3868/stats -``` +Monitor the structured `room.created`, `room.joined`, `room.left`, and +`room.expiredCleaned` log events. There is no statistics HTTP endpoint. ## Troubleshooting @@ -276,15 +359,18 @@ curl http://localhost:3868/stats ``` 2. **CORS Issues** - - Ensure `CORS_ORIGINS` environment variable is properly configured - - Check that client origin matches allowed origins + - CORS is intentionally permissive: every origin is accepted and there is no + allowlist to configure + - `Origin` is not the auth boundary here - access control is the out-of-band + pairing code plus the room membership check on the client-to-client relay. + See the comment on `corsOptions` in `src/server.ts` for why 3. **Connection Timeouts** - Verify firewall settings - Check WebSocket support in reverse proxy configuration 4. **Memory Leaks** - - Monitor room cleanup with `/stats` endpoint + - Monitor the structured `room.expiredCleaned` log events - Ensure `ROOM_TIMEOUT` is configured appropriately ## Contributing @@ -301,4 +387,4 @@ This project is part of the OneKey ecosystem. ## Support -For issues and questions, please open an issue on GitHub or contact the OneKey development team. \ No newline at end of file +For issues and questions, please open an issue on GitHub or contact the OneKey development team. diff --git a/packages/transfer-server/env.example b/packages/transfer-server/env.example index ab8f1fe..9ad361e 100644 --- a/packages/transfer-server/env.example +++ b/packages/transfer-server/env.example @@ -3,9 +3,6 @@ # Server port (default: 3868) PORT=3868 -# CORS allowed origins, comma-separated -CORS_ORIGINS=http://localhost:3000,http://localhost:3001,http://localhost:3868 - # Maximum users per room (default: 2) MAX_USERS_PER_ROOM=2 diff --git a/packages/transfer-server/package.json b/packages/transfer-server/package.json index 0a2c110..063f6a8 100644 --- a/packages/transfer-server/package.json +++ b/packages/transfer-server/package.json @@ -9,9 +9,11 @@ "start": "node dist/server.js", "dev": "nodemon src/server.ts", "clean": "rimraf dist", - "test": "yarn build && ts-node --project test/tsconfig.json test/smoke.ts && ts-node --project test/tsconfig.json test/crash-logging.ts", + "test": "yarn build && ts-node --project test/tsconfig.json test/smoke.ts && ts-node --project test/tsconfig.json test/crash-logging.ts && yarn test:compatibility", + "test:compatibility": "TS_NODE_PROJECT=test/tsconfig.json node --require ts-node/register --test --test-force-exit test/relay-compatibility.ts test/relay-policy.ts test/room-lifecycle.ts", "test:crash": "yarn build && ts-node --project test/tsconfig.json test/crash-logging.ts", - "postinstall": "echo 'e2ee-server dependencies installed'" + "postinstall": "echo 'e2ee-server dependencies installed'", + "lint": "eslint src test --max-warnings 0" }, "dependencies": { "@noble/hashes": "^1.8.0", @@ -21,7 +23,7 @@ "cors": "^2.8.5", "express": "^4.18.2", "fast-json-stable-stringify": "^2.1.0", - "lodash": "^4.17.21", + "lodash": "^4.18.1", "lru-cache": "^10.0.0", "memoizee": "^0.4.15", "nanoid": "^5.0.4", @@ -35,6 +37,7 @@ "@types/lodash": "^4.14.202", "@types/memoizee": "^0.4.11", "@types/node": "^20.0.0", + "eslint": "^10.10.0", "nodemon": "^3.0.0", "pino-pretty": "^13.1.3", "rimraf": "^5.0.5", diff --git a/packages/transfer-server/src/JsBridgeE2EEServer.ts b/packages/transfer-server/src/JsBridgeE2EEServer.ts index 69492b7..f4aef4b 100644 --- a/packages/transfer-server/src/JsBridgeE2EEServer.ts +++ b/packages/transfer-server/src/JsBridgeE2EEServer.ts @@ -1,60 +1,29 @@ -/* eslint-disable no-restricted-syntax */ import { JsBridgeBase } from '@onekeyfe/cross-inpage-provider-core'; import { IJsBridgeMessageTypes } from '@onekeyfe/cross-inpage-provider-types'; import { E2eeError, E2eeErrorCode } from './errors'; -import { createModuleLogger } from './utils/logger'; +import { CHUNK_PACKET_BYTES, RESPONSE_PACKET_BYTES, RELAY_BYTES_PER_SECOND, RelayTrafficBudget, isValidTransferChunk, measureJsonBytes } from './relayPolicy'; +import { RequestRateLimiter } from './requestRateLimiter'; +import { capForLog, createModuleLogger } from './utils/logger'; import type { IJsBridgeConfig, IJsBridgeMessagePayload, IJsonRpcRequest, } from '@onekeyfe/cross-inpage-provider-types'; +import type { RoomManager } from './roomManager'; import type { Socket } from 'socket.io'; const logger = createModuleLogger('jsBridge'); -const RATE_LIMIT_INTERVAL_MS = 3000; - -// Upper bound on distinct methods tracked per connection. `method` comes from -// the client, so without a cap a single socket could grow this map forever. -// Well above the number of methods a real client calls. -const RATE_LIMIT_MAX_TRACKED_METHODS = 64; - -// Log lines carry client-controlled strings, which can be as large as -// maxHttpBufferSize (10MB). pino writes to fd 1 synchronously, so a verbatim -// field turns a rejected packet into disk and log-pipeline amplification - -// measured at 954MB of log output from three seconds of traffic. -const LOG_FIELD_MAX_LENGTH = 64; - // Rejecting a payload happens before rate limiting can apply (a malformed // packet may carry no method to limit on), so the log itself has to be capped // per connection or it can be triggered at socket speed. const INVALID_PAYLOAD_LOG_INTERVAL_MS = 1000; const INVALID_PAYLOAD_LOG_BURST = 5; -/** Truncate a client-controlled value before it reaches a log line. */ -function capForLog(value: unknown, max: number = LOG_FIELD_MAX_LENGTH): string | null { - if (typeof value !== 'string') { - return null; - } - return value.length > max ? `${value.slice(0, max)}...(${value.length})` : value; -} - -const CLIENT_TO_CLIENT_RATE_LIMIT_ERROR_CODE = -387_155_488; - -// Rate limiting whitelist - methods that are exempt from rate limiting -const RATE_LIMIT_WHITELIST = new Set([ - 'changeTransferDirection', - 'getRoomUsers', - 'leaveRoom', - 'cancelTransfer', -]); - -const SUPPORTED_MESSAGE_TYPES: ReadonlySet = new Set([ - IJsBridgeMessageTypes.REQUEST, - IJsBridgeMessageTypes.RESPONSE, -]); +type IResponseEvent = 'e2ee-response' | 'e2ee-c2c-response'; +type IRequestEvent = 'e2ee-request' | 'e2ee-c2c-request'; type IPayloadCheckResult = | { valid: true; payload: IJsBridgeMessagePayload } @@ -77,8 +46,26 @@ function checkBridgePayload( const payload = raw as IJsBridgeMessagePayload; - if (!payload.type || !SUPPORTED_MESSAGE_TYPES.has(payload.type)) { - return { valid: false, reason: 'payload.type is missing or unsupported' }; + const expectedType = requireMethod ? IJsBridgeMessageTypes.REQUEST : IJsBridgeMessageTypes.RESPONSE; + if (payload.type !== expectedType) { + return { valid: false, reason: 'payload.type does not match its event' }; + } + if ((!requireMethod && payload.id === undefined) || (payload.id !== undefined && + !(typeof payload.id === 'number' && Number.isSafeInteger(payload.id)))) { + return { valid: false, reason: 'payload.id is invalid' }; + } + for (const key of ['scope', 'peerOrigin', 'origin'] as const) { + const value = payload[key]; + if (value !== undefined && (typeof value !== 'string' || value.length > 1024)) { + return { valid: false, reason: 'bridge metadata is invalid or too large' }; + } + } + + const remoteId = payload.remoteId; + if (remoteId !== undefined && remoteId !== null && + !(typeof remoteId === 'string' && remoteId.length <= 1024) && + !(typeof remoteId === 'number' && Number.isFinite(remoteId))) { + return { valid: false, reason: 'payload.remoteId is invalid' }; } if (requireMethod) { @@ -94,26 +81,24 @@ function checkBridgePayload( export class JsBridgeE2EEServer extends JsBridgeBase { constructor( config: IJsBridgeConfig, - { socketClient }: { socketClient: Socket }, + { + socketClient, + roomManager, + }: { socketClient: Socket; roomManager: RoomManager }, ) { super(config); this.socketClient = socketClient; + this.roomManager = roomManager; this.setup(); } private socketClient: Socket; - /** - * Rate limit state, scoped to this connection rather than kept in a - * module-level map that lived for the lifetime of the process. - * - * Cleared explicitly on disconnect rather than left to GC: JsBridgeBase - * instances are currently retained for the lifetime of the process, so - * anything hanging off them has to be released by hand. For the same reason - * this is a plain Map - a structure that preallocates would turn into a - * fixed cost per connection. - */ - private rateLimitState = new Map(); + private roomManager: RoomManager; + + private readonly requestLimits = new RequestRateLimiter(); + + private readonly relayTraffic = new RelayTrafficBudget(); private invalidPayloadLogWindowStart = 0; @@ -124,13 +109,47 @@ export class JsBridgeE2EEServer extends JsBridgeBase { override sendAsString = false; sendPayload(payload: IJsBridgeMessagePayload | string): void { + this.emitResponse('e2ee-response', payload); + } + + private emitResponse( + eventName: IResponseEvent, + payload: IJsBridgeMessagePayload | string, + ): void { const p = payload as IJsBridgeMessagePayload; - const e = p?.error as { message: string; code: number } | undefined; - if (e && e?.code && e?.code === CLIENT_TO_CLIENT_RATE_LIMIT_ERROR_CODE) { - this.socketClient.emit('e2ee-c2c-response', payload); - return; + // The bridge copies errors into plain objects before reaching this exit, + // so E2eeError.toJSON() alone cannot keep server stacks off the socket. + if (p?.error && typeof p.error === 'object') { + delete (p.error as { stack?: string }).stack; } - this.socketClient.emit('e2ee-response', payload); + this.socketClient.emit(eventName, payload); + } + + private sendRequestError( + eventName: IRequestEvent, + payload: IJsBridgeMessagePayload, + error: E2eeError, + ): void { + if (payload.id === undefined) return; + // C2S and C2C bridges allocate IDs independently. Carry the response channel + // alongside this request instead of inferring it from an ID or error code. + // Match JsBridgeBase.responseError's wire envelope without its C2S egress. + this.emitResponse( + eventName === 'e2ee-c2c-request' ? 'e2ee-c2c-response' : 'e2ee-response', + this.buildErrorResponse(payload, error), + ); + } + + private buildErrorResponse(payload: IJsBridgeMessagePayload, error: E2eeError): IJsBridgeMessagePayload { + return { + id: payload.id, + type: IJsBridgeMessageTypes.RESPONSE, + origin: '', + scope: payload.scope, + remoteId: payload.remoteId, + peerOrigin: payload.peerOrigin, + error: error.toJSON(), + }; } /** @@ -243,83 +262,29 @@ export class JsBridgeE2EEServer extends JsBridgeBase { eventName: string; sendErrorResponse: () => void; }) { - // Rate limiting check - const req = payload?.data as IJsonRpcRequest | undefined; - const method = typeof req?.method === 'string' ? req.method : ''; - - // Check if method is in whitelist - if (RATE_LIMIT_WHITELIST.has(method)) { - return false; - } - - // no socket id in the key: the map already belongs to this connection - const rateLimitKey = `${eventName}:${method}`; - - const now = Date.now(); - const lastTime = this.rateLimitState.get(rateLimitKey); - - if (lastTime !== undefined && now - lastTime < RATE_LIMIT_INTERVAL_MS) { - sendErrorResponse(); - return true; - } - - if ( - lastTime === undefined && - this.rateLimitState.size >= RATE_LIMIT_MAX_TRACKED_METHODS - ) { - this.pruneRateLimitState(now); - - if (this.rateLimitState.size >= RATE_LIMIT_MAX_TRACKED_METHODS) { - // Every tracked window is still live, so this connection is flooding - // distinct method names. Refuse to track a new one and treat it as - // limited: the flood throttles itself and the existing windows - the - // expensive calls it is trying to reset - stay intact. - logger.debug( - { socketId: this.socketClient.id }, - 'jsBridge.rateLimitCapacityReached', - ); - sendErrorResponse(); - return true; - } - } - - this.rateLimitState.set(rateLimitKey, now); - return false; - } - - /** - * Reclaim entries whose window has already passed - they cannot rate limit - * anything any more. - * - * This only ever drops expired entries. Live windows are never touched: since - * `method` is client-controlled, wiping the map on a flood would let the - * flooder reset the windows of the calls it was just blocked on, turning the - * bound into a rate-limit bypass. - */ - private pruneRateLimitState(now: number): void { - for (const [key, time] of this.rateLimitState) { - if (now - time >= RATE_LIMIT_INTERVAL_MS) { - this.rateLimitState.delete(key); - } - } + const req = payload.data as IJsonRpcRequest | undefined; + if (!this.requestLimits.isLimited(eventName, req?.method ?? '')) return false; + sendErrorResponse(); + return true; } - private buildRateLimitResponder(payload: IJsBridgeMessagePayload) { + private buildRateLimitResponder( + eventName: IRequestEvent, + payload: IJsBridgeMessagePayload, + ) { return () => { logger.debug( { socketId: this.socketClient.id }, 'jsBridge.rateLimitExceeded', ); - this.responseError({ - id: payload.id || -9999, - error: new E2eeError( + this.sendRequestError( + eventName, + payload, + new E2eeError( E2eeErrorCode.RATE_LIMIT_EXCEEDED, 'Rate limit, please try again later', ), - scope: payload.scope, - remoteId: payload.remoteId, - peerOrigin: payload.peerOrigin, - }); + ); }; } @@ -327,7 +292,7 @@ export class JsBridgeE2EEServer extends JsBridgeBase { // JsBridgeBase instances outlive their socket, so per-connection state is // released explicitly rather than left for GC to reclaim this.socketClient.on('disconnect', () => { - this.rateLimitState.clear(); + this.requestLimits.clear(); }); this.socketClient.on( @@ -343,7 +308,7 @@ export class JsBridgeE2EEServer extends JsBridgeBase { const isRateLimited = this.checkIsRateLimited({ payload: p, eventName: 'e2ee-request', - sendErrorResponse: this.buildRateLimitResponder(p), + sendErrorResponse: this.buildRateLimitResponder('e2ee-request', p), }); if (isRateLimited) { @@ -360,6 +325,8 @@ export class JsBridgeE2EEServer extends JsBridgeBase { this.socketClient.on( 'e2ee-c2c-request', this.safeHandler('e2ee-c2c-request', (raw) => { + const traffic = this.checkRelayTraffic(raw, false); + if (traffic.disconnected) return; const envelope = this.checkC2cEnvelope('e2ee-c2c-request', raw, { requireMethod: true, }); @@ -367,17 +334,26 @@ export class JsBridgeE2EEServer extends JsBridgeBase { return; } const { payload: p, roomId } = envelope; + const request = p.data as IJsonRpcRequest; + if (traffic.bytes === undefined || (request.method === 'sendTransferChunk' && + (traffic.bytes > CHUNK_PACKET_BYTES || !isValidTransferChunk(request.params)))) { + this.sendRequestError('e2ee-c2c-request', p, new E2eeError( + E2eeErrorCode.INVALID_PARAMETER, 'Invalid transfer payload or packet size', + )); + return; + } const isRateLimited = this.checkIsRateLimited({ payload: p, eventName: 'e2ee-c2c-request', - sendErrorResponse: this.buildRateLimitResponder(p), + sendErrorResponse: this.buildRateLimitResponder('e2ee-c2c-request', p), }); if (isRateLimited) { return; } + this.roomManager.updateRoomActivity(roomId); this.socketClient.to(roomId).emit('e2ee-c2c-request', p); }), ); @@ -385,7 +361,9 @@ export class JsBridgeE2EEServer extends JsBridgeBase { this.socketClient.on( 'e2ee-c2c-response', this.safeHandler('e2ee-c2c-response', (raw) => { - // a response carries a result rather than a method, so `method` is not required + const traffic = this.checkRelayTraffic(raw, true); + if (traffic.disconnected) return; + // A response carries a result rather than a method. const envelope = this.checkC2cEnvelope('e2ee-c2c-response', raw, { requireMethod: false, }); @@ -394,11 +372,35 @@ export class JsBridgeE2EEServer extends JsBridgeBase { } const { payload: p, roomId } = envelope; + if (traffic.bytes === undefined || traffic.bytes > RESPONSE_PACKET_BYTES) { + // Complete the original caller's RPC with a small error. Never send + // a response back to the responder or relay its oversized error body. + this.socketClient.to(roomId).emit('e2ee-c2c-response', this.buildErrorResponse( + p, new E2eeError(E2eeErrorCode.INVALID_PARAMETER, 'Peer response exceeds relay limits'), + )); + return; + } + + this.roomManager.updateRoomActivity(roomId); this.socketClient.to(roomId).emit('e2ee-c2c-response', p); }), ); } + private checkRelayTraffic(raw: unknown, response: boolean): { disconnected: boolean; bytes?: number } { + const packetLimit = this.roomManager.maxMessageSize; + const bytes = raw === undefined ? 0 : measureJsonBytes(raw, packetLimit); + if (!this.relayTraffic.consume( + bytes ?? packetLimit, response, Math.max(RELAY_BYTES_PER_SECOND, packetLimit), + )) { + // Stop parsing and responding to a sustained flood on this connection. + this.socketClient.disconnect(true); + return { disconnected: true }; + } + if (bytes === undefined) this.logInvalidPayload('relay', raw, 'packet exceeds JSON limits'); + return { disconnected: false, bytes }; + } + /** * Client-to-client events are wrapped in a `{ payload, roomId }` envelope. * Destructuring it blindly throws when the client emits the event with no @@ -430,6 +432,19 @@ export class JsBridgeE2EEServer extends JsBridgeBase { return undefined; } + // `socket.to(roomId)` is a delivery operator: it reads the membership of the + // recipients and never checks the sender's. Without this, any connected + // socket that knows a roomId can inject client-to-client calls into a room + // it never joined - bypassing the room-slot invariant the pairing flow + // relies on. Membership is authoritative in RoomManager, so ask it. + if ( + !this.socketClient.rooms.has(roomId) || + !this.roomManager.isUserInRoom(roomId, this.socketClient.id).isInRoom + ) { + this.logInvalidPayload(eventName, payload, 'sender is not a room member'); + return undefined; + } + return { payload: checked.payload, roomId }; } } diff --git a/packages/transfer-server/src/decorators/e2eeApiMethod.ts b/packages/transfer-server/src/decorators/e2eeApiMethod.ts index 329e8fb..0982979 100644 --- a/packages/transfer-server/src/decorators/e2eeApiMethod.ts +++ b/packages/transfer-server/src/decorators/e2eeApiMethod.ts @@ -17,15 +17,12 @@ export function e2eeApiMethod() { ) { // Get existing allowed methods or initialize empty set const allowedMethods = - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access target.constructor[E2EE_API_METHODS_KEY] || new Set(); // Add this method to the allowed methods set - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call allowedMethods.add(propertyKey); // Store the updated set back on the constructor - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access target.constructor[E2EE_API_METHODS_KEY] = allowedMethods; return descriptor; @@ -36,9 +33,7 @@ export function e2eeApiMethod() { * Check if a method is allowed to be called via E2EE API */ export function isMethodAllowed(instance: any, methodName: string): boolean { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access const constructor = instance.constructor; - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access const allowedMethods = constructor[E2EE_API_METHODS_KEY] as Set; return allowedMethods?.has(methodName) || false; diff --git a/packages/transfer-server/src/e2eeServerApi.ts b/packages/transfer-server/src/e2eeServerApi.ts index 32815a0..ce8f116 100644 --- a/packages/transfer-server/src/e2eeServerApi.ts +++ b/packages/transfer-server/src/e2eeServerApi.ts @@ -1,7 +1,3 @@ -/* eslint-disable prettier/prettier */ -/* eslint-disable no-restricted-syntax */ -/* eslint-disable new-cap */ - import { JsBridgeE2EEServer } from './JsBridgeE2EEServer'; import { E2eeError, E2eeErrorCode } from './errors'; import { memoizee } from './utils/cacheUtils'; @@ -61,15 +57,13 @@ function createBridgeE2EEServer({ receiveHandler: async (payload) => { const req: IJsonRpcRequest = payload.data as IJsonRpcRequest; - // @ts-ignore - // eslint-disable-next-line @typescript-eslint/no-unsafe-call const result = await callE2EEServerApiMethod(req); - // eslint-disable-next-line @typescript-eslint/no-unsafe-return return result; }, }, { socketClient, + roomManager, }, ); } diff --git a/packages/transfer-server/src/e2eeServerApiProxy.ts b/packages/transfer-server/src/e2eeServerApiProxy.ts index b3ea321..c37dbc6 100644 --- a/packages/transfer-server/src/e2eeServerApiProxy.ts +++ b/packages/transfer-server/src/e2eeServerApiProxy.ts @@ -1,5 +1,3 @@ -/* eslint-disable no-restricted-syntax */ - import { JsBridgeE2EEClient } from './JsBridgeE2EEClient'; import { RemoteApiProxyBase } from './utils/RemoteApiProxyBase'; diff --git a/packages/transfer-server/src/errors.ts b/packages/transfer-server/src/errors.ts index 334e468..3f0905e 100644 --- a/packages/transfer-server/src/errors.ts +++ b/packages/transfer-server/src/errors.ts @@ -64,13 +64,21 @@ export class E2eeError extends Error { return new E2eeError(code, message); } - // Convert to JSON for serialization + // Convert to JSON for serialization, without the server stack trace. + // + // This is not what keeps the stack off the socket path: JsBridgeBase + // .createPayload() replaces payload.error with its own plain copy + // (toPlainError), reading err.stack off the instance directly, so toJSON() + // never runs before a response is emitted. The stack is stripped at the + // shared egress point instead - see JsBridgeE2EEServer.emitResponse(). This + // stays as defence in depth for any path that serializes an E2eeError + // directly. `stack` remains on the instance for server-side pino logging, + // which also reads err.stack rather than going through toJSON. toJSON() { return { name: this.name, message: this.message, code: this.code, - stack: this.stack, }; } -} \ No newline at end of file +} diff --git a/packages/transfer-server/src/relayPolicy.ts b/packages/transfer-server/src/relayPolicy.ts new file mode 100644 index 0000000..23c203c --- /dev/null +++ b/packages/transfer-server/src/relayPolicy.ts @@ -0,0 +1,99 @@ +export const TRANSFER_CHUNK_BYTES = 64 * 1024; +export const TRANSFER_MAX_BYTES = 64 * 1024 * 1024; +export const TRANSFER_MAX_CHUNKS = Math.ceil(TRANSFER_MAX_BYTES / TRANSFER_CHUNK_BYTES); +export const CHUNK_PACKET_BYTES = TRANSFER_CHUNK_BYTES + 8 * 1024; +export const RESPONSE_PACKET_BYTES = 256 * 1024; +export const CHUNK_REQUESTS_PER_SECOND = 512; +export const RELAY_RESPONSES_PER_SECOND = CHUNK_REQUESTS_PER_SECOND * 2; +export const RELAY_MESSAGES_PER_SECOND = CHUNK_REQUESTS_PER_SECOND + RELAY_RESPONSES_PER_SECOND + 64; +// Full chunk envelopes need 36 MiB; reserve another 12 MiB for ACKs/control traffic. +export const RELAY_BYTES_PER_SECOND = CHUNK_PACKET_BYTES * CHUNK_REQUESTS_PER_SECOND + 12 * 1024 * 1024; + +/** Count the entire JSON envelope without building another full packet string. */ +export function measureJsonBytes(value: unknown, limit: number): number | undefined { + let bytes = 0; + let nodes = 0; + const addString = (text: string) => { + // Count JSON escapes without allocating an escaped copy of legacy payloads. + bytes += Buffer.byteLength(text) + 2; + if (bytes > limit) return false; + for (let index = 0; index < text.length; index += 1) { + const code = text.charCodeAt(index); + if (code === 34 || code === 92) bytes += 1; + else if (code < 32) bytes += [8, 9, 10, 12, 13].includes(code) ? 1 : 5; + else if (code >= 0xd800 && code <= 0xdbff) { + const next = text.charCodeAt(index + 1); + if (next >= 0xdc00 && next <= 0xdfff) index += 1; + else bytes += 3; + } else if (code >= 0xdc00 && code <= 0xdfff) bytes += 3; + if (bytes > limit) return false; + } + return bytes <= limit; + }; + const visit = (item: unknown, depth: number): boolean => { + nodes += 1; + if (depth > 64 || nodes > 16_384 || bytes > limit) return false; + if (item === null) bytes += 4; + else if (typeof item === 'string') return addString(item); + else if (typeof item === 'boolean') bytes += item ? 4 : 5; + else if (typeof item === 'number' && Number.isFinite(item)) bytes += String(item).length; + else if (Array.isArray(item)) { + bytes += 2; + for (let i = 0; i < item.length; i += 1) { + if (i) bytes += 1; + if (!visit(item[i] ?? null, depth + 1)) return false; + } + } else if (item && typeof item === 'object' && + (Object.getPrototypeOf(item) === Object.prototype || Object.getPrototypeOf(item) === null)) { + bytes += 2; + let first = true; + for (const key in item) { + if (!Object.prototype.hasOwnProperty.call(item, key)) continue; + const entry = (item as Record)[key]; + if (entry === undefined) continue; + if (!first) bytes += 1; + first = false; + if (!addString(key)) return false; + bytes += 1; + if (!visit(entry, depth + 1)) return false; + } + } else return false; + return bytes <= limit; + }; + return visit(value, 0) ? bytes : undefined; +} + +export function isValidTransferChunk(params: unknown): boolean { + if (!Array.isArray(params) || params.length !== 1) return false; + const chunk = params[0] as { transferId?: unknown; index?: unknown; data?: unknown } | undefined; + return Boolean(chunk && typeof chunk === 'object' && !Array.isArray(chunk) && + Object.keys(chunk).length === 3 && + typeof chunk.transferId === 'string' && /^[a-zA-Z0-9-]{1,64}$/.test(chunk.transferId) && + typeof chunk.index === 'number' && Number.isSafeInteger(chunk.index) && + chunk.index >= 0 && chunk.index < TRANSFER_MAX_CHUNKS && + typeof chunk.data === 'string' && chunk.data.length > 0 && + chunk.data.length <= TRANSFER_CHUNK_BYTES && /^[A-Za-z0-9+/]*={0,2}$/.test(chunk.data)); +} + +/** One bounded budget for both relay directions, including rejected packets. */ +export class RelayTrafficBudget { + private startedAt = 0; + private messages = 0; + private responses = 0; + private bytes = 0; + + consume(bytes: number, response: boolean, byteLimit: number): boolean { + const now = Date.now(); + if (now - this.startedAt >= 1000) { + this.startedAt = now; + this.messages = 0; + this.responses = 0; + this.bytes = 0; + } + this.messages += 1; + this.responses += response ? 1 : 0; + this.bytes += bytes; + return this.messages <= RELAY_MESSAGES_PER_SECOND && + this.responses <= RELAY_RESPONSES_PER_SECOND && this.bytes <= byteLimit; + } +} diff --git a/packages/transfer-server/src/requestRateLimiter.ts b/packages/transfer-server/src/requestRateLimiter.ts new file mode 100644 index 0000000..bfc1537 --- /dev/null +++ b/packages/transfer-server/src/requestRateLimiter.ts @@ -0,0 +1,51 @@ +import { CHUNK_REQUESTS_PER_SECOND } from './relayPolicy'; + +const METHOD_INTERVAL_MS = 3000; +const MAX_TRACKED_METHODS = 64; +const ROOM_QUERY_CAPACITY = 10; +const ROOM_QUERY_REFILL_PER_MS = 5 / 1000; +const UNTHROTTLED_METHODS = new Set(['changeTransferDirection', 'leaveRoom', 'cancelTransfer']); + +/** Request policies are separate from the shared request/response traffic budget. */ +export class RequestRateLimiter { + private methods = new Map(); + private roomQueries?: { tokens: number; updatedAt: number }; + private chunks = { startedAt: 0, count: 0 }; + + clear(): void { + this.methods.clear(); + this.roomQueries = undefined; + this.chunks = { startedAt: 0, count: 0 }; + } + + isLimited(eventName: string, method: string): boolean { + const now = Date.now(); + if (eventName === 'e2ee-request' && method === 'getRoomUsers') { + const tokens = this.roomQueries + ? Math.min(ROOM_QUERY_CAPACITY, this.roomQueries.tokens + Math.max(0, now - this.roomQueries.updatedAt) * ROOM_QUERY_REFILL_PER_MS) + : ROOM_QUERY_CAPACITY; + this.roomQueries = { tokens, updatedAt: now }; + if (tokens < 1) return true; + this.roomQueries.tokens -= 1; + return false; + } + if (eventName === 'e2ee-c2c-request' && method === 'sendTransferChunk') { + if (now - this.chunks.startedAt >= 1000) this.chunks = { startedAt: now, count: 0 }; + this.chunks.count += 1; + return this.chunks.count > CHUNK_REQUESTS_PER_SECOND; + } + if (UNTHROTTLED_METHODS.has(method)) return false; + const key = `${eventName}:${method}`; + const previous = this.methods.get(key); + if (previous !== undefined && now - previous < METHOD_INTERVAL_MS) return true; + if (previous === undefined && this.methods.size >= MAX_TRACKED_METHODS) { + // Only expired entries can be evicted; method-name floods cannot reset live limits. + for (const [tracked, timestamp] of this.methods) { + if (now - timestamp >= METHOD_INTERVAL_MS) this.methods.delete(tracked); + } + if (this.methods.size >= MAX_TRACKED_METHODS) return true; + } + this.methods.set(key, now); + return false; + } +} diff --git a/packages/transfer-server/src/roomManager.ts b/packages/transfer-server/src/roomManager.ts index 508cd03..f952c83 100644 --- a/packages/transfer-server/src/roomManager.ts +++ b/packages/transfer-server/src/roomManager.ts @@ -1,11 +1,9 @@ -/* eslint-disable no-restricted-syntax */ - import { sortBy } from "lodash"; import { e2eeApiMethod } from "./decorators/e2eeApiMethod"; import { E2eeError, E2eeErrorCode } from "./errors"; import cryptoUtils from "./utils/cryptoUtils"; -import { createModuleLogger } from "./utils/logger"; +import { capForLog, createModuleLogger } from "./utils/logger"; import stringUtils from "./utils/stringUtils"; import timerUtils from "./utils/timerUtils"; @@ -14,6 +12,14 @@ import type { IE2EESocketUserInfo, IRoom, IRoomConfig } from "./types"; const logger = createModuleLogger("roomManager"); +const JOIN_FIELD_LIMITS = { + appPlatform: 64, + appPlatformName: 256, + appVersion: 64, + appBuildNumber: 64, + appDeviceName: 512, +} as const; + export type IRoomManagerContext = { socketClient: Socket; }; @@ -23,6 +29,12 @@ export class RoomManager { private config: IRoomConfig; + private readonly pendingJoins = new WeakMap>(); + + get maxMessageSize(): number { + return this.config.maxMessageSize; + } + private socketServer: SocketIOServer; private cleanupInterval: NodeJS.Timeout | number; @@ -46,6 +58,16 @@ export class RoomManager { /** * Create new room * @returns Room information (room ID and encryption key) + * + * NOTE on `encryptionKey` (returned here and as `roomKey` from joinRoom): + * this key is NOT used by the OneKey client, and it is not what protects + * transferred data. The client derives its own end-to-end key locally from + * the pairing code shown in the QR code (only its roomId prefix reaches the + * server), an ECDHE shared secret negotiated between the devices, and the + * room's user list. The server knows the user list but not the pairing-code + * secret or the ECDHE shared secret. It never encrypts or decrypts payloads + * with this generated key - it only relays them. The field is + * kept for wire compatibility with existing clients. */ @e2eeApiMethod() async createRoom(): Promise<{ roomId: string; encryptionKey: string }> { @@ -101,6 +123,11 @@ export class RoomManager { * @param encryptionKey Encryption key * @param socketId User's Socket ID * @returns Join result + * + * The returned `roomKey` is server-generated and unused by the client - see + * the note on createRoom(). It is not part of the end-to-end encryption + * scheme; the client derives its own key from the pairing code and an ECDHE + * exchange this server is not party to. */ @e2eeApiMethod() async joinRoom( @@ -120,7 +147,19 @@ export class RoomManager { roomKey?: string; error?: string; userCount?: number; + chunkedTransferVersion?: number; + maxMessageSize?: number; }> { + if (!params || typeof params !== "object") { + throw new E2eeError(E2eeErrorCode.INVALID_PARAMETER, "Invalid room join parameters"); + } + for (const key of Object.keys(JOIN_FIELD_LIMITS) as Array) { + const value = params[key]; + // Missing optional metadata remains compatible with older clients. + if (value !== undefined && (typeof value !== "string" || value.length > JOIN_FIELD_LIMITS[key])) { + throw new E2eeError(E2eeErrorCode.INVALID_PARAMETER, "Invalid room join metadata"); + } + } await timerUtils.wait(1000); if (!context) { throw new E2eeError( @@ -140,58 +179,78 @@ export class RoomManager { throw new E2eeError(E2eeErrorCode.ROOM_NOT_FOUND, "Room not found"); } - // Check if room is full - if (room.users.size >= room.maxUsers) { - this.socketServer.to(roomId).emit("room-full", { - roomId, - userCount: room.users.size, - }); - - throw new E2eeError( - E2eeErrorCode.CONNECTION_REJECTED, - "Connection Rejected" - ); + const socket = context.socketClient; + if (!socket.connected) { + throw new E2eeError(E2eeErrorCode.OPERATION_FAILED, "Connection closed during room join"); } - // Check if user is already in room (by socketId) + // Rejoining an existing membership is idempotent even when the room is full. for (const [userId, userInfo] of room.users) { - if (userInfo.socketId === socketId) { - return { - success: true, - userId, - roomId, - roomKey: room.encryptionKey, - userCount: room.users.size, - }; - } + if (userInfo.socketId === socketId) return this.buildJoinResult(room, userId); + } + const pending = this.pendingJoins.get(room) ?? new Set(); + this.pendingJoins.set(room, pending); + if (pending.has(socket)) { + throw new E2eeError(E2eeErrorCode.OPERATION_FAILED, "Room join already in progress"); + } + if (room.users.size + pending.size >= room.maxUsers) { + this.socketServer.to(roomId).emit("room-full", { roomId, userCount: room.users.size }); + throw new E2eeError(E2eeErrorCode.CONNECTION_REJECTED, "Connection Rejected"); } - // Create new user - const userId = cryptoUtils.generateUserId(); - const userInfo: IE2EESocketUserInfo = { - id: userId, - socketId, - joinedAt: new Date(), - appPlatformName: params.appPlatformName, - appVersion: params.appVersion, - appBuildNumber: params.appBuildNumber, - appPlatform: params.appPlatform, - appDeviceName: params.appDeviceName, - }; - - room.users.set(userId, userInfo); - room.lastActivity = new Date(); - - logger.info({ userId, roomId, userCount: room.users.size }, "room.joined"); - - await context?.socketClient.join(roomId); + // Reserve capacity without exposing a member before the adapter commits. + // A disconnect releases the reservation even if an adapter is still pending. + pending.add(socket); + const releaseReservation = () => { pending.delete(socket); }; + socket.once("disconnect", releaseReservation); + let addedUserId: string | undefined; + try { + await socket.join(roomId); + if (!socket.connected || this.rooms.get(roomId) !== room || !socket.rooms.has(roomId)) { + throw new E2eeError(E2eeErrorCode.OPERATION_FAILED, "Connection closed during room join"); + } + const userId = cryptoUtils.generateUserId(); + const userInfo: IE2EESocketUserInfo = { + id: userId, + socketId, + joinedAt: new Date(), + appPlatformName: params.appPlatformName, + appVersion: params.appVersion, + appBuildNumber: params.appBuildNumber, + appPlatform: params.appPlatform, + appDeviceName: params.appDeviceName, + }; + room.users.set(userId, userInfo); + addedUserId = userId; + room.lastActivity = new Date(); + logger.info({ userId, roomId, userCount: room.users.size }, "room.joined"); + socket.to(roomId).emit("user-joined", { roomId, userId, userCount: room.users.size }); + return this.buildJoinResult(room, userId); + } catch (error) { + if (addedUserId) room.users.delete(addedUserId); + // Some adapters can mutate their room set before rejecting join(). Do + // not leave delivery membership behind after a failed admission. + try { + await socket.leave(roomId); + } catch { + socket.disconnect(true); + } + throw error; + } finally { + releaseReservation(); + socket.off("disconnect", releaseReservation); + } + } + private buildJoinResult(room: IRoom, userId: string) { return { success: true, userId, - roomId, + roomId: room.id, userCount: room.users.size, roomKey: room.encryptionKey, + chunkedTransferVersion: 1, + maxMessageSize: this.config.maxMessageSize, }; } @@ -220,7 +279,9 @@ export class RoomManager { const { roomId, userId } = params; const room = this.rooms.get(roomId); if (!room) { - throw new E2eeError(E2eeErrorCode.ROOM_NOT_FOUND, "Room not found"); + // Expiry notifications cause legacy clients to acknowledge with leaveRoom. + // Repeating an already completed departure must not reject that cleanup. + return { success: true, userCount: 0, roomDestroyed: true }; } const socketValidation = this.isUserInRoom( @@ -309,26 +370,23 @@ export class RoomManager { "context is required" ); } - logger.debug({ roomId }, "room.getRoomUsers"); - const room = this.rooms.get(roomId); - if (!room) { - logger.debug({ roomId }, "room.getRoomUsersNotFound"); - return []; - } - // Validate that the socket is in the room + logger.debug({ roomId: capForLog(roomId) }, "room.getRoomUsers"); + + // Preserve the legacy missing-room result without revealing whether a + // room exists to non-members. Both cases expose the same empty list. const socketValidation = this.isUserInRoom(roomId, context.socketClient.id); if (!socketValidation.isInRoom) { - throw new E2eeError( - E2eeErrorCode.SOCKET_NOT_IN_ROOM, - "Socket must be in the room to set transfer direction" - ); + logger.debug({ roomId: capForLog(roomId) }, "room.getRoomUsersUnavailable"); + return []; } + const room = this.rooms.get(roomId); + if (!room) return []; const users: IE2EESocketUserInfo[] = sortBy( Array.from(room.users.values()), (item) => item.joinedAt.getTime() ); - logger.debug({ roomId, userCount: users.length }, "room.getRoomUsersResult"); + logger.debug({ roomId: capForLog(roomId), userCount: users.length }, "room.getRoomUsersResult"); return users.map((item) => ({ ...item, socketId: undefined, @@ -476,6 +534,16 @@ export class RoomManager { if (timeSinceActivity > this.config.roomTimeout) { this.rooms.delete(roomId); + if (room.users.size > 0) { + this.socketServer.to(roomId).emit("user-left", { + roomId, + userId: room.users.keys().next().value!, + userCount: 0, + }); + } + // Keep other sessions on the same connection intact; remove only this + // room's delivery membership. Existing clients understand user-left. + this.socketServer.in(roomId).socketsLeave(roomId); cleanedCount += 1; logger.info({ roomId }, "room.expiredCleaned"); } diff --git a/packages/transfer-server/src/server.ts b/packages/transfer-server/src/server.ts index c6b42ea..d7c0b32 100644 --- a/packages/transfer-server/src/server.ts +++ b/packages/transfer-server/src/server.ts @@ -1,9 +1,7 @@ // Must come first: installs browser-global shims that // @onekeyfe/cross-inpage-provider-core reads while its module is evaluated. -// eslint-disable-next-line import/order, import/first import './utils/nodeCompat'; // Then the crash guards, before any other module can throw at load time. -// eslint-disable-next-line import/order, import/first import { markServerStarted } from './utils/processGuards'; import { createServer } from 'http'; @@ -47,19 +45,6 @@ class E2EEServer { constructor() { this.config = { port: parseInt(process.env.PORT || '3868', 10), - corsOrigins: process.env.CORS_ORIGINS?.split(',') || [ - 'http://localhost:3000', - 'http://localhost:3001', - 'http://localhost:3868', - 'null', - 'chrome-extension://*', - 'moz-extension://*', - 'ws://*', - 'wss://*', - 'http://*', - 'https://*', - '*', - ], roomConfig: { maxUsers: parseInt(process.env.MAX_USERS_PER_ROOM || '2', 10), roomTimeout: parseInt(process.env.ROOM_TIMEOUT || '3600000', 10), // 1 hour @@ -76,16 +61,13 @@ class E2EEServer { this.setupMiddleware(); this.setupRoutes(); + // Native clients may omit Origin and desktop clients may send "null". + // Keep credentialed polling compatible even though this service uses no + // cookie authentication. Origin is not authorization. + // Preserve the previous allow-all behavior, which never enforced its + // configured allowlist. Relay access is checked through room membership. this.corsOptions = { - origin: (origin, callback) => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - if (!origin || this.config.corsOrigins.includes(origin)) { - callback(null, true); - } else { - callback(null, true); - // callback(new Error('Invalid CORS request')); - } - }, + origin: true, methods: ['GET', 'POST'], credentials: true, }; @@ -241,7 +223,7 @@ class E2EEServer { private getNetworkIPs(): string[] { const interfaces = networkInterfaces(); const ips: string[] = []; - + for (const name of Object.keys(interfaces)) { const netInterface = interfaces[name]; if (netInterface) { @@ -253,7 +235,7 @@ class E2EEServer { } } } - + return ips; } @@ -270,45 +252,45 @@ class E2EEServer { this.httpServer.listen(this.config.port, () => { const networkIPs = this.getNetworkIPs(); - + // Calculate padding for proper alignment (box width is 58 chars inside) const boxWidth = 58; - + const portText = `🚀 Server started successfully`; const portLine = `║ ${portText}${' '.repeat(boxWidth - portText.length - 2)} ║`; - + const portInfoText = `📡 Port: ${this.config.port}`; const portInfoLine = `║ ${portInfoText}${' '.repeat(boxWidth - portInfoText.length - 2)} ║`; - + const usersText = `👥 Max room users: ${this.config.roomConfig.maxUsers}`; const usersLine = `║ ${usersText}${' '.repeat(boxWidth - usersText.length - 2)} ║`; - + const timeoutMinutes = Math.floor(this.config.roomConfig.roomTimeout / 60_000); const timeoutText = `⏰ Room timeout: ${timeoutMinutes} minutes`; const timeoutLine = `║ ${timeoutText}${' '.repeat(boxWidth - timeoutText.length - 2)} ║`; - + const localhostTitleText = `🏠 Localhost`; const localhostTitleLine = `║ ${localhostTitleText}${' '.repeat(boxWidth - localhostTitleText.length - 2)} ║`; - + const localhostEndpointText = ` - endpoint: http://localhost:${this.config.port}`; const localhostEndpointLine = `║ ${localhostEndpointText}${' '.repeat(boxWidth - localhostEndpointText.length - 2)} ║`; - + const localhostHealthText = ` - health: http://localhost:${this.config.port}/health`; const localhostHealthLine = `║ ${localhostHealthText}${' '.repeat(boxWidth - localhostHealthText.length - 2)} ║`; - + const networkLines = networkIPs.map(ip => { const lanTitleText = `🔗 LAN (${ip})`; const lanTitleLine = `║ ${lanTitleText}${' '.repeat(boxWidth - lanTitleText.length - 2)} ║`; - + const lanEndpointText = ` - endpoint: http://${ip}:${this.config.port}`; const lanEndpointLine = `║ ${lanEndpointText}${' '.repeat(boxWidth - lanEndpointText.length - 2)} ║`; - + const lanHealthText = ` - health: http://${ip}:${this.config.port}/health`; const lanHealthLine = `║ ${lanHealthText}${' '.repeat(boxWidth - lanHealthText.length - 2)} ║`; - + return `${lanTitleLine}\n${lanEndpointLine}\n${lanHealthLine}`; }).join('\n'); - + // human-readable banner; the structured startup event is logged below console.log(` ╔══════════════════════════════════════════════════════════╗ diff --git a/packages/transfer-server/src/types.ts b/packages/transfer-server/src/types.ts index 9c2a21f..029df43 100644 --- a/packages/transfer-server/src/types.ts +++ b/packages/transfer-server/src/types.ts @@ -4,35 +4,31 @@ import type { IJsBridgeMessagePayload } from '@onekeyfe/cross-inpage-provider-ty // Export error classes export { E2eeError, E2eeErrorCode } from './errors'; -// Socket.IO event type definitions +// Client relay input wraps the bridge payload; peer output is unwrapped. +export interface IRelayEnvelope { + roomId: string; + payload: IJsBridgeMessagePayload; +} + export interface IServerToClientEvents { - 'room-created': (data: { roomId: string; encryptionKey: string }) => void; - 'room-joined': (data: { roomId: string; userId: string }) => void; - 'user-joined': (data: { userId: string; userCount: number }) => void; - 'user-left': (data: { userId: string; userCount: number }) => void; - 'encrypted-data': (data: { - encryptedData: string; - senderId: string; - timestamp: number; + 'e2ee-response': (payload: IJsBridgeMessagePayload) => void; + 'e2ee-c2c-request': (payload: IJsBridgeMessagePayload) => void; + 'e2ee-c2c-response': (payload: IJsBridgeMessagePayload) => void; + 'user-joined': (data: { roomId: string; userId: string; userCount: number }) => void; + 'user-left': (data: { roomId: string; userId: string; userCount: number }) => void; + 'room-full': (data: { roomId: string; userCount: number }) => void; + 'start-transfer': (data: { + roomId: string; + fromUserId: string; + toUserId: string; + randomNumber: string; }) => void; - 'room-error': (data: { error: string }) => void; - 'room-status': (data: { userCount: number; users: string[] }) => void; - 'room-list': (data: { rooms: IRoomListItem[] }) => void; } export interface IClientToServerEvents { - 'e2ee-request': (event: string, payload: IJsBridgeMessagePayload) => void; - 'e2ee-response': (event: string, payload: unknown) => void; - - 'create-room': () => void; - 'join-room': (data: { roomId: string; encryptionKey: string }) => void; - 'send-encrypted-data': (data: { - roomId: string; - encryptedData: string; - }) => void; - 'leave-room': (data: { roomId: string }) => void; - 'get-room-status': (data: { roomId: string }) => void; - 'get-room-list': () => void; + 'e2ee-request': (payload: IJsBridgeMessagePayload) => void; + 'e2ee-c2c-request': (envelope: IRelayEnvelope) => void; + 'e2ee-c2c-response': (envelope: IRelayEnvelope) => void; } export interface IInterServerEvents { @@ -48,6 +44,12 @@ export interface ISocketData { // Room data structure export interface IRoom { id: string; + // Server-generated key handed to clients on create/join. The OneKey client + // does not consume it, and this server never encrypts with it - payloads are + // relayed as-is. Real end-to-end protection comes from a key the clients + // derive themselves (pairing code + ECDHE shared secret + room user list). + // The server knows the user list but not the secret key material. + // See RoomManager.createRoom(). encryptionKey: string; users: Map; transferDirection?: @@ -61,16 +63,6 @@ export interface IRoom { maxUsers: number; } -// Room list item -export interface IRoomListItem { - roomId: string; - userCount: number; - maxUsers: number; - users: string[]; - createdAt: string; - lastActivity: string; -} - // User information export interface IE2EESocketUserInfo { id: string; @@ -83,14 +75,6 @@ export interface IE2EESocketUserInfo { appDeviceName: string; } -// Encrypted message structure -export interface IEncryptedMessage { - encryptedData: string; - senderId: string; - timestamp: number; - roomId: string; -} - // Room configuration export interface IRoomConfig { maxUsers: number; @@ -101,17 +85,9 @@ export interface IRoomConfig { // Server configuration export interface IServerConfig { port: number; - corsOrigins: string[]; roomConfig: IRoomConfig; } -// API response type -export interface IApiResponse { - success: boolean; - data?: T; - error?: string; -} - export interface IE2EEServerApi { roomManager: RoomManager; } diff --git a/packages/transfer-server/src/utils/RemoteApiProxyBase.ts b/packages/transfer-server/src/utils/RemoteApiProxyBase.ts index d3d1350..02852dc 100644 --- a/packages/transfer-server/src/utils/RemoteApiProxyBase.ts +++ b/packages/transfer-server/src/utils/RemoteApiProxyBase.ts @@ -1,21 +1,19 @@ -/* eslint-disable no-restricted-syntax */ -/* eslint-disable @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access */ - import { isMethodAllowed } from '../decorators/e2eeApiMethod'; import { E2eeError, E2eeErrorCode } from '../errors'; import type { IRoomManagerContext } from '../roomManager'; import type { IJsonRpcRequest } from '@onekeyfe/cross-inpage-provider-types'; -export function buildCallRemoteApiMethod( +export function buildCallRemoteApiMethod< + T extends IJsonRpcRequest & { module?: string }, +>( moduleGetter: (module: any) => Promise, remoteApiType: 'e2eeServerApi', context: IRoomManagerContext, ) { return async function callRemoteApiMethod(message: T) { const { method, params = [] } = message; - // @ts-ignore - const module = message?.module as any; + const module = message?.module; if (!module) { throw new E2eeError(E2eeErrorCode.MODULE_REQUIRED, 'callRemoteApiMethod ERROR: module is required'); } @@ -29,10 +27,8 @@ export function buildCallRemoteApiMethod( ); } - // eslint-disable-next-line @typescript-eslint/no-unsafe-call const result = await moduleInstance[method]( - // @ts-ignore - ...[].concat(params as any[]), + ...(Array.isArray(params) ? params : [params]), context, ); return result; @@ -69,7 +65,6 @@ abstract class RemoteApiProxyBase { async callRemoteMethod(key: string, ...params: any[]) { this.checkEnvAvailable(); - // eslint-disable-next-line @typescript-eslint/await-thenable await this.checkEnvAvailable(); // make this method to promise, so that background won't crash if error occurs diff --git a/packages/transfer-server/src/utils/bufferUtils.ts b/packages/transfer-server/src/utils/bufferUtils.ts index 5dd3522..74d7bc2 100644 --- a/packages/transfer-server/src/utils/bufferUtils.ts +++ b/packages/transfer-server/src/utils/bufferUtils.ts @@ -1,5 +1,3 @@ -/* eslint-disable no-restricted-syntax */ - import { bytesToHex as bytesToHex0, hexToBytes, @@ -17,7 +15,6 @@ function toBuffer( ): Buffer { if (isString(data)) { if (encoding === 'hex') { - // eslint-disable-next-line no-param-reassign data = hexUtils.stripHexPrefix(data); } // buffer from hex string in default diff --git a/packages/transfer-server/src/utils/cacheUtils.ts b/packages/transfer-server/src/utils/cacheUtils.ts index 88fd438..9ef22c1 100644 --- a/packages/transfer-server/src/utils/cacheUtils.ts +++ b/packages/transfer-server/src/utils/cacheUtils.ts @@ -5,7 +5,6 @@ import cache from 'memoizee'; export type IMemoizeeOptions = cache.Options; export const memoizee: typeof cache = (f, options) => { - // eslint-disable-next-line @typescript-eslint/unbound-method let { normalizer } = options ?? {}; if (!normalizer) { normalizer = (...args) => { diff --git a/packages/transfer-server/src/utils/cryptoUtils.ts b/packages/transfer-server/src/utils/cryptoUtils.ts index a1148d3..4f076be 100644 --- a/packages/transfer-server/src/utils/cryptoUtils.ts +++ b/packages/transfer-server/src/utils/cryptoUtils.ts @@ -1,4 +1,3 @@ -/* eslint-disable no-restricted-syntax */ import crypto from 'crypto'; // TODO use node native module @@ -246,7 +245,6 @@ export default class CryptoUtils { let result = 0; for (let i = 0; i < a.length; i += 1) { - // eslint-disable-next-line no-bitwise result |= a.charCodeAt(i) ^ b.charCodeAt(i); } diff --git a/packages/transfer-server/src/utils/logger.ts b/packages/transfer-server/src/utils/logger.ts index f918dea..cc92f0a 100644 --- a/packages/transfer-server/src/utils/logger.ts +++ b/packages/transfer-server/src/utils/logger.ts @@ -6,6 +6,12 @@ import type { DestinationStream, Logger } from 'pino'; const LOG_LEVEL = process.env.LOG_LEVEL || 'info'; +/** Bound client-controlled log fields before synchronous output. */ +export function capForLog(value: unknown, max = 64): string | null { + if (typeof value !== 'string') return null; + return value.length > max ? `${value.slice(0, max)}...(${value.length})` : value; +} + // Pretty output is opt-in and only meant for local development: // `pino-pretty` is a devDependency and must never be a hard requirement at runtime. const LOG_PRETTY = @@ -13,7 +19,7 @@ const LOG_PRETTY = function createPrettyStream(): DestinationStream | undefined { try { - // eslint-disable-next-line @typescript-eslint/no-var-requires, global-require, @typescript-eslint/no-unsafe-assignment + // eslint-disable-next-line @typescript-eslint/no-require-imports -- Load the optional development dependency lazily. const pretty = require('pino-pretty') as ( options: Record, ) => DestinationStream; diff --git a/packages/transfer-server/src/utils/stringUtils.ts b/packages/transfer-server/src/utils/stringUtils.ts index 443b7f8..d9ea26b 100644 --- a/packages/transfer-server/src/utils/stringUtils.ts +++ b/packages/transfer-server/src/utils/stringUtils.ts @@ -1,4 +1,3 @@ -/* eslint-disable no-restricted-syntax */ import { E2eeError, E2eeErrorCode } from '../errors'; function addSeparatorToString({ str, diff --git a/packages/transfer-server/src/utils/timerUtils.ts b/packages/transfer-server/src/utils/timerUtils.ts index 623f906..0494683 100644 --- a/packages/transfer-server/src/utils/timerUtils.ts +++ b/packages/transfer-server/src/utils/timerUtils.ts @@ -47,7 +47,6 @@ const timeout = (p: Promise, ms: number, message?: string) => p.then((value) => resolve(value)).catch((err) => reject(err)); }); -// eslint-disable-next-line @typescript-eslint/no-unused-vars const sleepUntil = ({ conditionFn, until, diff --git a/packages/transfer-server/test/relay-compatibility.ts b/packages/transfer-server/test/relay-compatibility.ts new file mode 100644 index 0000000..b7c4681 --- /dev/null +++ b/packages/transfer-server/test/relay-compatibility.ts @@ -0,0 +1,454 @@ +// Load the same browser-global shims as the production server before the bridge. +import '../src/utils/nodeCompat'; + +import assert from 'node:assert/strict'; +import { createServer } from 'node:http'; +import { test } from 'node:test'; + +import { Server } from 'socket.io'; +import { io } from 'socket.io-client'; + +import { e2eeServerApiSetup } from '../src/e2eeServerApi'; +import { E2eeErrorCode } from '../src/errors'; +import { RoomManager } from '../src/roomManager'; +import { CHUNK_PACKET_BYTES, CHUNK_REQUESTS_PER_SECOND, RELAY_RESPONSES_PER_SECOND } from '../src/relayPolicy'; + +import type { Socket } from 'socket.io-client'; + +type IPacket = { + id: number; + type: string; + scope?: string; + remoteId?: string | number | null; + peerOrigin?: string; + error?: { code: number; message: string; stack?: string }; + data?: unknown; +}; + +const ROOM_TIMEOUT = 60_000; +const appInfo = { + appPlatform: 'compatibility-test', appPlatformName: 'compatibility-test', + appVersion: '1.0.0', appBuildNumber: '1', appDeviceName: 'synthetic-peer', +}; + +function receive(socket: Socket, event: string, id: number): Promise { + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + socket.off(event, listener); + reject(new Error(`Timed out waiting for ${event}, id ${id}`)); + }, 5000); + const listener = (packet: IPacket) => { + if (packet.id === id) { + clearTimeout(timer); + socket.off(event, listener); + resolve(packet); + } + }; + socket.on(event, listener); + }); +} + +test('relay compatibility over real Socket.IO with a controlled activity clock', async (t) => { + // Only Date is mocked: HTTP, Socket.IO, RPC delays, and timeout guards use + // real timers. Advancing this clock never waits for a one-hour session. + let now = 1_700_000_000_000; + t.mock.timers.enable({ apis: ['Date'], now }); + const advance = (ms: number) => { + now += ms; + t.mock.timers.setTime(now); + }; + + const httpServer = createServer(); + // Keep transport heartbeats beyond the simulated timeline so these tests + // isolate RoomManager's idle TTL rather than the client's ping deadline. + const socketServer = new Server(httpServer, { pingInterval: 3_600_000, pingTimeout: 3_600_000, maxHttpBufferSize: 10 * 1024 * 1024 }); + const roomConfig = { maxUsers: 2, roomTimeout: ROOM_TIMEOUT, maxMessageSize: 10 * 1024 * 1024 }; + const manager = new RoomManager({ config: roomConfig, socketServer }); + socketServer.on('connection', (socketClient) => { + e2eeServerApiSetup({ socketClient, roomManager: manager }); + socketClient.on("disconnect", () => { void manager.leaveRoomBySocket(socketClient); }); + // A test-only barrier confirms all preceding synchronous relay handlers + // ran, including intentionally silent drops. It changes no production API. + socketClient.on('test-barrier', (ack: () => void) => ack()); + }); + await new Promise((resolve) => httpServer.listen(0, '127.0.0.1', resolve)); + const address = httpServer.address(); + assert(address && typeof address !== 'string'); + const url = `http://127.0.0.1:${address.port}`; + const sockets: Socket[] = []; + + t.after(async () => { + sockets.forEach((socket) => socket.disconnect()); + manager.destroy(); + await new Promise((resolve) => socketServer.close(() => resolve())); + }); + + const connect = async () => { + const socket = io(url, { transports: ['websocket'], reconnection: false }); + sockets.push(socket); + await new Promise((resolve, reject) => { + socket.once('connect', resolve); + socket.once('connect_error', reject); + }); + return socket; + }; + const [a, b, outsider] = await Promise.all([connect(), connect(), connect()]); + let sequence = 100; + const request = (method: string, params: unknown[] = [], module = 'roomManager'): IPacket => ({ + id: sequence++, type: 'REQUEST', data: { module, method, params }, + }); + const call = async (socket: Socket, method: string, params: unknown[] = []) => { + const packet = request(method, params); + const reply = receive(socket, 'e2ee-response', packet.id); + socket.emit('e2ee-request', packet); + return reply; + }; + const createSession = async () => { + const created = await call(a, 'createRoom'); + assert.equal(created.error, undefined); + const { roomId } = created.data as { roomId: string }; + const joined = await Promise.all([ + call(a, 'joinRoomAfterCreate', [{ roomId, ...appInfo }]), + call(b, 'joinRoom', [{ roomId, ...appInfo }]), + ]); + joined.forEach((reply) => assert.equal(reply.error, undefined)); + return roomId; + }; + const barrier = (socket: Socket) => socket.timeout(5000).emitWithAck('test-barrier'); + const relay = async (sender: Socket, recipient: Socket, roomId: string, event: string, packet: IPacket) => { + const delivered = receive(recipient, event, packet.id); + sender.emit(event, { roomId, payload: packet }); + assert.deepEqual(await delivered, packet); + }; + // Invoke the actual scheduled cleanup body deterministically. No mocked + // membership map or fake cleanup implementation can mask expiry regressions. + const cleanup = () => { + const cleanupExpiredRooms = Reflect.get(manager, 'cleanupExpiredRooms') as () => void; + cleanupExpiredRooms.call(manager); + }; + let roomId = await createSession(); + + await t.test('pairing queries may overlap with CLI polling, while floods remain bounded', async () => { + for (let poll = 0; poll < 3; poll += 1) { + const replies = await Promise.all([ + call(a, 'getRoomUsers', [{ roomId }]), + call(a, 'getRoomUsers', [{ roomId }]), + call(a, 'getRoomUsers', [{ roomId }]), + ]); + replies.forEach((reply) => { + assert.equal(reply.error, undefined, 'poll, key derivation, and UI queries can coincide'); + assert.equal((reply.data as unknown[]).length, 2); + }); + advance(1000); + } + advance(2000); + const burst = await Promise.all(Array.from({ length: 11 }, () => call(a, 'getRoomUsers', [{ roomId }]))); + assert.equal(burst.filter((reply) => !reply.error).length, 10); + assert.equal(burst.filter((reply) => reply.error?.code === E2eeErrorCode.RATE_LIMIT_EXCEEDED).length, 1); + advance(199); + assert.equal((await call(a, 'getRoomUsers', [{ roomId }])).error?.code, E2eeErrorCode.RATE_LIMIT_EXCEEDED); + advance(1); + assert.equal((await call(a, 'getRoomUsers', [{ roomId }])).error, undefined, 'one token refills every 200ms'); + const privateRoom = await call(outsider, 'getRoomUsers', [{ roomId }]); + const missingRoom = await call(outsider, 'getRoomUsers', [{ roomId: 'missing-room' }]); + assert.equal(privateRoom.error, undefined); + assert.equal(missingRoom.error, undefined); + assert.deepEqual(privateRoom.data, []); + assert.deepEqual(privateRoom.data, missingRoom.data); + }); + + await t.test('concurrent C2S and rejected C2C requests with the same id keep separate error channels', async () => { + advance(5000); + const sharedId = sequence++; + const c2s: IPacket = { + ...request('joinRoom', [{ roomId: 'invalid-room-id', ...appInfo }]), + id: sharedId, scope: 'c2s-scope', remoteId: 'c2s-remote', peerOrigin: 'c2s-origin', + }; + const c2c: IPacket = { + ...request('sendTransferChunk', [{ transferId: 'invalid-chunk', index: 0, data: '' }], 'api'), + id: sharedId, scope: 'c2c-scope', remoteId: 'c2c-remote', peerOrigin: 'c2c-origin', + }; + const c2sReplies: IPacket[] = []; + const c2cReplies: IPacket[] = []; + const collectC2s = (packet: IPacket) => c2sReplies.push(packet); + const collectC2c = (packet: IPacket) => c2cReplies.push(packet); + a.on('e2ee-response', collectC2s); + a.on('e2ee-c2c-response', collectC2c); + try { + const serverReply = receive(a, 'e2ee-response', sharedId); + const peerReply = receive(a, 'e2ee-c2c-response', sharedId); + a.emit('e2ee-request', c2s); + a.emit('e2ee-c2c-request', { roomId, payload: c2c }); + const replies = await Promise.all([serverReply, peerReply]); + for (const [index, response] of replies.entries()) { + const original = index === 0 ? c2s : c2c; + assert.equal(response.id, sharedId); + assert.equal(response.type, 'RESPONSE'); + assert.equal(response.scope, original.scope); + assert.equal(response.remoteId, original.remoteId); + assert.equal(response.peerOrigin, index === 0 ? 'e2ee-server' : original.peerOrigin); + assert.equal(response.error?.stack, undefined); + } + assert.equal(replies[0].error?.code, E2eeErrorCode.INVALID_ROOM_ID); + assert.equal(replies[1].error?.code, E2eeErrorCode.INVALID_PARAMETER); + await barrier(a); + assert.equal(c2sReplies.length, 1); + assert.equal(c2cReplies.length, 1); + } finally { + a.off('e2ee-response', collectC2s); + a.off('e2ee-c2c-response', collectC2c); + } + const zeroReply = receive(a, 'e2ee-c2c-response', 0); + a.emit('e2ee-c2c-request', { roomId, payload: { ...c2c, id: 0 } }); + assert.equal((await zeroReply).id, 0, 'a rejected C2C request preserves even a zero id'); + }); + + await t.test('ordinary C2C method throttling also responds on the C2C channel', async () => { + const accepted = request('legacyTransferMethod', ['synthetic-data'], 'api'); + await relay(a, b, roomId, 'e2ee-c2c-request', accepted); + const blocked = { ...accepted, id: sequence++ }; + const response = receive(a, 'e2ee-c2c-response', blocked.id); + a.emit('e2ee-c2c-request', { roomId, payload: blocked }); + const rejected = await response; + assert.equal(rejected.id, blocked.id); + assert.equal(rejected.error?.code, E2eeErrorCode.RATE_LIMIT_EXCEEDED); + assert.equal(rejected.error?.stack, undefined); + }); + + await t.test('all chunk envelope levels are bounded while valid maximum chunks and legacy payloads pass', async () => { + const padding = 'A'.repeat(9 * 1024 * 1024); + const seen: IPacket[] = []; + const collect = (packet: IPacket) => seen.push(packet); + b.on('e2ee-c2c-request', collect); + try { + for (const level of ['envelope', 'payload', 'rpc'] as const) { + advance(2000); + const packet = request('sendTransferChunk', [{ transferId: 'size-check', index: 0, data: 'AAAA' }], 'api'); + const envelope = { roomId, payload: packet }; + const target = level === 'envelope' ? envelope : level === 'payload' ? packet : packet.data; + Object.assign(target as object, { padding }); + const response = receive(a, 'e2ee-c2c-response', packet.id); + a.emit('e2ee-c2c-request', envelope); + const rejected = await response; + assert.equal(rejected.error?.code, E2eeErrorCode.INVALID_PARAMETER); + assert.equal(rejected.error?.stack, undefined); + } + await barrier(b); + assert.equal(seen.length, 0, 'padding never reaches the peer'); + advance(2000); + const maximum = request('sendTransferChunk', [{ transferId: 'size-check', index: 1023, data: 'A'.repeat(64 * 1024) }], 'api'); + await relay(a, b, roomId, 'e2ee-c2c-request', maximum); + const outOfRange = request('sendTransferChunk', [{ transferId: 'size-check', index: 1024, data: 'AAAA' }], 'api'); + const rangeReply = receive(a, 'e2ee-c2c-response', outOfRange.id); + a.emit('e2ee-c2c-request', { roomId, payload: outOfRange }); + assert.equal((await rangeReply).error?.code, E2eeErrorCode.INVALID_PARAMETER); + advance(2000); + await relay(a, b, roomId, 'e2ee-c2c-request', request('sendTransferData', [{ rawData: padding }], 'api')); + } finally { b.off('e2ee-c2c-request', collect); } + }); + + await t.test('request and response events cannot exchange payload types', async () => { + advance(2000); + const seen: IPacket[] = []; + const collect = (packet: IPacket) => seen.push(packet); + b.on('e2ee-c2c-request', collect); + b.on('e2ee-c2c-response', collect); + a.emit('e2ee-c2c-response', { roomId, payload: request('sendTransferChunk', [{ index: 1024, data: 'AAAA', transferId: 'bypass' }], 'api') }); + a.emit('e2ee-c2c-response', { roomId, payload: { type: 'RESPONSE', data: 'missing-id' } }); + a.emit('e2ee-c2c-request', { roomId, payload: { ...request('cancelTransfer', [], 'api'), type: 'RESPONSE' } }); + await barrier(a); + await barrier(b); + assert.equal(seen.length, 0); + b.off('e2ee-c2c-request', collect); + b.off('e2ee-c2c-response', collect); + }); + + await t.test('512 full chunk envelopes plus manifest, finish and ACKs fit one traffic window', async () => { + advance(5000); + const replies: IPacket[] = []; + const chunks: number[] = []; + const collect = (packet: IPacket) => replies.push(packet); + const acknowledge = (packet: IPacket) => { + const rpc = packet.data as { method: string; params: Array<{ index: number }> }; + if (rpc.method === 'sendTransferChunk') chunks.push(rpc.params[0].index); + b.emit('e2ee-c2c-response', { roomId, payload: { id: packet.id, type: 'RESPONSE', data: { accepted: true } } }); + }; + a.on('e2ee-c2c-response', collect); + b.on('e2ee-c2c-request', acknowledge); + try { + a.emit('e2ee-c2c-request', { roomId, payload: request('beginChunkedTransfer', [{ transferId: 'burst', totalBytes: 512 * 65536 }], 'api') }); + for (let index = 0; index < CHUNK_REQUESTS_PER_SECOND; index += 1) { + const envelope = { roomId, payload: request('sendTransferChunk', [{ transferId: 'burst', index, data: 'A'.repeat(65536) }], 'api'), padding: '' }; + envelope.padding = 'A'.repeat(CHUNK_PACKET_BYTES - Buffer.byteLength(JSON.stringify(envelope))); + a.emit('e2ee-c2c-request', envelope); + } + const finish = request('finishChunkedTransfer', [{ transferId: 'burst' }], 'api'); + const done = receive(a, 'e2ee-c2c-response', finish.id); + a.emit('e2ee-c2c-request', { roomId, payload: finish }); + await done; + assert.equal(chunks.length, CHUNK_REQUESTS_PER_SECOND); + assert.equal(replies.length, CHUNK_REQUESTS_PER_SECOND + 2); + assert.equal(replies.some((packet) => packet.error), false); + assert.equal(a.connected, true); + assert.equal(b.connected, true); + } finally { + a.off('e2ee-c2c-response', collect); + b.off('e2ee-c2c-request', acknowledge); + } + }); + + await t.test('string, numeric and null remote IDs preserve both RPC and relay envelopes', async () => { + advance(5000); + for (const remoteId of ['remote', 42, 1.5, null]) { + const packet = { ...request('getRoomUsers', [{ roomId }]), remoteId }; + const response = receive(a, 'e2ee-response', packet.id); + a.emit('e2ee-request', packet); + assert.equal((await response).remoteId, remoteId); + await relay(a, b, roomId, 'e2ee-c2c-request', { ...request('cancelTransfer', [], 'api'), remoteId }); + await relay(b, a, roomId, 'e2ee-c2c-response', { id: sequence++, type: 'RESPONSE', remoteId, data: true }); + } + }); + + await t.test('one-way rejection never emits an uncorrelated response', async () => { + advance(5000); + const replies: IPacket[] = []; + const collect = (packet: IPacket) => replies.push(packet); + a.on('e2ee-response', collect); + a.on('e2ee-c2c-response', collect); + try { + a.emit('e2ee-c2c-request', { roomId, payload: { type: 'REQUEST', data: { module: 'api', method: 'sendTransferChunk', params: [] } } }); + for (let index = 0; index < 11; index += 1) { + a.emit('e2ee-request', { type: 'REQUEST', data: { module: 'roomManager', method: 'getRoomUsers', params: [{ roomId }] } }); + } + await barrier(a); + assert.equal(replies.length, 0); + } finally { a.off('e2ee-response', collect); a.off('e2ee-c2c-response', collect); } + }); + + await t.test('complex but correlatable packets return small errors only to the authorized caller', async () => { + advance(5000); + let extra: unknown = 'small'; + for (let index = 0; index < 70; index += 1) extra = { nested: extra }; + const packet = request('cancelTransfer', [], 'api'); + const reply = receive(a, 'e2ee-c2c-response', packet.id); + a.emit('e2ee-c2c-request', { roomId, payload: packet, extra }); + assert.equal((await reply).error?.code, E2eeErrorCode.INVALID_PARAMETER); + const responseId = sequence++; + const rejectedResponse = receive(a, 'e2ee-c2c-response', responseId); + b.emit('e2ee-c2c-response', { roomId, payload: { id: responseId, type: 'RESPONSE', data: extra } }); + assert.equal((await rejectedResponse).error?.code, E2eeErrorCode.INVALID_PARAMETER); + const unauthorized: IPacket[] = []; + const collect = (value: IPacket) => unauthorized.push(value); + outsider.on('e2ee-c2c-response', collect); + outsider.emit('e2ee-c2c-request', { roomId, payload: request('cancelTransfer', [], 'api'), extra }); + await barrier(outsider); + assert.equal(unauthorized.length, 0); + outsider.off('e2ee-c2c-response', collect); + assert.equal(a.connected, true); + assert.equal(b.connected, true); + }); + + await t.test('legacy requests, transfer chunks, and peer responses each extend idle TTL', async () => { + let lastAcceptedAt = now; + const events: Array<[string, IPacket]> = [ + ['e2ee-c2c-request', request('sendTransferData', ['synthetic-data'], 'api')], + ['e2ee-c2c-request', request('sendTransferChunk', [{ transferId: 'active-chunk', index: 0, data: 'AAAA' }], 'api')], + ['e2ee-c2c-response', { id: sequence++, type: 'RESPONSE', data: 'synthetic-ack' }], + ]; + for (const [event, packet] of events) { + advance(lastAcceptedAt + ROOM_TIMEOUT * 0.75 - now); + await relay(a, b, roomId, event, packet); + lastAcceptedAt = now; + advance(ROOM_TIMEOUT * 0.5); + cleanup(); + assert(manager.isUserInRoom(roomId, a.id!).isInRoom, `${event} keeps an active session alive across its old expiry`); + } + advance(lastAcceptedAt + ROOM_TIMEOUT + 1 - now); + cleanup(); + assert.equal(manager.isUserInRoom(roomId, a.id!).isInRoom, false, 'genuinely idle rooms still expire'); + }); + + for (const kind of ['non-member', 'malformed-envelope', 'invalid-chunk', 'throttled-request'] as const) { + await t.test(`${kind} traffic cannot keep a room alive`, async () => { + advance(5000); + roomId = await createSession(); + const limited = request('same-method', [], 'api'); + if (kind === 'throttled-request') { + // A supported idle timeout shorter than the method throttle makes a + // rejection occur near expiry without mutating private room state. + roomConfig.roomTimeout = 1000; + await relay(a, b, roomId, 'e2ee-c2c-request', limited); + } + const idleTimeout = kind === 'throttled-request' ? 1000 : ROOM_TIMEOUT; + const before = now; + const peerMessages: IPacket[] = []; + const collect = (packet: IPacket) => peerMessages.push(packet); + b.on('e2ee-c2c-request', collect); + b.on('e2ee-c2c-response', collect); + try { + advance(idleTimeout - 1); + let sender = a; + if (kind === 'non-member') { + sender = outsider; + // Socket.IO membership alone is insufficient; RoomManager must also + // authorize the sender, even if an adapter still lists this socket. + await socketServer.sockets.sockets.get(outsider.id!)!.join(roomId); + sender.emit('e2ee-c2c-request', { roomId, payload: request('outsider', [], 'api') }); + sender.emit('e2ee-c2c-response', { roomId, payload: { id: sequence++, type: 'RESPONSE', data: 'forged' } }); + } else if (kind === 'malformed-envelope') { + sender.emit('e2ee-c2c-request', { roomId, payload: { id: sequence++, type: 'REQUEST', data: {} } }); + sender.emit('e2ee-c2c-response', { roomId, payload: { id: sequence++, type: 'BOGUS' } }); + } else { + const packet = kind === 'invalid-chunk' + ? request('sendTransferChunk', [{ transferId: 'invalid-chunk', index: -1, data: 'AAAA' }], 'api') + : { ...limited, id: sequence++ }; + const response = receive(sender, 'e2ee-c2c-response', packet.id); + sender.emit('e2ee-c2c-request', { roomId, payload: packet }); + assert.equal((await response).error?.code, kind === 'invalid-chunk' + ? E2eeErrorCode.INVALID_PARAMETER : E2eeErrorCode.RATE_LIMIT_EXCEEDED); + } + await barrier(sender); + await barrier(b); + assert.equal(peerMessages.length, 0); + advance(before + idleTimeout + 1 - now); + cleanup(); + assert.equal(manager.isUserInRoom(roomId, a.id!).isInRoom, false, `${kind} must not renew idle TTL`); + } finally { + b.off('e2ee-c2c-request', collect); + b.off('e2ee-c2c-response', collect); + roomConfig.roomTimeout = ROOM_TIMEOUT; + } + }); + } + for (const mode of ['oversized', 'flood'] as const) { + await t.test(`response ${mode} is bounded without losing a recoverable RPC`, async () => { + advance(5000); + const [sender, recipient] = await Promise.all([connect(), connect()]); + const created = await call(sender, 'createRoom'); + const isolatedRoom = (created.data as { roomId: string }).roomId; + await call(sender, 'joinRoomAfterCreate', [{ roomId: isolatedRoom, ...appInfo }]); + await call(recipient, 'joinRoom', [{ roomId: isolatedRoom, ...appInfo }]); + const received: IPacket[] = []; + recipient.on('e2ee-c2c-response', (packet: IPacket) => received.push(packet)); + const disconnected = new Promise((resolve) => sender.once('disconnect', () => resolve())); + const count = mode === 'flood' ? RELAY_RESPONSES_PER_SECOND + 8 : 1; + for (let index = 0; index < count; index += 1) { + sender.emit('e2ee-c2c-response', { roomId: isolatedRoom, payload: { + id: sequence++, type: 'RESPONSE', data: mode === 'oversized' ? 'A'.repeat(300 * 1024) : 'synthetic-ack', + } }); + } + if (mode === 'flood') await disconnected; + else await barrier(sender); + await barrier(recipient); + assert.equal(received.length, mode === 'flood' ? RELAY_RESPONSES_PER_SECOND : 1); + if (mode === 'oversized') { + assert.equal(sender.connected, true); + assert.equal(received[0].error?.code, E2eeErrorCode.INVALID_PARAMETER); + assert.ok(Buffer.byteLength(JSON.stringify(received[0])) < 1024); + } + assert.equal(recipient.connected, true); + assert.equal((await call(recipient, 'getRoomUsers', [{ roomId: isolatedRoom }])).error, undefined); + }); + } + +}); diff --git a/packages/transfer-server/test/relay-policy.ts b/packages/transfer-server/test/relay-policy.ts new file mode 100644 index 0000000..b4f9ada --- /dev/null +++ b/packages/transfer-server/test/relay-policy.ts @@ -0,0 +1,68 @@ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; + +import { + CHUNK_PACKET_BYTES, RELAY_BYTES_PER_SECOND, RELAY_MESSAGES_PER_SECOND, RelayTrafficBudget, + TRANSFER_MAX_BYTES, TRANSFER_CHUNK_BYTES, TRANSFER_MAX_CHUNKS, measureJsonBytes, +} from '../src/relayPolicy'; +import { capForLog } from '../src/utils/logger'; + +test('packet accounting includes JSON escaping, UTF-8, keys, arrays, and outer metadata', () => { + const packet = { roomId: 'room', payload: { origin: 'https://example.test', data: ['中文', '\u0000"\\\n', 123, true, null, { field: 'value' }], omitted: undefined } }; + const bytes = Buffer.byteLength(JSON.stringify(packet)); + assert.equal(measureJsonBytes(packet, bytes), bytes); + assert.equal(measureJsonBytes(packet, bytes - 1), undefined); + assert.equal(measureJsonBytes({ padding: 'A'.repeat(9 * 1024 * 1024) }, CHUNK_PACKET_BYTES), undefined); + assert.equal(measureJsonBytes({ binary: Buffer.alloc(10) }, 1000), undefined); + const circular: { self?: unknown } = {}; + circular.self = circular; + assert.equal(measureJsonBytes(circular, 1000), undefined); +}); + +test('request and response bytes share one budget, with a fresh window after idle', (t) => { + t.mock.timers.enable({ apis: ['Date'], now: 1_700_000_000_000 }); + const budget = new RelayTrafficBudget(); + assert.equal(budget.consume(RELAY_BYTES_PER_SECOND / 3, false, RELAY_BYTES_PER_SECOND), true); + assert.equal(budget.consume(RELAY_BYTES_PER_SECOND / 3, true, RELAY_BYTES_PER_SECOND), true); + assert.equal(budget.consume(RELAY_BYTES_PER_SECOND / 3, false, RELAY_BYTES_PER_SECOND), true); + assert.equal(budget.consume(RELAY_BYTES_PER_SECOND / 3, true, RELAY_BYTES_PER_SECOND), false); + t.mock.timers.tick(1000); + assert.equal(budget.consume(RELAY_BYTES_PER_SECOND / 3, false, RELAY_BYTES_PER_SECOND), true); +}); + +test('mixed request/response traffic cannot bypass the aggregate message budget', () => { + const budget = new RelayTrafficBudget(); + for (let index = 0; index < RELAY_MESSAGES_PER_SECOND; index += 1) { + assert.equal(budget.consume(1, index % 2 === 0, RELAY_BYTES_PER_SECOND), true); + } + assert.equal(budget.consume(1, false, RELAY_BYTES_PER_SECOND), false); +}); + +test('v1 count derives from the wire-size limits and log fields stay bounded', () => { + assert.equal(TRANSFER_MAX_CHUNKS, 1024); + assert.equal(TRANSFER_MAX_CHUNKS * TRANSFER_CHUNK_BYTES, TRANSFER_MAX_BYTES); + assert.equal(capForLog('A'.repeat(10000)), `${'A'.repeat(64)}...(10000)`); + assert.equal(capForLog({ padding: 'unsafe' }), null); +}); + + +test('JSON string accounting matches escaping and Unicode without copying the payload', () => { + const strings = ['"\\\n', String.fromCharCode(0, 1, 8, 9, 10, 12, 13, 31), '中文😀', + String.fromCharCode(0xd800), String.fromCharCode(0xdc00), String.fromCharCode(0xd800, 0xd800, 0xdc00), + 'A'.repeat(9 * 1024 * 1024)]; + let seed = 3431; + for (let sample = 0; sample < 1000; sample += 1) { + let value = ''; + for (let index = 0; index < 32; index += 1) { + seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0; + value += String.fromCharCode(seed & 0xffff); + } + strings.push(value); + } + for (const value of strings) { + const packet = { value }; + const bytes = Buffer.byteLength(JSON.stringify(packet)); + assert.equal(measureJsonBytes(packet, bytes), bytes); + assert.equal(measureJsonBytes(packet, bytes - 1), undefined); + } +}); diff --git a/packages/transfer-server/test/room-lifecycle.ts b/packages/transfer-server/test/room-lifecycle.ts new file mode 100644 index 0000000..4e244d7 --- /dev/null +++ b/packages/transfer-server/test/room-lifecycle.ts @@ -0,0 +1,184 @@ +import assert from 'node:assert/strict'; +import { createServer } from 'node:http'; +import { test } from 'node:test'; + +import { Server } from 'socket.io'; +import { io } from 'socket.io-client'; + +import { E2eeErrorCode } from '../src/errors'; +import { RoomManager } from '../src/roomManager'; + +import type { Socket } from 'socket.io-client'; +import type { IRoom } from '../src/types'; + +function deferred() { + let resolve!: () => void; + const promise = new Promise((done) => { resolve = done; }); + return { promise, resolve }; +} + +test('room admission stays consistent across disconnects and asynchronous adapters', { timeout: 40_000 }, async (t) => { + const http = createServer(); + const server = new Server(http); + const config = { maxUsers: 2, roomTimeout: 60_000, maxMessageSize: 10 * 1024 * 1024 }; + const manager = new RoomManager({ config, socketServer: server }); + server.on('connection', (socket) => { + socket.on('disconnect', () => { void manager.leaveRoomBySocket(socket); }); + }); + await new Promise((resolve) => http.listen(0, '127.0.0.1', resolve)); + const address = http.address(); + assert(address && typeof address !== 'string'); + const clients: Socket[] = []; + const connect = async () => { + const client = io(`http://127.0.0.1:${address.port}`, { transports: ['websocket'], reconnection: false }); + clients.push(client); + await new Promise((resolve, reject) => { + client.once('connect', resolve); + client.once('connect_error', reject); + }); + const socket = server.sockets.sockets.get(client.id!); + assert(socket); + return { client, socket, context: { socketClient: socket } }; + }; + t.after(async () => { + clients.forEach((client) => client.disconnect()); + await new Promise((resolve) => server.close(() => resolve())); + manager.destroy(); + }); + const params = (roomId: string) => ({ roomId, appPlatform: 'test', appPlatformName: 'test', appVersion: '1', appBuildNumber: '1', appDeviceName: 'synthetic' }); + const owner = await connect(); + const { roomId } = await manager.createRoom(); + const ownerJoin = await manager.joinRoom(params(roomId), owner.context); + assert.equal(ownerJoin.maxMessageSize, config.maxMessageSize); + const users = () => manager.getRoomUsers({ roomId }, owner.context); + + await t.test('join metadata is bounded before admission without rejecting absent legacy fields', async () => { + const peer = await connect(); + for (const invalid of [ + { appPlatform: 'A'.repeat(65) }, + { appPlatformName: 'A'.repeat(257) }, + { appVersion: 'A'.repeat(65) }, + { appBuildNumber: 'A'.repeat(65) }, + { appDeviceName: 'A'.repeat(513) }, + { appDeviceName: 42 }, + ]) { + await assert.rejects(manager.joinRoom(Object.assign(params(roomId), invalid), peer.context), { code: E2eeErrorCode.INVALID_PARAMETER }); + } + assert.equal(peer.socket.rooms.has(roomId), false); + const legacyParams = params(roomId); + Reflect.deleteProperty(legacyParams, 'appDeviceName'); + const joined = await manager.joinRoom(legacyParams, peer.context); + assert.equal(joined.success, true); + await manager.leaveRoom({ roomId, userId: joined.userId! }, peer.context); + }); + + await t.test('disconnect during the pre-join delay never adds a member or emits joined', async () => { + const peer = await connect(); + let notifications = 0; + const onJoined = () => { notifications += 1; }; + owner.client.on('user-joined', onJoined); + const joining = manager.joinRoom(params(roomId), peer.context); + const rejection = assert.rejects(joining, { code: E2eeErrorCode.OPERATION_FAILED }); + const disconnected = new Promise((resolve) => peer.socket.once('disconnect', () => resolve())); + peer.client.disconnect(); + await disconnected; + await rejection; + assert.equal((await users()).length, 1); + assert.equal(peer.socket.rooms.has(roomId), false); + assert.equal(notifications, 0); + owner.client.off('user-joined', onJoined); + }); + + await t.test('disconnect releases pending capacity before an adapter resolves', async () => { + const peer = await connect(); + const entered = deferred(); + const release = deferred(); + const originalJoin = peer.socket.join.bind(peer.socket); + peer.socket.join = async (room) => { entered.resolve(); await release.promise; await originalJoin(room); }; + const joining = manager.joinRoom(params(roomId), peer.context); + const rejection = assert.rejects(joining, { code: E2eeErrorCode.OPERATION_FAILED }); + await entered.promise; + const disconnected = new Promise((resolve) => peer.socket.once('disconnect', () => resolve())); + peer.client.disconnect(); + await disconnected; + const replacement = await connect(); + const admitted = await manager.joinRoom(params(roomId), replacement.context); + assert.equal(admitted.success, true, 'the unresolved adapter no longer reserves a slot'); + release.resolve(); + await rejection; + assert.equal(peer.socket.rooms.has(roomId), false); + assert.equal((await users()).length, 2); + await manager.leaveRoom({ roomId, userId: admitted.userId! }, replacement.context); + }); + + await t.test('an adapter rejection rolls back delivery membership and leaves no ghost', async () => { + const peer = await connect(); + const originalJoin = peer.socket.join.bind(peer.socket); + peer.socket.join = async (room) => { await originalJoin(room); throw new Error('Synthetic adapter failure'); }; + await assert.rejects(manager.joinRoom(params(roomId), peer.context), /Synthetic adapter failure/); + assert.equal(peer.socket.rooms.has(roomId), false); + assert.equal((await users()).length, 1); + }); + + await t.test('pending admissions reserve capacity; committed rejoin is idempotent in a full room', async () => { + const [peer, competitor] = await Promise.all([connect(), connect()]); + const entered = deferred(); + const release = deferred(); + const originalJoin = peer.socket.join.bind(peer.socket); + peer.socket.join = async (room) => { entered.resolve(); await release.promise; await originalJoin(room); }; + const joining = manager.joinRoom(params(roomId), peer.context); + await entered.promise; + assert.equal((await users()).length, 1, 'pending entries are not visible to clients'); + await assert.rejects(manager.joinRoom(params(roomId), competitor.context), { code: E2eeErrorCode.CONNECTION_REJECTED }); + release.resolve(); + const admitted = await joining; + const rejoined = await manager.joinRoom(params(roomId), owner.context); + assert.equal(rejoined.userId, ownerJoin.userId); + assert.equal(rejoined.userCount, 2); + await manager.leaveRoom({ roomId, userId: admitted.userId! }, peer.context); + }); + + await t.test('a room removed while the adapter is pending cannot be resurrected', async () => { + const peer = await connect(); + const entered = deferred(); + const release = deferred(); + const originalJoin = peer.socket.join.bind(peer.socket); + peer.socket.join = async (room) => { entered.resolve(); await release.promise; await originalJoin(room); }; + const joining = manager.joinRoom(params(roomId), peer.context); + const rejection = assert.rejects(joining, { code: E2eeErrorCode.OPERATION_FAILED }); + await entered.promise; + await manager.leaveRoom({ roomId, userId: ownerJoin.userId! }, owner.context); + release.resolve(); + await rejection; + assert.deepEqual(await users(), []); + assert.equal(peer.socket.rooms.has(roomId), false); + }); + + await t.test('expiry notifies legacy clients and removes only expired room membership', async () => { + const peer = await connect(); + const expired = await manager.createRoom(); + const [a, b] = await Promise.all([ + manager.joinRoom(params(expired.roomId), owner.context), + manager.joinRoom(params(expired.roomId), peer.context), + ]); + const active = await manager.createRoom(); + await manager.joinRoom(params(active.roomId), owner.context); + const rooms = Reflect.get(manager, 'rooms') as Map; + rooms.get(expired.roomId)!.lastActivity = new Date(0); + const left = (client: Socket) => new Promise<{ roomId: string; userCount: number }>((resolve) => client.once('user-left', resolve)); + const notices = Promise.all([left(owner.client), left(peer.client)]); + const cleanup = Reflect.get(manager, 'cleanupExpiredRooms') as () => void; + cleanup.call(manager); + (await notices).forEach((notice) => assert.deepEqual( + { roomId: notice.roomId, userCount: notice.userCount }, { roomId: expired.roomId, userCount: 0 }, + )); + assert.equal(owner.client.connected, true); + assert.equal(peer.client.connected, true); + assert.equal(owner.socket.rooms.has(expired.roomId), false); + assert.equal(peer.socket.rooms.has(expired.roomId), false); + assert.equal(owner.socket.rooms.has(active.roomId), true); + assert.equal(manager.isUserInRoom(active.roomId, owner.socket.id).isInRoom, true); + assert.deepEqual(await manager.leaveRoom({ roomId: expired.roomId, userId: a.userId! }, owner.context), { success: true, userCount: 0, roomDestroyed: true }); + assert.equal((await manager.leaveRoom({ roomId: expired.roomId, userId: b.userId! }, peer.context)).success, true); + }); +}); diff --git a/packages/transfer-server/test/smoke.ts b/packages/transfer-server/test/smoke.ts index 653a314..334f46f 100644 --- a/packages/transfer-server/test/smoke.ts +++ b/packages/transfer-server/test/smoke.ts @@ -65,11 +65,9 @@ async function startServer(): Promise { if (server.exitCode !== null) { throw new Error(`server exited during startup:\n${output.join('')}`); } - // eslint-disable-next-line no-await-in-loop if ((await health()) === 200) { return server; } - // eslint-disable-next-line no-await-in-loop await wait(250); } @@ -194,7 +192,6 @@ async function deliver( : (received?.data?.result as string); return carried === token; } - // eslint-disable-next-line no-await-in-loop await wait(100); } return false; @@ -206,7 +203,6 @@ async function waitFor(read: () => T | undefined, timeoutMs = 3000): Promise< if (value !== undefined) { return value; } - // eslint-disable-next-line no-await-in-loop await wait(100); } return undefined; @@ -397,16 +393,147 @@ async function main(): Promise { await clientA.call('roomManager', 'joinRoomAfterCreate', [ { roomId: room.roomId, ...appInfo('A') }, ]); - await clientB.call('roomManager', 'joinRoom', [{ roomId: room.roomId, ...appInfo('B') }]); - + const joinedEvents: Array<{ roomId: string; userId: string; userCount: number }> = []; + let joinerNotified = false; + clientA.socket.on('user-joined', (event) => joinedEvents.push(event)); + clientB.socket.on('user-joined', () => { joinerNotified = true; }); + const joined = await clientB.call('roomManager', 'joinRoom', [{ roomId: room.roomId, ...appInfo('B') }]); + check(joined.chunkedTransferVersion === 1, 'relay advertises chunked transfer support'); + + // Reading on the notified socket also waits for its preceding join event. const usersBefore = await clientA.call('roomManager', 'getRoomUsers', [ { roomId: room.roomId }, ]); + check( + joinedEvents.length === 1 && joinedEvents[0].roomId === room.roomId && + joinedEvents[0].userId === joined.userId && joinedEvents[0].userCount === 2, + 'existing member receives the peer join event with room identity', + ); + check(!joinerNotified, 'joining peer does not receive its own join event'); check(usersBefore.length === 2, 'session established', `${usersBefore.length} users in room`); // --- baseline: peers can talk both ways before anything goes wrong --- await checkBidirectional(clientA, clientB, room.roomId, 'before'); + // --- security: a socket that never joined the room must not be able to + // inject c2c traffic into it. The relay emits to the client-supplied + // roomId, so without a membership check any connected socket could push + // cancelTransfer / verifyPairingCode / a forged response into a live + // session it only knows the id of. The outsider knows room.roomId but + // never called joinRoom, so both channels must be dropped, and the two + // real members must be unaffected. --- + const outsider = makeClient('smoke-client-outsider'); + await outsider.ready; + const injectToken = `inject-${Date.now()}`; + const seenBefore = { + aReq: clientA.c2cRequests.length, + bReq: clientB.c2cRequests.length, + aRes: clientA.c2cResponses.length, + bRes: clientB.c2cResponses.length, + }; + outsider.socket.emit('e2ee-c2c-request', { + payload: { + id: Date.now(), + type: 'REQUEST', + data: { module: 'peer', method: `inject_${injectToken}`, params: [injectToken] }, + }, + roomId: room.roomId, + }); + outsider.socket.emit('e2ee-c2c-response', { + payload: { id: Date.now(), type: 'RESPONSE', data: { result: injectToken } }, + roomId: room.roomId, + }); + outsider.socket.emit('e2ee-c2c-request', { + payload: { + id: Date.now() + 1, + type: 'REQUEST', + data: { + module: 'api', method: 'sendTransferChunk', + params: [{ transferId: 'outsider-chunk', index: 0, data: 'AAAA' }], + }, + }, + roomId: room.roomId, + }); + await wait(1000); + const injected = + clientA.c2cRequests.length > seenBefore.aReq || + clientB.c2cRequests.length > seenBefore.bReq || + clientA.c2cResponses.length > seenBefore.aRes || + clientB.c2cResponses.length > seenBefore.bRes; + check( + !injected, + 'non-member cannot inject requests, responses, or transfer chunks', + injected ? 'INJECTED - membership check bypassed' : 'both channels dropped', + ); + check((await health()) === 200, 'server alive after c2c injection attempt'); + await checkBidirectional(clientA, clientB, room.roomId, 'after injection attempt'); + + const privateRoom = await outsider.callRaw('roomManager', 'getRoomUsers', [{ roomId: room.roomId }]); + const missingRoom = await outsider.callRaw('roomManager', 'getRoomUsers', [{ roomId: 'missing-room' }]); + check( + !privateRoom.error && !missingRoom.error && + JSON.stringify(privateRoom.data) === "[]" && JSON.stringify(missingRoom.data) === "[]", + 'private and missing rooms return identical empty lists to non-members', + ); + const queryFlood = await Promise.all(Array.from({ length: 32 }, () => + outsider.callRaw('roomManager', 'getRoomUsers', [{ roomId: room.roomId }]), + )); + check(queryFlood.some((reply) => reply.error?.code === 1100), 'room queries exceeding the burst allowance are rate limited'); + check(queryFlood.every((reply) => reply.error?.stack === undefined), 'rate limit errors omit server stacks'); + for (let poll = 0; poll < 3; poll += 1) { + await wait(1000); + const users = await clientA.callRaw('roomManager', 'getRoomUsers', [{ roomId: room.roomId }]); + check(!users.error && users.data?.length === 2, `one-second member poll ${poll + 1} succeeds`); + } + outsider.socket.disconnect(); + + // More than one chunk must pass the relay without the legacy 3 second limit. + const chunkStart = clientB.c2cRequests.length; + const chunkData = 'A'.repeat(64 * 1024); + for (let index = 0; index < 12; index += 1) { + clientA.socket.emit('e2ee-c2c-request', { + roomId: room.roomId, + payload: { id: 70000 + index, type: 'REQUEST', data: { + module: 'api', method: 'sendTransferChunk', + params: [{ transferId: 'smoke-chunks', index, data: chunkData }], + } }, + }); + } + await wait(500); + const chunks = clientB.c2cRequests.slice(chunkStart); + check(chunks.length === 12, 'consecutive 64 KiB chunks are forwarded', String(chunks.length)); + check(chunks.every((packet, index) => packet.data.params[0].index === index && packet.data.params[0].data === chunkData), 'chunk contents and order remain intact'); + + const invalidStart = clientB.c2cRequests.length; + for (const params of [ + [{ transferId: 'smoke-chunks', index: 0, data: chunkData + 'A' }], + [{ transferId: 'smoke-chunks', index: -1, data: 'AAAA' }], + [{ transferId: 'smoke-chunks', index: 0, data: 'AAAA', extra: 'large' }], + [{ transferId: 'smoke-chunks', index: 0, data: 'AAAA' }, 'extra'], + [{ transferId: 'smoke-chunks', index: 0, data: '????' }], + ]) { + clientA.socket.emit('e2ee-c2c-request', { + roomId: room.roomId, + payload: { id: 71000, type: 'REQUEST', data: { module: 'api', method: 'sendTransferChunk', params } }, + }); + } + await wait(300); + check(clientB.c2cRequests.length === invalidStart, 'oversized and malformed chunks never reach the peer'); + + await wait(1100); + const burstStart = clientB.c2cRequests.length; + for (let index = 0; index < 530; index += 1) { + clientA.socket.emit('e2ee-c2c-request', { + roomId: room.roomId, + payload: { id: 72000 + index, type: 'REQUEST', data: { + module: 'api', method: 'sendTransferChunk', + params: [{ transferId: 'smoke-burst', index, data: 'AAAA' }], + } }, + }); + } + await wait(500); + check(clientB.c2cRequests.length - burstStart === 512, 'chunk floods stay capped at 512 messages per second'); + // --- both clients attack every listener --- console.log( `\nfiring ${MALFORMED_PACKETS.length} malformed packets from each client (${ @@ -507,6 +634,29 @@ async function main(): Promise { check(stillWorks.length === 2, 'session unaffected by oversized-log flood'); logFlooder.socket.disconnect(); + // --- an error response must never carry the server stack trace. It cannot + // be stopped by E2eeError.toJSON(): JsBridgeBase.createPayload() + // replaces payload.error with its own plain copy first, and that copy + // (toPlainError) reads err.stack straight off the instance. So it is + // stripped in sendPayload(), and it has to stay stripped. + // A fresh client keeps this off the rate-limit windows used above. --- + const errorProbe = makeClient('smoke-client-F'); + await errorProbe.ready; + const errorPayload = await errorProbe.callRaw('roomManager', 'joinRoom', [ + { roomId: 'not-a-valid-room-id', ...appInfo('F') }, + ]); + check( + Boolean(errorPayload.error), + 'invalid roomId is rejected with an error', + `code=${String(errorPayload.error?.code)}`, + ); + check( + errorPayload.error?.stack === undefined, + 'error response carries no server stack trace', + errorPayload.error?.stack ? 'LEAKED - server stack sent to client' : 'no stack field', + ); + errorProbe.socket.disconnect(); + const clientC = makeClient('smoke-client-C'); await clientC.ready; check(clientC.socket.connected, 'new client can still connect'); diff --git a/yarn.lock b/yarn.lock index c1925e8..ea5c7ba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15,15 +15,6 @@ __metadata: languageName: node linkType: hard -"@babel/code-frame@npm:7.12.11": - version: 7.12.11 - resolution: "@babel/code-frame@npm:7.12.11" - dependencies: - "@babel/highlight": "npm:^7.10.4" - checksum: 10c0/836ffd155506768e991d6dd8c51db37cad5958ed1c8e0a2329ccd9527165d5c752e943d66a5c3c92ffd45f343419f0742e7636629a529f4fbd5303e3637746b9 - languageName: node - linkType: hard - "@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.27.1": version: 7.27.1 resolution: "@babel/code-frame@npm:7.27.1" @@ -135,7 +126,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-validator-identifier@npm:^7.25.9, @babel/helper-validator-identifier@npm:^7.27.1": +"@babel/helper-validator-identifier@npm:^7.27.1": version: 7.27.1 resolution: "@babel/helper-validator-identifier@npm:7.27.1" checksum: 10c0/c558f11c4871d526498e49d07a84752d1800bf72ac0d3dad100309a2eaba24efbf56ea59af5137ff15e3a00280ebe588560534b0e894a4750f8b1411d8f78b84 @@ -159,18 +150,6 @@ __metadata: languageName: node linkType: hard -"@babel/highlight@npm:^7.10.4": - version: 7.25.9 - resolution: "@babel/highlight@npm:7.25.9" - dependencies: - "@babel/helper-validator-identifier": "npm:^7.25.9" - chalk: "npm:^2.4.2" - js-tokens: "npm:^4.0.0" - picocolors: "npm:^1.0.0" - checksum: 10c0/ae0ed93c151b85a07df42936117fa593ce91563a22dfc8944a90ae7088c9679645c33e00dcd20b081c1979665d65f986241172dae1fc9e5922692fc3ff685a49 - languageName: node - linkType: hard - "@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.23.9, @babel/parser@npm:^7.27.2, @babel/parser@npm:^7.28.0": version: 7.28.0 resolution: "@babel/parser@npm:7.28.0" @@ -412,6 +391,28 @@ __metadata: languageName: node linkType: hard +"@cacheable/memory@npm:^2.2.0": + version: 2.2.0 + resolution: "@cacheable/memory@npm:2.2.0" + dependencies: + "@cacheable/utils": "npm:^2.5.0" + "@keyv/bigmap": "npm:^1.3.1" + hookified: "npm:^1.15.1" + keyv: "npm:^5.6.0" + checksum: 10c0/c74889eb09871be0696065778fff013a498ddc73b538d113c2cb6ece361746f2959fbddfa4ce03ce53c9259998be05949e393212e72413dbd07616744eabd261 + languageName: node + linkType: hard + +"@cacheable/utils@npm:^2.5.0": + version: 2.5.0 + resolution: "@cacheable/utils@npm:2.5.0" + dependencies: + hashery: "npm:^1.5.1" + keyv: "npm:^5.6.0" + checksum: 10c0/99e901accd71eeb0bce91484d6c66d7ef2c731b541561eb5dc9ea7fed78bdc0b65e5dd2a80edde50191143bcfc5f238fc4742475b32e82d43b43eea224038e35 + languageName: node + linkType: hard + "@cspotcode/source-map-support@npm:^0.8.0": version: 0.8.1 resolution: "@cspotcode/source-map-support@npm:0.8.1" @@ -428,38 +429,79 @@ __metadata: languageName: node linkType: hard -"@eslint-community/eslint-utils@npm:^4.2.0": - version: 4.7.0 - resolution: "@eslint-community/eslint-utils@npm:4.7.0" +"@eslint-community/eslint-utils@npm:^4.8.0, @eslint-community/eslint-utils@npm:^4.9.1": + version: 4.10.1 + resolution: "@eslint-community/eslint-utils@npm:4.10.1" dependencies: eslint-visitor-keys: "npm:^3.4.3" peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - checksum: 10c0/c0f4f2bd73b7b7a9de74b716a664873d08ab71ab439e51befe77d61915af41a81ecec93b408778b3a7856185244c34c2c8ee28912072ec14def84ba2dec70adf + checksum: 10c0/b514586655698bc6b74db72a496c77e813c78b63e36a83429845ac7057dd77113b0b6c31e6e590d5baaeee2abae6ea22363a41209bcafa078d895ab83ce83011 languageName: node linkType: hard -"@eslint-community/regexpp@npm:^4.4.0": - version: 4.12.1 - resolution: "@eslint-community/regexpp@npm:4.12.1" - checksum: 10c0/a03d98c246bcb9109aec2c08e4d10c8d010256538dcb3f56610191607214523d4fb1b00aa81df830b6dffb74c5fa0be03642513a289c567949d3e550ca11cdf6 +"@eslint-community/regexpp@npm:^4.12.2": + version: 4.12.2 + resolution: "@eslint-community/regexpp@npm:4.12.2" + checksum: 10c0/fddcbc66851b308478d04e302a4d771d6917a0b3740dc351513c0da9ca2eab8a1adf99f5e0aa7ab8b13fa0df005c81adeee7e63a92f3effd7d367a163b721c2d languageName: node linkType: hard -"@eslint/eslintrc@npm:^0.4.3": - version: 0.4.3 - resolution: "@eslint/eslintrc@npm:0.4.3" +"@eslint/config-array@npm:^0.23.5": + version: 0.23.5 + resolution: "@eslint/config-array@npm:0.23.5" dependencies: - ajv: "npm:^6.12.4" - debug: "npm:^4.1.1" - espree: "npm:^7.3.0" - globals: "npm:^13.9.0" - ignore: "npm:^4.0.6" - import-fresh: "npm:^3.2.1" - js-yaml: "npm:^3.13.1" - minimatch: "npm:^3.0.4" - strip-json-comments: "npm:^3.1.1" - checksum: 10c0/0eed93369f72ef044686d07824742121f9b95153ff34f4614e4e69d64332ee68c84eb70da851a9005bb76b3d1d64ad76c2e6293a808edc0f7dfb883689ca136d + "@eslint/object-schema": "npm:^3.0.5" + debug: "npm:^4.3.1" + minimatch: "npm:^10.2.4" + checksum: 10c0/b24833c4c76e78ee075d306cd3f095db46b2db0f90cc13a6ee6e4275f9889731c05bf5403ab5fefb79c756e07ac9184ed0e04570341382f9eccbccc80e6d1a0c + languageName: node + linkType: hard + +"@eslint/config-helpers@npm:^0.7.0": + version: 0.7.0 + resolution: "@eslint/config-helpers@npm:0.7.0" + dependencies: + "@eslint/core": "npm:^1.2.1" + checksum: 10c0/fd40d57d6f1db49f7b647048b88a433dc7f6522ef3edf855a43cb526ef4fc40622ceed0dc8de2e03d254f30f8e035370570de1d4bd8e7c2b1200131451e0d331 + languageName: node + linkType: hard + +"@eslint/core@npm:^1.2.1": + version: 1.2.1 + resolution: "@eslint/core@npm:1.2.1" + dependencies: + "@types/json-schema": "npm:^7.0.15" + checksum: 10c0/10979b40588ecfef771fcb5013a542a35fb30692cc95a65f3481b0b36fbd89f5679efeb30d57f4eed35203d859aabace2a620177d6c536f71b299a1af2f3398f + languageName: node + linkType: hard + +"@eslint/js@npm:^10.0.1": + version: 10.0.1 + resolution: "@eslint/js@npm:10.0.1" + peerDependencies: + eslint: ^10.0.0 + peerDependenciesMeta: + eslint: + optional: true + checksum: 10c0/9f3fcaf71ba7fdf65d82e8faad6ecfe97e11801cc3c362b306a88ea1ed1344ae0d35330dddb0e8ad18f010f6687a70b75491b9e01c8af57acd7987cee6b3ec6c + languageName: node + linkType: hard + +"@eslint/object-schema@npm:^3.0.5": + version: 3.0.5 + resolution: "@eslint/object-schema@npm:3.0.5" + checksum: 10c0/1db337431f520b99e9edda64ef5fafd7ec6a029843eeb608753025125b6649d861d843cffafafd3c4e37926d7d5f9ec0c6a8e3665c13c3da2144e8132892e92e + languageName: node + linkType: hard + +"@eslint/plugin-kit@npm:^0.7.3": + version: 0.7.3 + resolution: "@eslint/plugin-kit@npm:0.7.3" + dependencies: + "@eslint/core": "npm:^1.2.1" + levn: "npm:^0.4.1" + checksum: 10c0/2daebaedcfbd261478e8b87dfb46d3118f48277c19eb298483ebc6f3b438e9688b86ddca13b3aeb08dfb06be00fb6073506852386aa124645c1170e7e33d140c languageName: node linkType: hard @@ -486,21 +528,44 @@ __metadata: languageName: node linkType: hard -"@humanwhocodes/config-array@npm:^0.5.0": - version: 0.5.0 - resolution: "@humanwhocodes/config-array@npm:0.5.0" +"@humanfs/core@npm:^0.19.2": + version: 0.19.2 + resolution: "@humanfs/core@npm:0.19.2" dependencies: - "@humanwhocodes/object-schema": "npm:^1.2.0" - debug: "npm:^4.1.1" - minimatch: "npm:^3.0.4" - checksum: 10c0/217fac9e03492361825a2bf761d4bb7ec6d10002a10f7314142245eb13ac9d123523d24d5619c3c4159af215c7b3e583ed386108e227014bef4efbf9caca8ccc + "@humanfs/types": "npm:^0.15.0" + checksum: 10c0/d0a1d52d7b30c27d49475a53072d1510b81c5803e44b342fb8faf3887f1aa27593a1e6dc76a45268e7892d3f4e198146659281f6b6d55eacf3fd5a38bac30c5c languageName: node linkType: hard -"@humanwhocodes/object-schema@npm:^1.2.0": - version: 1.2.1 - resolution: "@humanwhocodes/object-schema@npm:1.2.1" - checksum: 10c0/c3c35fdb70c04a569278351c75553e293ae339684ed75895edc79facc7276e351115786946658d78133130c0cca80e57e2203bc07f8fa7fe7980300e8deef7db +"@humanfs/node@npm:^0.16.6": + version: 0.16.8 + resolution: "@humanfs/node@npm:0.16.8" + dependencies: + "@humanfs/core": "npm:^0.19.2" + "@humanfs/types": "npm:^0.15.0" + "@humanwhocodes/retry": "npm:^0.4.0" + checksum: 10c0/56140579db811af4e160b195d45d0f29acf644d192c93fe24c9e594ebf06f19dfc157494a07c84540b8a071c0e4b37209c2362765d31734f4d0be869c2422e25 + languageName: node + linkType: hard + +"@humanfs/types@npm:^0.15.0": + version: 0.15.0 + resolution: "@humanfs/types@npm:0.15.0" + checksum: 10c0/fc26b9a024b0e55f7eaf64036df94345bf5d36d6a41ef80ef38e78f1f7430ce26cf435af736adae58913baae18eac3f38c18739054a3d379102015978eae862e + languageName: node + linkType: hard + +"@humanwhocodes/module-importer@npm:^1.0.1": + version: 1.0.1 + resolution: "@humanwhocodes/module-importer@npm:1.0.1" + checksum: 10c0/909b69c3b86d482c26b3359db16e46a32e0fb30bd306a3c176b8313b9e7313dba0f37f519de6aa8b0a1921349e505f259d19475e123182416a506d7f87e7f529 + languageName: node + linkType: hard + +"@humanwhocodes/retry@npm:^0.4.0, @humanwhocodes/retry@npm:^0.4.2": + version: 0.4.3 + resolution: "@humanwhocodes/retry@npm:0.4.3" + checksum: 10c0/3775bb30087d4440b3f7406d5a057777d90e4b9f435af488a4923ef249e93615fb78565a85f173a186a076c7706a81d0d57d563a2624e4de2c5c9c66c486ce42 languageName: node linkType: hard @@ -821,6 +886,25 @@ __metadata: languageName: node linkType: hard +"@keyv/bigmap@npm:^1.3.1": + version: 1.3.1 + resolution: "@keyv/bigmap@npm:1.3.1" + dependencies: + hashery: "npm:^1.4.0" + hookified: "npm:^1.15.0" + peerDependencies: + keyv: ^5.6.0 + checksum: 10c0/acc6a4a5edf462ce23e95672ab4bfaf7cd1941dff6bf3a2f671ce467961ace1fac9d3eb75a9ed9a8e92012e00a7d8b16ad1677bc539a52c3ad0cec31473e2349 + languageName: node + linkType: hard + +"@keyv/serialize@npm:^1.1.1": + version: 1.1.1 + resolution: "@keyv/serialize@npm:1.1.1" + checksum: 10c0/b0008cae4a54400c3abf587b8cc2474c6f528ee58969ce6cf9cb07a04006f80c73c85971d6be6544408318a2bc40108236a19a82aea0a6de95aae49533317374 + languageName: node + linkType: hard + "@koa/router@npm:^12.0.0": version: 12.0.2 resolution: "@koa/router@npm:12.0.2" @@ -1239,13 +1323,9 @@ __metadata: dependencies: "@types/jest": "npm:^29.0.0" "@types/lodash": "npm:^4.14.200" - "@typescript-eslint/eslint-plugin": "npm:^5.0.0" - "@typescript-eslint/parser": "npm:^5.0.0" - eslint: "npm:^7.32.0" - eslint-config-prettier: "npm:^8.5.0" - eslint-plugin-prettier: "npm:^4.0.0" + eslint: "npm:^10.10.0" jest: "npm:^29.0.0" - lodash: "npm:^4.17.21" + lodash: "npm:^4.18.1" prettier: "npm:^2.8.8" ts-jest: "npm:^29.0.0" typescript: "npm:^5.0.0" @@ -1272,10 +1352,8 @@ __metadata: "@midwayjs/validate": "npm:^3.20.12" "@types/jest": "npm:^29.0.0" "@types/node": "npm:^20.0.0" - "@typescript-eslint/eslint-plugin": "npm:^5.0.0" - "@typescript-eslint/parser": "npm:^5.0.0" cross-env: "npm:^10.0.0" - eslint: "npm:^7.32.0" + eslint: "npm:^10.10.0" jest: "npm:^29.0.0" mongoose: "npm:^8.17.1" nodemon: "npm:^3.0.0" @@ -1288,12 +1366,18 @@ __metadata: version: 0.0.0-use.local resolution: "@onekeyhq/monorepo@workspace:." dependencies: + "@eslint/js": "npm:^10.0.1" "@midwayjs/core": "npm:^3.20.11" "@midwayjs/decorator": "npm:^3.20.11" "@midwayjs/logger": "npm:^3.4.2" "@midwayjs/validate": "npm:^3.20.12" + eslint: "npm:^10.10.0" + eslint-config-prettier: "npm:^10.1.8" + globals: "npm:^17.12.0" mongoose: "npm:^8.17.1" rimraf: "npm:^5.0.5" + typescript: "npm:^5.0.0" + typescript-eslint: "npm:^8.70.0" languageName: unknown linkType: soft @@ -1311,9 +1395,10 @@ __metadata: "@types/node": "npm:^20.0.0" asmcrypto.js: "npm:^2.3.2" cors: "npm:^2.8.5" + eslint: "npm:^10.10.0" express: "npm:^4.18.2" fast-json-stable-stringify: "npm:^2.1.0" - lodash: "npm:^4.17.21" + lodash: "npm:^4.18.1" lru-cache: "npm:^10.0.0" memoizee: "npm:^0.4.15" nanoid: "npm:^5.0.4" @@ -1538,6 +1623,20 @@ __metadata: languageName: node linkType: hard +"@types/esrecurse@npm:^4.3.1": + version: 4.3.1 + resolution: "@types/esrecurse@npm:4.3.1" + checksum: 10c0/90dad74d5da3ad27606d8e8e757322f33171cfeaa15ad558b615cf71bb2a516492d18f55f4816384685a3eb2412142e732bbae9a4a7cd2cf3deb7572aa4ebe03 + languageName: node + linkType: hard + +"@types/estree@npm:^1.0.6, @types/estree@npm:^1.0.8": + version: 1.0.9 + resolution: "@types/estree@npm:1.0.9" + checksum: 10c0/3ad3286ca2988cd550dafb8f2ad599c8474868e954fa601a36655bdfefd8039f7c714b8c1c7f2ae219ffbd58bd4660e66fa7479a0120fc02d4777057d4865387 + languageName: node + linkType: hard + "@types/express-serve-static-core@npm:^4.17.33": version: 4.19.6 resolution: "@types/express-serve-static-core@npm:4.19.6" @@ -1653,7 +1752,7 @@ __metadata: languageName: node linkType: hard -"@types/json-schema@npm:^7.0.9": +"@types/json-schema@npm:^7.0.15": version: 7.0.15 resolution: "@types/json-schema@npm:7.0.15" checksum: 10c0/a996a745e6c5d60292f36731dd41341339d4eeed8180bb09226e5c8d23759067692b1d88e5d91d72ee83dfc00d3aca8e7bd43ea120516c17922cbcb7c3e252db @@ -1789,13 +1888,6 @@ __metadata: languageName: node linkType: hard -"@types/semver@npm:^7.3.12": - version: 7.7.0 - resolution: "@types/semver@npm:7.7.0" - checksum: 10c0/6b5f65f647474338abbd6ee91a6bbab434662ddb8fe39464edcbcfc96484d388baad9eb506dff217b6fc1727a88894930eb1f308617161ac0f376fe06be4e1ee - languageName: node - linkType: hard - "@types/send@npm:*": version: 0.17.5 resolution: "@types/send@npm:0.17.5" @@ -1887,124 +1979,138 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/eslint-plugin@npm:^5.0.0": - version: 5.62.0 - resolution: "@typescript-eslint/eslint-plugin@npm:5.62.0" +"@typescript-eslint/eslint-plugin@npm:8.70.0": + version: 8.70.0 + resolution: "@typescript-eslint/eslint-plugin@npm:8.70.0" dependencies: - "@eslint-community/regexpp": "npm:^4.4.0" - "@typescript-eslint/scope-manager": "npm:5.62.0" - "@typescript-eslint/type-utils": "npm:5.62.0" - "@typescript-eslint/utils": "npm:5.62.0" - debug: "npm:^4.3.4" - graphemer: "npm:^1.4.0" - ignore: "npm:^5.2.0" - natural-compare-lite: "npm:^1.4.0" - semver: "npm:^7.3.7" - tsutils: "npm:^3.21.0" + "@eslint-community/regexpp": "npm:^4.12.2" + "@typescript-eslint/scope-manager": "npm:8.70.0" + "@typescript-eslint/type-utils": "npm:8.70.0" + "@typescript-eslint/utils": "npm:8.70.0" + "@typescript-eslint/visitor-keys": "npm:8.70.0" + ignore: "npm:^7.0.5" + natural-compare: "npm:^1.4.0" + ts-api-utils: "npm:^2.5.0" peerDependencies: - "@typescript-eslint/parser": ^5.0.0 - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - peerDependenciesMeta: - typescript: - optional: true - checksum: 10c0/3f40cb6bab5a2833c3544e4621b9fdacd8ea53420cadc1c63fac3b89cdf5c62be1e6b7bcf56976dede5db4c43830de298ced3db60b5494a3b961ca1b4bff9f2a + "@typescript-eslint/parser": ^8.70.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.1.0" + checksum: 10c0/e69f6f697493f4b5cb0411498edd63d75afb3e6881c632f9b23c416e64dd06458956ae489a253f0d08b90645b935a32a509d1e2b17068974a2a34a5c2b3f334b languageName: node linkType: hard -"@typescript-eslint/parser@npm:^5.0.0": - version: 5.62.0 - resolution: "@typescript-eslint/parser@npm:5.62.0" +"@typescript-eslint/parser@npm:8.70.0": + version: 8.70.0 + resolution: "@typescript-eslint/parser@npm:8.70.0" dependencies: - "@typescript-eslint/scope-manager": "npm:5.62.0" - "@typescript-eslint/types": "npm:5.62.0" - "@typescript-eslint/typescript-estree": "npm:5.62.0" - debug: "npm:^4.3.4" + "@typescript-eslint/scope-manager": "npm:8.70.0" + "@typescript-eslint/types": "npm:8.70.0" + "@typescript-eslint/typescript-estree": "npm:8.70.0" + "@typescript-eslint/visitor-keys": "npm:8.70.0" + debug: "npm:^4.4.3" peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - peerDependenciesMeta: - typescript: - optional: true - checksum: 10c0/315194b3bf39beb9bd16c190956c46beec64b8371e18d6bb72002108b250983eb1e186a01d34b77eb4045f4941acbb243b16155fbb46881105f65e37dc9e24d4 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.1.0" + checksum: 10c0/6d35e0cad56902295599c3da7e4f4c97ebf6f19fcc4333100ad05ef757ba9c431db9cdbd886b6748eb88afd0bc64f8c1c7a81b37589bbd4cc346f6e9eb4e3d12 languageName: node linkType: hard -"@typescript-eslint/scope-manager@npm:5.62.0": - version: 5.62.0 - resolution: "@typescript-eslint/scope-manager@npm:5.62.0" +"@typescript-eslint/project-service@npm:8.70.0": + version: 8.70.0 + resolution: "@typescript-eslint/project-service@npm:8.70.0" dependencies: - "@typescript-eslint/types": "npm:5.62.0" - "@typescript-eslint/visitor-keys": "npm:5.62.0" - checksum: 10c0/861253235576c1c5c1772d23cdce1418c2da2618a479a7de4f6114a12a7ca853011a1e530525d0931c355a8fd237b9cd828fac560f85f9623e24054fd024726f + "@typescript-eslint/tsconfig-utils": "npm:^8.70.0" + "@typescript-eslint/types": "npm:^8.70.0" + debug: "npm:^4.4.3" + peerDependencies: + typescript: ">=4.8.4 <6.1.0" + checksum: 10c0/eb7d0b4c91015e9aa47672ba20dfda0717e5fa09afa7e28f25b5d7b7b0cd8af4b394f94c3a1528358a7d8c8be4165ed4291a345cbe19bb137a24c6a4f006b398 languageName: node linkType: hard -"@typescript-eslint/type-utils@npm:5.62.0": - version: 5.62.0 - resolution: "@typescript-eslint/type-utils@npm:5.62.0" +"@typescript-eslint/scope-manager@npm:8.70.0": + version: 8.70.0 + resolution: "@typescript-eslint/scope-manager@npm:8.70.0" dependencies: - "@typescript-eslint/typescript-estree": "npm:5.62.0" - "@typescript-eslint/utils": "npm:5.62.0" - debug: "npm:^4.3.4" - tsutils: "npm:^3.21.0" + "@typescript-eslint/types": "npm:8.70.0" + "@typescript-eslint/visitor-keys": "npm:8.70.0" + checksum: 10c0/ca05825fb16266d90e73bd1745407f719fc6d5fdb33045cdae21d3259f13c9b54d8b956d5c64a3eb83baff7690527fb16d01a2d7318379f97fb79a1cbf8ca103 + languageName: node + linkType: hard + +"@typescript-eslint/tsconfig-utils@npm:8.70.0, @typescript-eslint/tsconfig-utils@npm:^8.70.0": + version: 8.70.0 + resolution: "@typescript-eslint/tsconfig-utils@npm:8.70.0" peerDependencies: - eslint: "*" - peerDependenciesMeta: - typescript: - optional: true - checksum: 10c0/93112e34026069a48f0484b98caca1c89d9707842afe14e08e7390af51cdde87378df29d213d3bbd10a7cfe6f91b228031b56218515ce077bdb62ddea9d9f474 + typescript: ">=4.8.4 <6.1.0" + checksum: 10c0/925df7c1fbb4e8050dda3fa3dad4a6f515a1f7f2b43f5c145d9eaac856adb84bba04eb5038cc0f78c2307c1df4e2d64dbb4e2e47abffa4844e25cfe62ce402cf + languageName: node + linkType: hard + +"@typescript-eslint/type-utils@npm:8.70.0": + version: 8.70.0 + resolution: "@typescript-eslint/type-utils@npm:8.70.0" + dependencies: + "@typescript-eslint/types": "npm:8.70.0" + "@typescript-eslint/typescript-estree": "npm:8.70.0" + "@typescript-eslint/utils": "npm:8.70.0" + debug: "npm:^4.4.3" + ts-api-utils: "npm:^2.5.0" + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.1.0" + checksum: 10c0/b745dc16a9ae50fc1bf7880388425d05d7d1775d23819dfcbdbe5930829fb426d7a011a9848494f427f61e236a4deeb9e8f0fd60db4b7a717a791c8454d63584 languageName: node linkType: hard -"@typescript-eslint/types@npm:5.62.0": - version: 5.62.0 - resolution: "@typescript-eslint/types@npm:5.62.0" - checksum: 10c0/7febd3a7f0701c0b927e094f02e82d8ee2cada2b186fcb938bc2b94ff6fbad88237afc304cbaf33e82797078bbbb1baf91475f6400912f8b64c89be79bfa4ddf +"@typescript-eslint/types@npm:8.70.0, @typescript-eslint/types@npm:^8.70.0": + version: 8.70.0 + resolution: "@typescript-eslint/types@npm:8.70.0" + checksum: 10c0/e3f98d0c0e708fcadebb3b457bf3f38a2512220b6bc9823212cacf310377539fb3c24f9e9e754d13b611a3ac6028a0bba16d02eeef03198c65e7ba605bb2036d languageName: node linkType: hard -"@typescript-eslint/typescript-estree@npm:5.62.0": - version: 5.62.0 - resolution: "@typescript-eslint/typescript-estree@npm:5.62.0" +"@typescript-eslint/typescript-estree@npm:8.70.0": + version: 8.70.0 + resolution: "@typescript-eslint/typescript-estree@npm:8.70.0" dependencies: - "@typescript-eslint/types": "npm:5.62.0" - "@typescript-eslint/visitor-keys": "npm:5.62.0" - debug: "npm:^4.3.4" - globby: "npm:^11.1.0" - is-glob: "npm:^4.0.3" - semver: "npm:^7.3.7" - tsutils: "npm:^3.21.0" - peerDependenciesMeta: - typescript: - optional: true - checksum: 10c0/d7984a3e9d56897b2481940ec803cb8e7ead03df8d9cfd9797350be82ff765dfcf3cfec04e7355e1779e948da8f02bc5e11719d07a596eb1cb995c48a95e38cf + "@typescript-eslint/project-service": "npm:8.70.0" + "@typescript-eslint/tsconfig-utils": "npm:8.70.0" + "@typescript-eslint/types": "npm:8.70.0" + "@typescript-eslint/visitor-keys": "npm:8.70.0" + debug: "npm:^4.4.3" + minimatch: "npm:^10.2.2" + semver: "npm:^7.7.3" + tinyglobby: "npm:^0.2.15" + ts-api-utils: "npm:^2.5.0" + peerDependencies: + typescript: ">=4.8.4 <6.1.0" + checksum: 10c0/11e6c9c0108184902bc229376cce00eb81ee2b88a7cc3bf02a8c159bf59b2c7af21d2b330976b1751fedd5c27e257731b0d60840268299582c458c2a0a29525e languageName: node linkType: hard -"@typescript-eslint/utils@npm:5.62.0": - version: 5.62.0 - resolution: "@typescript-eslint/utils@npm:5.62.0" +"@typescript-eslint/utils@npm:8.70.0": + version: 8.70.0 + resolution: "@typescript-eslint/utils@npm:8.70.0" dependencies: - "@eslint-community/eslint-utils": "npm:^4.2.0" - "@types/json-schema": "npm:^7.0.9" - "@types/semver": "npm:^7.3.12" - "@typescript-eslint/scope-manager": "npm:5.62.0" - "@typescript-eslint/types": "npm:5.62.0" - "@typescript-eslint/typescript-estree": "npm:5.62.0" - eslint-scope: "npm:^5.1.1" - semver: "npm:^7.3.7" + "@eslint-community/eslint-utils": "npm:^4.9.1" + "@typescript-eslint/scope-manager": "npm:8.70.0" + "@typescript-eslint/types": "npm:8.70.0" + "@typescript-eslint/typescript-estree": "npm:8.70.0" peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - checksum: 10c0/f09b7d9952e4a205eb1ced31d7684dd55cee40bf8c2d78e923aa8a255318d97279825733902742c09d8690f37a50243f4c4d383ab16bd7aefaf9c4b438f785e1 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.1.0" + checksum: 10c0/15353ecd0c5e29057e5846c6b84de601b679448e01293dbf1ae7dbe94ab6d4b586f8690a262630522441307fafad01ab3b0b46a85eb71907c19a2f124d7bba3f languageName: node linkType: hard -"@typescript-eslint/visitor-keys@npm:5.62.0": - version: 5.62.0 - resolution: "@typescript-eslint/visitor-keys@npm:5.62.0" +"@typescript-eslint/visitor-keys@npm:8.70.0": + version: 8.70.0 + resolution: "@typescript-eslint/visitor-keys@npm:8.70.0" dependencies: - "@typescript-eslint/types": "npm:5.62.0" - eslint-visitor-keys: "npm:^3.3.0" - checksum: 10c0/7c3b8e4148e9b94d9b7162a596a1260d7a3efc4e65199693b8025c71c4652b8042501c0bc9f57654c1e2943c26da98c0f77884a746c6ae81389fcb0b513d995d + "@typescript-eslint/types": "npm:8.70.0" + eslint-visitor-keys: "npm:^5.0.0" + checksum: 10c0/43fe28dc1045a686deead8c9334f6e9d8b3fa8f84917147aabd9747783b8bc531254a95a9bfb33355b25cf5ec9a72c62bcde313f13869d7a5e5603d96be593a9 languageName: node linkType: hard @@ -2036,7 +2142,7 @@ __metadata: languageName: node linkType: hard -"acorn-jsx@npm:^5.3.1": +"acorn-jsx@npm:^5.3.2": version: 5.3.2 resolution: "acorn-jsx@npm:5.3.2" peerDependencies: @@ -2054,21 +2160,21 @@ __metadata: languageName: node linkType: hard -"acorn@npm:^7.4.0": - version: 7.4.1 - resolution: "acorn@npm:7.4.1" +"acorn@npm:^8.11.0, acorn@npm:^8.4.1": + version: 8.15.0 + resolution: "acorn@npm:8.15.0" bin: acorn: bin/acorn - checksum: 10c0/bd0b2c2b0f334bbee48828ff897c12bd2eb5898d03bf556dcc8942022cec795ac5bb5b6b585e2de687db6231faf07e096b59a361231dd8c9344d5df5f7f0e526 + checksum: 10c0/dec73ff59b7d6628a01eebaece7f2bdb8bb62b9b5926dcad0f8931f2b8b79c2be21f6c68ac095592adb5adb15831a3635d9343e6a91d028bbe85d564875ec3ec languageName: node linkType: hard -"acorn@npm:^8.11.0, acorn@npm:^8.4.1": - version: 8.15.0 - resolution: "acorn@npm:8.15.0" +"acorn@npm:^8.16.0": + version: 8.18.0 + resolution: "acorn@npm:8.18.0" bin: acorn: bin/acorn - checksum: 10c0/dec73ff59b7d6628a01eebaece7f2bdb8bb62b9b5926dcad0f8931f2b8b79c2be21f6c68ac095592adb5adb15831a3635d9343e6a91d028bbe85d564875ec3ec + checksum: 10c0/be771be2135cc07910cf76f444ad514d7dcfd6d4a8026e597e93155275abc8ef61eee12211d52146e9d962874269b634f397464942087be013c89d0c54c5f8e5 languageName: node linkType: hard @@ -2086,27 +2192,15 @@ __metadata: languageName: node linkType: hard -"ajv@npm:^6.10.0, ajv@npm:^6.12.4": - version: 6.12.6 - resolution: "ajv@npm:6.12.6" +"ajv@npm:^6.14.0": + version: 6.15.0 + resolution: "ajv@npm:6.15.0" dependencies: fast-deep-equal: "npm:^3.1.1" fast-json-stable-stringify: "npm:^2.0.0" json-schema-traverse: "npm:^0.4.1" uri-js: "npm:^4.2.2" - checksum: 10c0/41e23642cbe545889245b9d2a45854ebba51cda6c778ebced9649420d9205f2efb39cb43dbc41e358409223b1ea43303ae4839db682c848b891e4811da1a5a71 - languageName: node - linkType: hard - -"ajv@npm:^8.0.1": - version: 8.17.1 - resolution: "ajv@npm:8.17.1" - dependencies: - fast-deep-equal: "npm:^3.1.3" - fast-uri: "npm:^3.0.1" - json-schema-traverse: "npm:^1.0.0" - require-from-string: "npm:^2.0.2" - checksum: 10c0/ec3ba10a573c6b60f94639ffc53526275917a2df6810e4ab5a6b959d87459f9ef3f00d5e7865b82677cb7d21590355b34da14d1d0b9c32d75f95a187e76fff35 + checksum: 10c0/67966499dd272ecde1c2e467084411132891523d057487587879d39ac04207f4351b7b2324c83198013967fbfa632c1612adc960114a30770fbe07a0773b32c2 languageName: node linkType: hard @@ -2140,15 +2234,6 @@ __metadata: languageName: node linkType: hard -"ansi-styles@npm:^3.2.1": - version: 3.2.1 - resolution: "ansi-styles@npm:3.2.1" - dependencies: - color-convert: "npm:^1.9.0" - checksum: 10c0/ece5a8ef069fcc5298f67e3f4771a663129abd174ea2dfa87923a2be2abf6cd367ef72ac87942da00ce85bd1d651d4cd8595aebdb1b385889b89b205860e977b - languageName: node - linkType: hard - "ansi-styles@npm:^4.0.0, ansi-styles@npm:^4.1.0": version: 4.3.0 resolution: "ansi-styles@npm:4.3.0" @@ -2233,13 +2318,6 @@ __metadata: languageName: node linkType: hard -"astral-regex@npm:^2.0.0": - version: 2.0.0 - resolution: "astral-regex@npm:2.0.0" - checksum: 10c0/f63d439cc383db1b9c5c6080d1e240bd14dae745f15d11ec5da863e182bbeca70df6c8191cffef5deba0b566ef98834610a68be79ac6379c95eeb26e1b310e25 - languageName: node - linkType: hard - "async@npm:^3.2.6": version: 3.2.6 resolution: "async@npm:3.2.6" @@ -2347,6 +2425,13 @@ __metadata: languageName: node linkType: hard +"balanced-match@npm:^4.0.2": + version: 4.0.4 + resolution: "balanced-match@npm:4.0.4" + checksum: 10c0/07e86102a3eb2ee2a6a1a89164f29d0dbaebd28f2ca3f5ca786f36b8b23d9e417eb3be45a4acf754f837be5ac0a2317de90d3fcb7f4f4dc95720a1f36b26a17b + languageName: node + linkType: hard + "base64id@npm:2.0.0, base64id@npm:~2.0.0": version: 2.0.0 resolution: "base64id@npm:2.0.0" @@ -2400,6 +2485,15 @@ __metadata: languageName: node linkType: hard +"brace-expansion@npm:^5.0.8": + version: 5.0.12 + resolution: "brace-expansion@npm:5.0.12" + dependencies: + balanced-match: "npm:^4.0.2" + checksum: 10c0/a40ac64d3bdb5bb222456514d3633c1f627f03b844f509c69e70f98b78be0ce85e21b44bbfecdd0ddd4cc0855ca4242b41ea96ddccf88c7c3c7a84405474bb3a + languageName: node + linkType: hard + "braces@npm:^3.0.3, braces@npm:~3.0.2": version: 3.0.3 resolution: "braces@npm:3.0.3" @@ -2492,6 +2586,19 @@ __metadata: languageName: node linkType: hard +"cacheable@npm:^2.5.0": + version: 2.5.0 + resolution: "cacheable@npm:2.5.0" + dependencies: + "@cacheable/memory": "npm:^2.2.0" + "@cacheable/utils": "npm:^2.5.0" + hookified: "npm:^1.15.0" + keyv: "npm:^5.6.0" + qified: "npm:^0.10.1" + checksum: 10c0/ad4086a60ed327196901c16360312705e35bfde5118457449cfabaf4ece0fecad046985ea8b66b3ece46dd469cb33798d87e1dac3ed2c00746578054bccbf74d + languageName: node + linkType: hard + "call-bind-apply-helpers@npm:^1.0.1, call-bind-apply-helpers@npm:^1.0.2": version: 1.0.2 resolution: "call-bind-apply-helpers@npm:1.0.2" @@ -2540,17 +2647,6 @@ __metadata: languageName: node linkType: hard -"chalk@npm:^2.4.2": - version: 2.4.2 - resolution: "chalk@npm:2.4.2" - dependencies: - ansi-styles: "npm:^3.2.1" - escape-string-regexp: "npm:^1.0.5" - supports-color: "npm:^5.3.0" - checksum: 10c0/e6543f02ec877732e3a2d1c3c3323ddb4d39fbab687c23f526e25bd4c6a9bf3b83a696e8c769d078e04e5754921648f7821b2a2acfd16c550435fd630026e073 - languageName: node - linkType: hard - "chalk@npm:^4.0.0, chalk@npm:^4.1.0, chalk@npm:^4.1.1": version: 4.1.2 resolution: "chalk@npm:4.1.2" @@ -2660,15 +2756,6 @@ __metadata: languageName: node linkType: hard -"color-convert@npm:^1.9.0": - version: 1.9.3 - resolution: "color-convert@npm:1.9.3" - dependencies: - color-name: "npm:1.1.3" - checksum: 10c0/5ad3c534949a8c68fca8fbc6f09068f435f0ad290ab8b2f76841b9e6af7e0bb57b98cb05b0e19fe33f5d91e5a8611ad457e5f69e0a484caad1f7487fd0e8253c - languageName: node - linkType: hard - "color-convert@npm:^2.0.1": version: 2.0.1 resolution: "color-convert@npm:2.0.1" @@ -2678,13 +2765,6 @@ __metadata: languageName: node linkType: hard -"color-name@npm:1.1.3": - version: 1.1.3 - resolution: "color-name@npm:1.1.3" - checksum: 10c0/566a3d42cca25b9b3cd5528cd7754b8e89c0eb646b7f214e8e2eaddb69994ac5f0557d9c175eb5d8f0ad73531140d9c47525085ee752a91a2ab15ab459caf6d6 - languageName: node - linkType: hard - "color-name@npm:~1.1.4": version: 1.1.4 resolution: "color-name@npm:1.1.4" @@ -2844,7 +2924,7 @@ __metadata: languageName: node linkType: hard -"cross-spawn@npm:^7.0.2, cross-spawn@npm:^7.0.3, cross-spawn@npm:^7.0.6": +"cross-spawn@npm:^7.0.3, cross-spawn@npm:^7.0.6": version: 7.0.6 resolution: "cross-spawn@npm:7.0.6" dependencies: @@ -2895,7 +2975,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:4.x, debug@npm:^4, debug@npm:^4.0.1, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4": +"debug@npm:4, debug@npm:4.x, debug@npm:^4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4": version: 4.4.1 resolution: "debug@npm:4.4.1" dependencies: @@ -2907,6 +2987,18 @@ __metadata: languageName: node linkType: hard +"debug@npm:^4.4.3, debug@npm:~4.4.1": + version: 4.4.3 + resolution: "debug@npm:4.4.3" + dependencies: + ms: "npm:^2.1.3" + peerDependenciesMeta: + supports-color: + optional: true + checksum: 10c0/d79136ec6c83ecbefd0f6a5593da6a9c91ec4d7ddc4b54c883d6e71ec9accb5f67a1a5e96d00a328196b5b5c86d365e98d8a3a70856aaf16b4e7b1985e67f5a6 + languageName: node + linkType: hard + "debug@npm:~4.3.1, debug@npm:~4.3.2, debug@npm:~4.3.4": version: 4.3.7 resolution: "debug@npm:4.3.7" @@ -3025,9 +3117,9 @@ __metadata: linkType: hard "diff@npm:^4.0.1": - version: 4.0.2 - resolution: "diff@npm:4.0.2" - checksum: 10c0/81b91f9d39c4eaca068eb0c1eb0e4afbdc5bb2941d197f513dd596b820b956fef43485876226d65d497bebc15666aa2aa82c679e84f65d5f2bfbf14ee46e32c1 + version: 4.0.4 + resolution: "diff@npm:4.0.4" + checksum: 10c0/855fb70b093d1d9643ddc12ea76dca90dc9d9cdd7f82c08ee8b9325c0dc5748faf3c82e2047ced5dcaa8b26e58f7903900be2628d0380a222c02d79d8de385df languageName: node linkType: hard @@ -3040,15 +3132,6 @@ __metadata: languageName: node linkType: hard -"doctrine@npm:^3.0.0": - version: 3.0.0 - resolution: "doctrine@npm:3.0.0" - dependencies: - esutils: "npm:^2.0.2" - checksum: 10c0/c96bdccabe9d62ab6fea9399fdff04a66e6563c1d6fb3a3a063e8d53c3bb136ba63e84250bbf63d00086a769ad53aef92d2bd483f03f837fc97b71cbee6b2520 - languageName: node - linkType: hard - "dunder-proto@npm:^1.0.1": version: 1.0.1 resolution: "dunder-proto@npm:1.0.1" @@ -3182,7 +3265,7 @@ __metadata: languageName: node linkType: hard -"enquirer@npm:^2.3.4, enquirer@npm:^2.3.5": +"enquirer@npm:^2.3.4": version: 2.4.1 resolution: "enquirer@npm:2.4.1" dependencies: @@ -3309,13 +3392,6 @@ __metadata: languageName: node linkType: hard -"escape-string-regexp@npm:^1.0.5": - version: 1.0.5 - resolution: "escape-string-regexp@npm:1.0.5" - checksum: 10c0/a968ad453dd0c2724e14a4f20e177aaf32bb384ab41b674a8454afe9a41c5e6fe8903323e0a1052f56289d04bd600f81278edf140b0fcc02f5cac98d0f5b5371 - languageName: node - linkType: hard - "escape-string-regexp@npm:^2.0.0": version: 2.0.0 resolution: "escape-string-regexp@npm:2.0.0" @@ -3330,119 +3406,85 @@ __metadata: languageName: node linkType: hard -"eslint-config-prettier@npm:^8.5.0": - version: 8.10.2 - resolution: "eslint-config-prettier@npm:8.10.2" +"eslint-config-prettier@npm:^10.1.8": + version: 10.1.8 + resolution: "eslint-config-prettier@npm:10.1.8" peerDependencies: eslint: ">=7.0.0" bin: eslint-config-prettier: bin/cli.js - checksum: 10c0/b5953cf7a86f685e1218b16707bf36643b525513d08495226a6820caccd8b7bfc6b9aa64ac7cb2415dbe2c1f7dc4995832148bdc53ad45777f75a8ded1073b29 - languageName: node - linkType: hard - -"eslint-plugin-prettier@npm:^4.0.0": - version: 4.2.5 - resolution: "eslint-plugin-prettier@npm:4.2.5" - dependencies: - prettier-linter-helpers: "npm:^1.0.0" - peerDependencies: - eslint: ">=7.28.0" - prettier: ">=2.0.0" - peerDependenciesMeta: - eslint-config-prettier: - optional: true - checksum: 10c0/75b3cdc90328aacf4cc7fabc522e651bd8208d40634c9b2772274332a696548136dac4608b141863bc462500c5a8012fbc2495623f684f631ddb62c2f5bca0a3 + checksum: 10c0/e1bcfadc9eccd526c240056b1e59c5cd26544fe59feb85f38f4f1f116caed96aea0b3b87868e68b3099e55caaac3f2e5b9f58110f85db893e83a332751192682 languageName: node linkType: hard -"eslint-scope@npm:^5.1.1": - version: 5.1.1 - resolution: "eslint-scope@npm:5.1.1" +"eslint-scope@npm:^9.1.2": + version: 9.1.2 + resolution: "eslint-scope@npm:9.1.2" dependencies: + "@types/esrecurse": "npm:^4.3.1" + "@types/estree": "npm:^1.0.8" esrecurse: "npm:^4.3.0" - estraverse: "npm:^4.1.1" - checksum: 10c0/d30ef9dc1c1cbdece34db1539a4933fe3f9b14e1ffb27ecc85987902ee663ad7c9473bbd49a9a03195a373741e62e2f807c4938992e019b511993d163450e70a - languageName: node - linkType: hard - -"eslint-utils@npm:^2.1.0": - version: 2.1.0 - resolution: "eslint-utils@npm:2.1.0" - dependencies: - eslint-visitor-keys: "npm:^1.1.0" - checksum: 10c0/69521c5d6569384b24093125d037ba238d3d6e54367f7143af9928f5286369e912c26cad5016d730c0ffb9797ac9e83831059d7f1d863f7dc84330eb02414611 - languageName: node - linkType: hard - -"eslint-visitor-keys@npm:^1.1.0, eslint-visitor-keys@npm:^1.3.0": - version: 1.3.0 - resolution: "eslint-visitor-keys@npm:1.3.0" - checksum: 10c0/10c91fdbbe36810dd4308e57f9a8bc7177188b2a70247e54e3af1fa05ebc66414ae6fd4ce3c6c6821591f43a556e9037bc6b071122e099b5f8b7d2f76df553e3 - languageName: node - linkType: hard - -"eslint-visitor-keys@npm:^2.0.0": - version: 2.1.0 - resolution: "eslint-visitor-keys@npm:2.1.0" - checksum: 10c0/9f0e3a2db751d84067d15977ac4b4472efd6b303e369e6ff241a99feac04da758f46d5add022c33d06b53596038dbae4b4aceb27c7e68b8dfc1055b35e495787 + estraverse: "npm:^5.2.0" + checksum: 10c0/9fb8bca5a73e5741efb6cec84467027b6cb6f4203ff9b43a938e272c5cd30800bde46a5c20dfd1609f840225f0b62b7673be391b20acadf8658ca9fa4729b3dd languageName: node linkType: hard -"eslint-visitor-keys@npm:^3.3.0, eslint-visitor-keys@npm:^3.4.3": +"eslint-visitor-keys@npm:^3.4.3": version: 3.4.3 resolution: "eslint-visitor-keys@npm:3.4.3" checksum: 10c0/92708e882c0a5ffd88c23c0b404ac1628cf20104a108c745f240a13c332a11aac54f49a22d5762efbffc18ecbc9a580d1b7ad034bf5f3cc3307e5cbff2ec9820 languageName: node linkType: hard -"eslint@npm:^7.32.0": - version: 7.32.0 - resolution: "eslint@npm:7.32.0" - dependencies: - "@babel/code-frame": "npm:7.12.11" - "@eslint/eslintrc": "npm:^0.4.3" - "@humanwhocodes/config-array": "npm:^0.5.0" - ajv: "npm:^6.10.0" - chalk: "npm:^4.0.0" - cross-spawn: "npm:^7.0.2" - debug: "npm:^4.0.1" - doctrine: "npm:^3.0.0" - enquirer: "npm:^2.3.5" +"eslint-visitor-keys@npm:^5.0.0, eslint-visitor-keys@npm:^5.0.1": + version: 5.0.1 + resolution: "eslint-visitor-keys@npm:5.0.1" + checksum: 10c0/16190bdf2cbae40a1109384c94450c526a79b0b9c3cb21e544256ed85ac48a4b84db66b74a6561d20fe6ab77447f150d711c2ad5ad74df4fcc133736bce99678 + languageName: node + linkType: hard + +"eslint@npm:^10.10.0": + version: 10.10.0 + resolution: "eslint@npm:10.10.0" + dependencies: + "@eslint-community/eslint-utils": "npm:^4.8.0" + "@eslint-community/regexpp": "npm:^4.12.2" + "@eslint/config-array": "npm:^0.23.5" + "@eslint/config-helpers": "npm:^0.7.0" + "@eslint/core": "npm:^1.2.1" + "@eslint/plugin-kit": "npm:^0.7.3" + "@humanfs/node": "npm:^0.16.6" + "@humanwhocodes/module-importer": "npm:^1.0.1" + "@humanwhocodes/retry": "npm:^0.4.2" + "@types/estree": "npm:^1.0.6" + ajv: "npm:^6.14.0" + cross-spawn: "npm:^7.0.6" + debug: "npm:^4.3.2" escape-string-regexp: "npm:^4.0.0" - eslint-scope: "npm:^5.1.1" - eslint-utils: "npm:^2.1.0" - eslint-visitor-keys: "npm:^2.0.0" - espree: "npm:^7.3.1" - esquery: "npm:^1.4.0" + eslint-scope: "npm:^9.1.2" + eslint-visitor-keys: "npm:^5.0.1" + espree: "npm:^11.2.0" + esquery: "npm:^1.7.0" esutils: "npm:^2.0.2" fast-deep-equal: "npm:^3.1.3" - file-entry-cache: "npm:^6.0.1" - functional-red-black-tree: "npm:^1.0.1" - glob-parent: "npm:^5.1.2" - globals: "npm:^13.6.0" - ignore: "npm:^4.0.6" - import-fresh: "npm:^3.0.0" + file-entry-cache: "npm:11.1.5 || >11.1.6 <12" + find-up: "npm:^5.0.0" + glob-parent: "npm:^6.0.2" + ignore: "npm:^5.2.0" imurmurhash: "npm:^0.1.4" is-glob: "npm:^4.0.0" - js-yaml: "npm:^3.13.1" json-stable-stringify-without-jsonify: "npm:^1.0.1" - levn: "npm:^0.4.1" - lodash.merge: "npm:^4.6.2" - minimatch: "npm:^3.0.4" + minimatch: "npm:^10.2.5" natural-compare: "npm:^1.4.0" - optionator: "npm:^0.9.1" - progress: "npm:^2.0.0" - regexpp: "npm:^3.1.0" - semver: "npm:^7.2.1" - strip-ansi: "npm:^6.0.0" - strip-json-comments: "npm:^3.1.0" - table: "npm:^6.0.9" - text-table: "npm:^0.2.0" - v8-compile-cache: "npm:^2.0.3" + optionator: "npm:^0.9.3" + peerDependencies: + jiti: "*" + peerDependenciesMeta: + jiti: + optional: true bin: eslint: bin/eslint.js - checksum: 10c0/84409f7767556179cb11529f1215f335c7dfccf90419df6147f949f14c347a960c7b569e80ed84011a0b6d10da1ef5046edbbb9b11c3e59aa6696d5217092e93 + checksum: 10c0/ba583145305f088585f91c5dab6825492ec657614558e4610cffc94174cf800f456e39ec3cafd3eed73e04bd9a05794067ae33b553f1323e3e7c1157242409d2 languageName: node linkType: hard @@ -3458,14 +3500,14 @@ __metadata: languageName: node linkType: hard -"espree@npm:^7.3.0, espree@npm:^7.3.1": - version: 7.3.1 - resolution: "espree@npm:7.3.1" +"espree@npm:^11.2.0": + version: 11.2.0 + resolution: "espree@npm:11.2.0" dependencies: - acorn: "npm:^7.4.0" - acorn-jsx: "npm:^5.3.1" - eslint-visitor-keys: "npm:^1.3.0" - checksum: 10c0/f4e81b903f03eaf0e6925cea20571632da427deb6e14ca37e481f72c11f36d7bb4945fe8a2ff15ab22d078d3cd93ee65355fa94de9c27485c356481775f25d85 + acorn: "npm:^8.16.0" + acorn-jsx: "npm:^5.3.2" + eslint-visitor-keys: "npm:^5.0.1" + checksum: 10c0/cf87e18ffd9dc113eb8d16588e7757701bc10c9934a71cce8b89c2611d51672681a918307bd6b19ac3ccd0e7ba1cbccc2f815b36b52fa7e73097b251014c3d81 languageName: node linkType: hard @@ -3479,12 +3521,12 @@ __metadata: languageName: node linkType: hard -"esquery@npm:^1.4.0": - version: 1.6.0 - resolution: "esquery@npm:1.6.0" +"esquery@npm:^1.7.0": + version: 1.7.0 + resolution: "esquery@npm:1.7.0" dependencies: estraverse: "npm:^5.1.0" - checksum: 10c0/cb9065ec605f9da7a76ca6dadb0619dfb611e37a81e318732977d90fab50a256b95fee2d925fba7c2f3f0523aa16f91587246693bc09bc34d5a59575fe6e93d2 + checksum: 10c0/77d5173db450b66f3bc685d11af4c90cffeedb340f34a39af96d43509a335ce39c894fd79233df32d38f5e4e219fa0f7076f6ec90bae8320170ba082c0db4793 languageName: node linkType: hard @@ -3497,13 +3539,6 @@ __metadata: languageName: node linkType: hard -"estraverse@npm:^4.1.1": - version: 4.3.0 - resolution: "estraverse@npm:4.3.0" - checksum: 10c0/9cb46463ef8a8a4905d3708a652d60122a0c20bb58dec7e0e12ab0e7235123d74214fc0141d743c381813e1b992767e2708194f6f6e0f9fd00c1b4e0887b8b6d - languageName: node - linkType: hard - "estraverse@npm:^5.1.0, estraverse@npm:^5.2.0": version: 5.3.0 resolution: "estraverse@npm:5.3.0" @@ -3648,14 +3683,7 @@ __metadata: languageName: node linkType: hard -"fast-diff@npm:^1.1.2": - version: 1.3.0 - resolution: "fast-diff@npm:1.3.0" - checksum: 10c0/5c19af237edb5d5effda008c891a18a585f74bf12953be57923f17a3a4d0979565fc64dbc73b9e20926b9d895f5b690c618cbb969af0cf022e3222471220ad29 - languageName: node - linkType: hard - -"fast-glob@npm:^3.0.3, fast-glob@npm:^3.2.9": +"fast-glob@npm:^3.0.3": version: 3.3.3 resolution: "fast-glob@npm:3.3.3" dependencies: @@ -3689,13 +3717,6 @@ __metadata: languageName: node linkType: hard -"fast-uri@npm:^3.0.1": - version: 3.0.6 - resolution: "fast-uri@npm:3.0.6" - checksum: 10c0/74a513c2af0584448aee71ce56005185f81239eab7a2343110e5bad50c39ad4fb19c5a6f99783ead1cac7ccaf3461a6034fda89fffa2b30b6d99b9f21c2f9d29 - languageName: node - linkType: hard - "fastq@npm:^1.6.0": version: 1.19.1 resolution: "fastq@npm:1.19.1" @@ -3726,12 +3747,24 @@ __metadata: languageName: node linkType: hard -"file-entry-cache@npm:^6.0.1": - version: 6.0.1 - resolution: "file-entry-cache@npm:6.0.1" +"fdir@npm:^6.5.0": + version: 6.5.0 + resolution: "fdir@npm:6.5.0" + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + checksum: 10c0/e345083c4306b3aed6cb8ec551e26c36bab5c511e99ea4576a16750ddc8d3240e63826cc624f5ae17ad4dc82e68a253213b60d556c11bfad064b7607847ed07f + languageName: node + linkType: hard + +"file-entry-cache@npm:11.1.5 || >11.1.6 <12": + version: 11.1.5 + resolution: "file-entry-cache@npm:11.1.5" dependencies: - flat-cache: "npm:^3.0.4" - checksum: 10c0/58473e8a82794d01b38e5e435f6feaf648e3f36fdb3a56e98f417f4efae71ad1c0d4ebd8a9a7c50c3ad085820a93fc7494ad721e0e4ebc1da3573f4e1c3c7cdd + flat-cache: "npm:^6.1.23" + checksum: 10c0/d05eca8794b27824d79a0f1a0bc76b81ca7232b6c9634bc086f365a6d9ddac749c4826b2b2d36bb7ef9ed84fdf0097f34979527daeb9255b4f17673a4ae50fc5 languageName: node linkType: hard @@ -3785,21 +3818,31 @@ __metadata: languageName: node linkType: hard -"flat-cache@npm:^3.0.4": - version: 3.2.0 - resolution: "flat-cache@npm:3.2.0" +"find-up@npm:^5.0.0": + version: 5.0.0 + resolution: "find-up@npm:5.0.0" dependencies: - flatted: "npm:^3.2.9" - keyv: "npm:^4.5.3" - rimraf: "npm:^3.0.2" - checksum: 10c0/b76f611bd5f5d68f7ae632e3ae503e678d205cf97a17c6ab5b12f6ca61188b5f1f7464503efae6dc18683ed8f0b41460beb48ac4b9ac63fe6201296a91ba2f75 + locate-path: "npm:^6.0.0" + path-exists: "npm:^4.0.0" + checksum: 10c0/062c5a83a9c02f53cdd6d175a37ecf8f87ea5bbff1fdfb828f04bfa021441bc7583e8ebc0872a4c1baab96221fb8a8a275a19809fb93fbc40bd69ec35634069a languageName: node linkType: hard -"flatted@npm:^3.2.9": - version: 3.3.3 - resolution: "flatted@npm:3.3.3" - checksum: 10c0/e957a1c6b0254aa15b8cce8533e24165abd98fadc98575db082b786b5da1b7d72062b81bfdcd1da2f4d46b6ed93bec2434e62333e9b4261d79ef2e75a10dd538 +"flat-cache@npm:^6.1.23": + version: 6.1.23 + resolution: "flat-cache@npm:6.1.23" + dependencies: + cacheable: "npm:^2.5.0" + flatted: "npm:^3.4.2" + hookified: "npm:^1.15.0" + checksum: 10c0/c757c24f51334e1917958cd9d62d4ed3d520a3815d5a1ec50d993ac3a93fa81b6de7e28264231914bb0d990babd426e4d713d3e2f3d782dae65c42a2ded1e651 + languageName: node + linkType: hard + +"flatted@npm:^3.4.2": + version: 3.4.4 + resolution: "flatted@npm:3.4.4" + checksum: 10c0/a3a52a88ea5a4c333e5a1f097dcd87a037fa31c236a77cf46222c2aa4036ef895ab9bc76138a66d9dc22bdb3652128f9598cd26e7439561bf19f67276e2bc37e languageName: node linkType: hard @@ -3905,13 +3948,6 @@ __metadata: languageName: node linkType: hard -"functional-red-black-tree@npm:^1.0.1": - version: 1.0.1 - resolution: "functional-red-black-tree@npm:1.0.1" - checksum: 10c0/5959eed0375803d9924f47688479bb017e0c6816a0e5ac151e22ba6bfe1d12c41de2f339188885e0aa8eeea2072dad509d8e4448467e816bde0a2ca86a0670d3 - languageName: node - linkType: hard - "gensync@npm:^1.0.0-beta.2": version: 1.0.0-beta.2 resolution: "gensync@npm:1.0.0-beta.2" @@ -3977,6 +4013,15 @@ __metadata: languageName: node linkType: hard +"glob-parent@npm:^6.0.2": + version: 6.0.2 + resolution: "glob-parent@npm:6.0.2" + dependencies: + is-glob: "npm:^4.0.3" + checksum: 10c0/317034d88654730230b3f43bb7ad4f7c90257a426e872ea0bf157473ac61c99bf5d205fad8f0185f989be8d2fa6d3c7dce1645d99d545b6ea9089c39f838e7f8 + languageName: node + linkType: hard + "glob@npm:^10.2.2, glob@npm:^10.3.7": version: 10.4.5 resolution: "glob@npm:10.4.5" @@ -4007,12 +4052,10 @@ __metadata: languageName: node linkType: hard -"globals@npm:^13.6.0, globals@npm:^13.9.0": - version: 13.24.0 - resolution: "globals@npm:13.24.0" - dependencies: - type-fest: "npm:^0.20.2" - checksum: 10c0/d3c11aeea898eb83d5ec7a99508600fbe8f83d2cf00cbb77f873dbf2bcb39428eff1b538e4915c993d8a3b3473fa71eeebfe22c9bb3a3003d1e26b1f2c8a42cd +"globals@npm:^17.12.0": + version: 17.12.0 + resolution: "globals@npm:17.12.0" + checksum: 10c0/8325cf8818c848871d17c32974c2c74e9c5f09c1b0e04421886f64f21e77c3e95f3ba25586743895a73acd860ada151132481d87d44a1f12993ae7758a694d03 languageName: node linkType: hard @@ -4032,20 +4075,6 @@ __metadata: languageName: node linkType: hard -"globby@npm:^11.1.0": - version: 11.1.0 - resolution: "globby@npm:11.1.0" - dependencies: - array-union: "npm:^2.1.0" - dir-glob: "npm:^3.0.1" - fast-glob: "npm:^3.2.9" - ignore: "npm:^5.2.0" - merge2: "npm:^1.4.1" - slash: "npm:^3.0.0" - checksum: 10c0/b39511b4afe4bd8a7aead3a27c4ade2b9968649abab0a6c28b1a90141b96ca68ca5db1302f7c7bd29eab66bf51e13916b8e0a3d0ac08f75e1e84a39b35691189 - languageName: node - linkType: hard - "gopd@npm:^1.2.0": version: 1.2.0 resolution: "gopd@npm:1.2.0" @@ -4060,16 +4089,9 @@ __metadata: languageName: node linkType: hard -"graphemer@npm:^1.4.0": - version: 1.4.0 - resolution: "graphemer@npm:1.4.0" - checksum: 10c0/e951259d8cd2e0d196c72ec711add7115d42eb9a8146c8eeda5b8d3ac91e5dd816b9cd68920726d9fd4490368e7ed86e9c423f40db87e2d8dfafa00fa17c3a31 - languageName: node - linkType: hard - "handlebars@npm:^4.7.8": - version: 4.7.8 - resolution: "handlebars@npm:4.7.8" + version: 4.7.9 + resolution: "handlebars@npm:4.7.9" dependencies: minimist: "npm:^1.2.5" neo-async: "npm:^2.6.2" @@ -4081,7 +4103,7 @@ __metadata: optional: true bin: handlebars: bin/handlebars - checksum: 10c0/7aff423ea38a14bb379316f3857fe0df3c5d66119270944247f155ba1f08e07a92b340c58edaa00cfe985c21508870ee5183e0634dcb53dd405f35c93ef7f10d + checksum: 10c0/22f8105a7e68e81aff2662bb434edf05f757d21d850731d71cec886d69c10cd33d3c43e34b2892968ec62de8241611851d3d0674c8ef324ea3e01dc66262faa9 languageName: node linkType: hard @@ -4115,6 +4137,15 @@ __metadata: languageName: node linkType: hard +"hashery@npm:^1.4.0, hashery@npm:^1.5.1": + version: 1.5.1 + resolution: "hashery@npm:1.5.1" + dependencies: + hookified: "npm:^1.15.0" + checksum: 10c0/ab4225b655a7b0d05df99b1a59d5b3a51fe433f82422ca25e6f3f4c4ddd30adb49ebd38e0047ef9bded93319c1e9fc857e16aa382e554929c871cb77d39fc463 + languageName: node + linkType: hard + "hasown@npm:^2.0.2": version: 2.0.2 resolution: "hasown@npm:2.0.2" @@ -4131,6 +4162,20 @@ __metadata: languageName: node linkType: hard +"hookified@npm:^1.15.0, hookified@npm:^1.15.1": + version: 1.15.1 + resolution: "hookified@npm:1.15.1" + checksum: 10c0/6b691374fa97ae57169fb29f90e723499fda5e85494654fbe55c4768b3ccbf3e14c0adc8d0f365f32c503b60d7c06f907781f5966c03d41c423575eb5e16860c + languageName: node + linkType: hard + +"hookified@npm:^2.1.1": + version: 2.2.0 + resolution: "hookified@npm:2.2.0" + checksum: 10c0/7017d2b66945490293a5aba239e7b39f39071dd940fa019348c7ffea92b91b8c267853c4c51680ed0f3687b33352582fa4d2cff6dd4c5a1c5c44b037276f07aa + languageName: node + linkType: hard + "html-escaper@npm:^2.0.0": version: 2.0.2 resolution: "html-escaper@npm:2.0.2" @@ -4233,13 +4278,6 @@ __metadata: languageName: node linkType: hard -"ignore@npm:^4.0.6": - version: 4.0.6 - resolution: "ignore@npm:4.0.6" - checksum: 10c0/836ee7dc7fd9436096e2dba429359dbb9fa0e33d309e2b2d81692f375f6ca82024fc00567f798613d50c6b989e9cd2ad2b065acf116325cde177f02c86b7d4e0 - languageName: node - linkType: hard - "ignore@npm:^5.1.1, ignore@npm:^5.2.0": version: 5.3.2 resolution: "ignore@npm:5.3.2" @@ -4247,13 +4285,10 @@ __metadata: languageName: node linkType: hard -"import-fresh@npm:^3.0.0, import-fresh@npm:^3.2.1": - version: 3.3.1 - resolution: "import-fresh@npm:3.3.1" - dependencies: - parent-module: "npm:^1.0.0" - resolve-from: "npm:^4.0.0" - checksum: 10c0/bf8cc494872fef783249709385ae883b447e3eb09db0ebd15dcead7d9afe7224dad7bd7591c6b73b0b19b3c0f9640eb8ee884f01cfaf2887ab995b0b36a0cbec +"ignore@npm:^7.0.5": + version: 7.0.9 + resolution: "ignore@npm:7.0.9" + checksum: 10c0/fec02e67b8c01a873d3b60eff6cce5e390365d7c45fea57f60f4ff52b9be5873c366867a3b8e154a3140330b72b41b94f19e1f28adfe0ef558c6b28bbf02a1b4 languageName: node linkType: hard @@ -5024,13 +5059,6 @@ __metadata: languageName: node linkType: hard -"json-buffer@npm:3.0.1": - version: 3.0.1 - resolution: "json-buffer@npm:3.0.1" - checksum: 10c0/0d1c91569d9588e7eef2b49b59851f297f3ab93c7b35c7c221e288099322be6b562767d11e4821da500f3219542b9afd2e54c5dc573107c1126ed1080f8e96d7 - languageName: node - linkType: hard - "json-parse-even-better-errors@npm:^2.3.0": version: 2.3.1 resolution: "json-parse-even-better-errors@npm:2.3.1" @@ -5045,13 +5073,6 @@ __metadata: languageName: node linkType: hard -"json-schema-traverse@npm:^1.0.0": - version: 1.0.0 - resolution: "json-schema-traverse@npm:1.0.0" - checksum: 10c0/71e30015d7f3d6dc1c316d6298047c8ef98a06d31ad064919976583eb61e1018a60a0067338f0f79cabc00d84af3fcc489bd48ce8a46ea165d9541ba17fb30c6 - languageName: node - linkType: hard - "json-stable-stringify-without-jsonify@npm:^1.0.1": version: 1.0.1 resolution: "json-stable-stringify-without-jsonify@npm:1.0.1" @@ -5107,12 +5128,12 @@ __metadata: languageName: node linkType: hard -"keyv@npm:^4.5.3": - version: 4.5.4 - resolution: "keyv@npm:4.5.4" +"keyv@npm:^5.6.0": + version: 5.6.0 + resolution: "keyv@npm:5.6.0" dependencies: - json-buffer: "npm:3.0.1" - checksum: 10c0/aa52f3c5e18e16bb6324876bb8b59dd02acf782a4b789c7b2ae21107fab95fab3890ed448d4f8dba80ce05391eeac4bfabb4f02a20221342982f806fa2cf271e + "@keyv/serialize": "npm:^1.1.1" + checksum: 10c0/c3ea795b6e03593ca57c8f70928a69bad14c13389a7fb75649a115ff55615244b04d8902798d841c17f0bb4a8a8866c97133b543b93f151b440170bba09176db languageName: node linkType: hard @@ -5222,10 +5243,19 @@ __metadata: languageName: node linkType: hard +"locate-path@npm:^6.0.0": + version: 6.0.0 + resolution: "locate-path@npm:6.0.0" + dependencies: + p-locate: "npm:^5.0.0" + checksum: 10c0/d3972ab70dfe58ce620e64265f90162d247e87159b6126b01314dd67be43d50e96a50b517bce2d9452a79409c7614054c277b5232377de50416564a77ac7aad3 + languageName: node + linkType: hard + "lodash-es@npm:^4.17.21": - version: 4.17.21 - resolution: "lodash-es@npm:4.17.21" - checksum: 10c0/fb407355f7e6cd523a9383e76e6b455321f0f153a6c9625e21a8827d10c54c2a2341bd2ae8d034358b60e07325e1330c14c224ff582d04612a46a4f0479ff2f2 + version: 4.18.1 + resolution: "lodash-es@npm:4.18.1" + checksum: 10c0/35d4dcf87ef07f8d090f409447575800108057e360b445f590d0d25d09e3d1e33a163d2fc100d4d072b0f901d5e2fc533cd7c4bfd8eeb38a06abec693823c8b8 languageName: node linkType: hard @@ -5236,24 +5266,10 @@ __metadata: languageName: node linkType: hard -"lodash.merge@npm:^4.6.2": - version: 4.6.2 - resolution: "lodash.merge@npm:4.6.2" - checksum: 10c0/402fa16a1edd7538de5b5903a90228aa48eb5533986ba7fa26606a49db2572bf414ff73a2c9f5d5fd36b31c46a5d5c7e1527749c07cbcf965ccff5fbdf32c506 - languageName: node - linkType: hard - -"lodash.truncate@npm:^4.4.2": - version: 4.4.2 - resolution: "lodash.truncate@npm:4.4.2" - checksum: 10c0/4e870d54e8a6c86c8687e057cec4069d2e941446ccab7f40b4d9555fa5872d917d0b6aa73bece7765500a3123f1723bcdba9ae881b679ef120bba9e1a0b0ed70 - languageName: node - linkType: hard - -"lodash@npm:^4.17.21": - version: 4.17.21 - resolution: "lodash@npm:4.17.21" - checksum: 10c0/d8cbea072bb08655bb4c989da418994b073a608dffa608b09ac04b43a791b12aeae7cd7ad919aa4c925f33b48490b5cfe6c1f71d827956071dae2e7bb3a6b74c +"lodash@npm:^4.18.1": + version: 4.18.1 + resolution: "lodash@npm:4.18.1" + checksum: 10c0/757228fc68805c59789e82185135cf85f05d0b2d3d54631d680ca79ec21944ec8314d4533639a14b8bcfbd97a517e78960933041a5af17ecb693ec6eecb99a27 languageName: node linkType: hard @@ -5388,7 +5404,7 @@ __metadata: languageName: node linkType: hard -"merge2@npm:^1.2.3, merge2@npm:^1.3.0, merge2@npm:^1.4.1": +"merge2@npm:^1.2.3, merge2@npm:^1.3.0": version: 1.4.1 resolution: "merge2@npm:1.4.1" checksum: 10c0/254a8a4605b58f450308fc474c82ac9a094848081bf4c06778200207820e5193726dc563a0d2c16468810516a5c97d9d3ea0ca6585d23c58ccfff2403e8dbbeb @@ -5465,12 +5481,21 @@ __metadata: languageName: node linkType: hard +"minimatch@npm:^10.2.2, minimatch@npm:^10.2.4, minimatch@npm:^10.2.5": + version: 10.2.6 + resolution: "minimatch@npm:10.2.6" + dependencies: + brace-expansion: "npm:^5.0.8" + checksum: 10c0/4559a836243b98bd4d17ea9f7edae698717c76399eea7be374f3737f33164e4907f19e9726891ddeb122f750a5a7fa80d2ac43e851d6e5984dc4ff42ec127d3a + languageName: node + linkType: hard + "minimatch@npm:^3.0.4, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": - version: 3.1.2 - resolution: "minimatch@npm:3.1.2" + version: 3.1.5 + resolution: "minimatch@npm:3.1.5" dependencies: brace-expansion: "npm:^1.1.7" - checksum: 10c0/0262810a8fc2e72cca45d6fd86bd349eee435eb95ac6aa45c9ea2180e7ee875ef44c32b55b5973ceabe95ea12682f6e3725cbb63d7a2d1da3ae1163c8b210311 + checksum: 10c0/2ecbdc0d33f07bddb0315a8b5afbcb761307a8778b48f0b312418ccbced99f104a2d17d8aca7573433c70e8ccd1c56823a441897a45e384ea76ef401a26ace70 languageName: node linkType: hard @@ -5575,6 +5600,15 @@ __metadata: languageName: node linkType: hard +"minizlib@npm:^3.1.0": + version: 3.1.0 + resolution: "minizlib@npm:3.1.0" + dependencies: + minipass: "npm:^7.1.2" + checksum: 10c0/5aad75ab0090b8266069c9aabe582c021ae53eb33c6c691054a13a45db3b4f91a7fb1bd79151e6b4e9e9a86727b522527c0a06ec7d45206b745d54cd3097bcec + languageName: node + linkType: hard + "mkdirp@npm:^0.5.1": version: 0.5.6 resolution: "mkdirp@npm:0.5.6" @@ -5586,15 +5620,6 @@ __metadata: languageName: node linkType: hard -"mkdirp@npm:^3.0.1": - version: 3.0.1 - resolution: "mkdirp@npm:3.0.1" - bin: - mkdirp: dist/cjs/src/bin.js - checksum: 10c0/9f2b975e9246351f5e3a40dcfac99fcd0baa31fbfab615fe059fb11e51f10e4803c63de1f384c54d656e4db31d000e4767e9ef076a22e12a641357602e31d57d - languageName: node - linkType: hard - "mod-info@npm:^1.0.0": version: 1.0.2 resolution: "mod-info@npm:1.0.2" @@ -5700,13 +5725,6 @@ __metadata: languageName: node linkType: hard -"natural-compare-lite@npm:^1.4.0": - version: 1.4.0 - resolution: "natural-compare-lite@npm:1.4.0" - checksum: 10c0/f6cef26f5044515754802c0fc475d81426f3b90fe88c20fabe08771ce1f736ce46e0397c10acb569a4dd0acb84c7f1ee70676122f95d5bfdd747af3a6c6bbaa8 - languageName: node - linkType: hard - "natural-compare@npm:^1.4.0": version: 1.4.0 resolution: "natural-compare@npm:1.4.0" @@ -5892,7 +5910,7 @@ __metadata: languageName: node linkType: hard -"optionator@npm:^0.9.1": +"optionator@npm:^0.9.3": version: 0.9.4 resolution: "optionator@npm:0.9.4" dependencies: @@ -5915,7 +5933,7 @@ __metadata: languageName: node linkType: hard -"p-limit@npm:^3.1.0": +"p-limit@npm:^3.0.2, p-limit@npm:^3.1.0": version: 3.1.0 resolution: "p-limit@npm:3.1.0" dependencies: @@ -5933,6 +5951,15 @@ __metadata: languageName: node linkType: hard +"p-locate@npm:^5.0.0": + version: 5.0.0 + resolution: "p-locate@npm:5.0.0" + dependencies: + p-limit: "npm:^3.0.2" + checksum: 10c0/2290d627ab7903b8b70d11d384fee714b797f6040d9278932754a6860845c4d3190603a0772a663c8cb5a7b21d1b16acb3a6487ebcafa9773094edc3dfe6009a + languageName: node + linkType: hard + "p-map@npm:^7.0.2": version: 7.0.3 resolution: "p-map@npm:7.0.3" @@ -5954,15 +5981,6 @@ __metadata: languageName: node linkType: hard -"parent-module@npm:^1.0.0": - version: 1.0.1 - resolution: "parent-module@npm:1.0.1" - dependencies: - callsites: "npm:^3.0.0" - checksum: 10c0/c63d6e80000d4babd11978e0d3fee386ca7752a02b035fd2435960ffaa7219dc42146f07069fb65e6e8bf1caef89daf9af7535a39bddf354d78bf50d8294f556 - languageName: node - linkType: hard - "parse-json@npm:^5.2.0": version: 5.2.0 resolution: "parse-json@npm:5.2.0" @@ -6041,7 +6059,7 @@ __metadata: languageName: node linkType: hard -"picocolors@npm:^1.0.0, picocolors@npm:^1.1.1": +"picocolors@npm:^1.1.1": version: 1.1.1 resolution: "picocolors@npm:1.1.1" checksum: 10c0/e2e3e8170ab9d7c7421969adaa7e1b31434f789afb9b3f115f6b96d91945041ac3ceb02e9ec6fe6510ff036bcc0bf91e69a1772edc0b707e12b19c0f2d6bcf58 @@ -6062,6 +6080,13 @@ __metadata: languageName: node linkType: hard +"picomatch@npm:^4.0.4": + version: 4.0.7 + resolution: "picomatch@npm:4.0.7" + checksum: 10c0/beb6ae02c43ae44e84883b90830196d9046b1726ead292adcf7f57945e0bb0d992d68563d87e03b484b6f3c9a5c6defda7523477f047d7f0e663f126cc01787f + languageName: node + linkType: hard + "pino-abstract-transport@npm:^3.0.0": version: 3.0.0 resolution: "pino-abstract-transport@npm:3.0.0" @@ -6145,15 +6170,6 @@ __metadata: languageName: node linkType: hard -"prettier-linter-helpers@npm:^1.0.0": - version: 1.0.0 - resolution: "prettier-linter-helpers@npm:1.0.0" - dependencies: - fast-diff: "npm:^1.1.2" - checksum: 10c0/81e0027d731b7b3697ccd2129470ed9913ecb111e4ec175a12f0fcfab0096516373bf0af2fef132af50cafb0a905b74ff57996d615f59512bb9ac7378fcc64ab - languageName: node - linkType: hard - "prettier@npm:^2.8.8": version: 2.8.8 resolution: "prettier@npm:2.8.8" @@ -6188,13 +6204,6 @@ __metadata: languageName: node linkType: hard -"progress@npm:^2.0.0": - version: 2.0.3 - resolution: "progress@npm:2.0.3" - checksum: 10c0/1697e07cb1068055dbe9fe858d242368ff5d2073639e652b75a7eb1f2a1a8d4afd404d719de23c7b48481a6aa0040686310e2dac2f53d776daa2176d3f96369c - languageName: node - linkType: hard - "promise-retry@npm:^2.0.1": version: 2.0.1 resolution: "promise-retry@npm:2.0.1" @@ -6256,6 +6265,15 @@ __metadata: languageName: node linkType: hard +"qified@npm:^0.10.1": + version: 0.10.1 + resolution: "qified@npm:0.10.1" + dependencies: + hookified: "npm:^2.1.1" + checksum: 10c0/4a39d45492c65a4b9795381acede1bd566028ce9764ead5b41d776b391a898bf0855dd2b59d724a6711fe253ba3cacf7c9a8832887cf9dc6d48a2c5e4997c82d + languageName: node + linkType: hard + "qs@npm:6.13.0": version: 6.13.0 resolution: "qs@npm:6.13.0" @@ -6353,13 +6371,6 @@ __metadata: languageName: node linkType: hard -"regexpp@npm:^3.1.0": - version: 3.2.0 - resolution: "regexpp@npm:3.2.0" - checksum: 10c0/d1da82385c8754a1681416b90b9cca0e21b4a2babef159099b88f640637d789c69011d0bc94705dacab85b81133e929d027d85210e8b8b03f8035164dbc14710 - languageName: node - linkType: hard - "require-directory@npm:^2.1.1": version: 2.1.1 resolution: "require-directory@npm:2.1.1" @@ -6367,13 +6378,6 @@ __metadata: languageName: node linkType: hard -"require-from-string@npm:^2.0.2": - version: 2.0.2 - resolution: "require-from-string@npm:2.0.2" - checksum: 10c0/aaa267e0c5b022fc5fd4eef49d8285086b15f2a1c54b28240fdf03599cbd9c26049fee3eab894f2e1f6ca65e513b030a7c264201e3f005601e80c49fb2937ce2 - languageName: node - linkType: hard - "resolve-cwd@npm:^3.0.0": version: 3.0.0 resolution: "resolve-cwd@npm:3.0.0" @@ -6383,13 +6387,6 @@ __metadata: languageName: node linkType: hard -"resolve-from@npm:^4.0.0": - version: 4.0.0 - resolution: "resolve-from@npm:4.0.0" - checksum: 10c0/8408eec31a3112ef96e3746c37be7d64020cda07c03a920f5024e77290a218ea758b26ca9529fd7b1ad283947f34b2291c1c0f6aa0ed34acfdda9c6014c8d190 - languageName: node - linkType: hard - "resolve-from@npm:^5.0.0": version: 5.0.0 resolution: "resolve-from@npm:5.0.0" @@ -6444,17 +6441,6 @@ __metadata: languageName: node linkType: hard -"rimraf@npm:^3.0.2": - version: 3.0.2 - resolution: "rimraf@npm:3.0.2" - dependencies: - glob: "npm:^7.1.3" - bin: - rimraf: bin.js - checksum: 10c0/9cb7757acb489bd83757ba1a274ab545eafd75598a9d817e0c3f8b164238dd90eba50d6b848bd4dcc5f3040912e882dc7ba71653e35af660d77b25c381d402e8 - languageName: node - linkType: hard - "rimraf@npm:^5.0.5": version: 5.0.10 resolution: "rimraf@npm:5.0.10" @@ -6530,7 +6516,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:^7.2.1, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.3.8, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.7.2": +"semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.8, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.7.2": version: 7.7.2 resolution: "semver@npm:7.7.2" bin: @@ -6539,6 +6525,15 @@ __metadata: languageName: node linkType: hard +"semver@npm:^7.7.3": + version: 7.8.5 + resolution: "semver@npm:7.8.5" + bin: + semver: bin/semver.js + checksum: 10c0/b1f3127a5be8125a94f37188b361c212466c292c6910adce3ec106cff5dc211ccaedc4739c11bb70fda59d6fc1f040a9bca289f4e093451521a2372e5231fe0c + languageName: node + linkType: hard + "send@npm:0.19.0": version: 0.19.0 resolution: "send@npm:0.19.0" @@ -6694,17 +6689,6 @@ __metadata: languageName: node linkType: hard -"slice-ansi@npm:^4.0.0": - version: 4.0.0 - resolution: "slice-ansi@npm:4.0.0" - dependencies: - ansi-styles: "npm:^4.0.0" - astral-regex: "npm:^2.0.0" - is-fullwidth-code-point: "npm:^3.0.0" - checksum: 10c0/6c25678db1270d4793e0327620f1e0f9f5bea4630123f51e9e399191bc52c87d6e6de53ed33538609e5eacbd1fab769fae00f3705d08d029f02102a540648918 - languageName: node - linkType: hard - "smart-buffer@npm:^4.2.0": version: 4.2.0 resolution: "smart-buffer@npm:4.2.0" @@ -6735,12 +6719,12 @@ __metadata: linkType: hard "socket.io-parser@npm:~4.2.4": - version: 4.2.4 - resolution: "socket.io-parser@npm:4.2.4" + version: 4.2.7 + resolution: "socket.io-parser@npm:4.2.7" dependencies: "@socket.io/component-emitter": "npm:~3.1.0" - debug: "npm:~4.3.1" - checksum: 10c0/9383b30358fde4a801ea4ec5e6860915c0389a091321f1c1f41506618b5cf7cd685d0a31c587467a0c4ee99ef98c2b99fb87911f9dfb329716c43b587f29ca48 + debug: "npm:~4.4.1" + checksum: 10c0/16a5579718b871114ca644d688987dd3cf9292010c949fb083633eefbc1d8fdaf424691d6511d746e5c10f5c88cbd8911cd0b1e807242159f40989be90842b4e languageName: node linkType: hard @@ -6942,7 +6926,7 @@ __metadata: languageName: node linkType: hard -"strip-json-comments@npm:^3.1.0, strip-json-comments@npm:^3.1.1": +"strip-json-comments@npm:^3.1.1": version: 3.1.1 resolution: "strip-json-comments@npm:3.1.1" checksum: 10c0/9681a6257b925a7fa0f285851c0e613cc934a50661fa7bb41ca9cbbff89686bb4a0ee366e6ecedc4daafd01e83eee0720111ab294366fe7c185e935475ebcecd @@ -6984,7 +6968,7 @@ __metadata: languageName: node linkType: hard -"supports-color@npm:^5.3.0, supports-color@npm:^5.5.0": +"supports-color@npm:^5.5.0": version: 5.5.0 resolution: "supports-color@npm:5.5.0" dependencies: @@ -7018,30 +7002,16 @@ __metadata: languageName: node linkType: hard -"table@npm:^6.0.9": - version: 6.9.0 - resolution: "table@npm:6.9.0" - dependencies: - ajv: "npm:^8.0.1" - lodash.truncate: "npm:^4.4.2" - slice-ansi: "npm:^4.0.0" - string-width: "npm:^4.2.3" - strip-ansi: "npm:^6.0.1" - checksum: 10c0/35646185712bb65985fbae5975dda46696325844b78735f95faefae83e86df0a265277819a3e67d189de6e858c509b54e66ca3958ffd51bde56ef1118d455bf4 - languageName: node - linkType: hard - "tar@npm:^7.4.3": - version: 7.4.3 - resolution: "tar@npm:7.4.3" + version: 7.5.22 + resolution: "tar@npm:7.5.22" dependencies: "@isaacs/fs-minipass": "npm:^4.0.0" chownr: "npm:^3.0.0" minipass: "npm:^7.1.2" - minizlib: "npm:^3.0.1" - mkdirp: "npm:^3.0.1" + minizlib: "npm:^3.1.0" yallist: "npm:^5.0.0" - checksum: 10c0/d4679609bb2a9b48eeaf84632b6d844128d2412b95b6de07d53d8ee8baf4ca0857c9331dfa510390a0727b550fd543d4d1a10995ad86cdf078423fbb8d99831d + checksum: 10c0/1311f6be85a8157ac4c9147bae43e13923d2a1aae15e4aa1bd5239e4e03d2cf53cfe103dde7f35832fbb4c938b042856bc8e9a0afd29abd05e2d1608788c4fea languageName: node linkType: hard @@ -7056,13 +7026,6 @@ __metadata: languageName: node linkType: hard -"text-table@npm:^0.2.0": - version: 0.2.0 - resolution: "text-table@npm:0.2.0" - checksum: 10c0/02805740c12851ea5982686810702e2f14369a5f4c5c40a836821e3eefc65ffeec3131ba324692a37608294b0fd8c1e55a2dd571ffed4909822787668ddbee5c - languageName: node - linkType: hard - "thread-stream@npm:^4.0.0": version: 4.2.0 resolution: "thread-stream@npm:4.2.0" @@ -7092,6 +7055,16 @@ __metadata: languageName: node linkType: hard +"tinyglobby@npm:^0.2.15": + version: 0.2.17 + resolution: "tinyglobby@npm:0.2.17" + dependencies: + fdir: "npm:^6.5.0" + picomatch: "npm:^4.0.4" + checksum: 10c0/7f7bb0f197c88bc4b20c231e0deca4240ca3bf313a88f5a7fee93a872b84966a4d50220947c0455ad07a60b3b360961c5b7fd979222aeb716a9f99b412002e4c + languageName: node + linkType: hard + "tmpl@npm:1.0.5": version: 1.0.5 resolution: "tmpl@npm:1.0.5" @@ -7140,6 +7113,15 @@ __metadata: languageName: node linkType: hard +"ts-api-utils@npm:^2.5.0": + version: 2.5.0 + resolution: "ts-api-utils@npm:2.5.0" + peerDependencies: + typescript: ">=4.8.4" + checksum: 10c0/767849383c114e7f1971fa976b20e73ac28fd0c70d8d65c0004790bf4d8f89888c7e4cf6d5949f9c1beae9bc3c64835bef77bbe27fddf45a3c7b60cebcf85c8c + languageName: node + linkType: hard + "ts-jest@npm:^29.0.0": version: 29.4.1 resolution: "ts-jest@npm:29.4.1" @@ -7230,13 +7212,6 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^1.8.1": - version: 1.14.1 - resolution: "tslib@npm:1.14.1" - checksum: 10c0/69ae09c49eea644bc5ebe1bca4fa4cc2c82b7b3e02f43b84bd891504edf66dbc6b2ec0eef31a957042de2269139e4acff911e6d186a258fb14069cd7f6febce2 - languageName: node - linkType: hard - "tsscmp@npm:1.0.6": version: 1.0.6 resolution: "tsscmp@npm:1.0.6" @@ -7244,17 +7219,6 @@ __metadata: languageName: node linkType: hard -"tsutils@npm:^3.21.0": - version: 3.21.0 - resolution: "tsutils@npm:3.21.0" - dependencies: - tslib: "npm:^1.8.1" - peerDependencies: - typescript: ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" - checksum: 10c0/02f19e458ec78ead8fffbf711f834ad8ecd2cc6ade4ec0320790713dccc0a412b99e7fd907c4cda2a1dc602c75db6f12e0108e87a5afad4b2f9e90a24cabd5a2 - languageName: node - linkType: hard - "type-check@npm:^0.4.0, type-check@npm:~0.4.0": version: 0.4.0 resolution: "type-check@npm:0.4.0" @@ -7271,13 +7235,6 @@ __metadata: languageName: node linkType: hard -"type-fest@npm:^0.20.2": - version: 0.20.2 - resolution: "type-fest@npm:0.20.2" - checksum: 10c0/dea9df45ea1f0aaa4e2d3bed3f9a0bfe9e5b2592bddb92eb1bf06e50bcf98dbb78189668cd8bc31a0511d3fc25539b4cd5c704497e53e93e2d40ca764b10bfc3 - languageName: node - linkType: hard - "type-fest@npm:^0.21.3": version: 0.21.3 resolution: "type-fest@npm:0.21.3" @@ -7309,6 +7266,21 @@ __metadata: languageName: node linkType: hard +"typescript-eslint@npm:^8.70.0": + version: 8.70.0 + resolution: "typescript-eslint@npm:8.70.0" + dependencies: + "@typescript-eslint/eslint-plugin": "npm:8.70.0" + "@typescript-eslint/parser": "npm:8.70.0" + "@typescript-eslint/typescript-estree": "npm:8.70.0" + "@typescript-eslint/utils": "npm:8.70.0" + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.1.0" + checksum: 10c0/bf6625631bfcc7392bb1403681ef934f4f428c41a6a090b081a77ed0d342326c31097956900d701d078ac0c8a599d53377b504455fa9ce7214365c4c8d4635a3 + languageName: node + linkType: hard + "typescript@npm:^4.1.0": version: 4.9.5 resolution: "typescript@npm:4.9.5" @@ -7448,13 +7420,6 @@ __metadata: languageName: node linkType: hard -"v8-compile-cache@npm:^2.0.3": - version: 2.4.0 - resolution: "v8-compile-cache@npm:2.4.0" - checksum: 10c0/387851192545e7f4d691ba674de90890bba76c0f08ee4909ab862377f556221e75b3a361466490e201203401d64d7795f889882bdabc98b6f3c0bf1038a535be - languageName: node - linkType: hard - "v8-to-istanbul@npm:^9.0.1": version: 9.3.0 resolution: "v8-to-istanbul@npm:9.3.0"