From 464c77f0d8f3b0d367323c3cc6ab2e4db722d6b1 Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Sun, 8 Jun 2025 20:40:53 +0200 Subject: [PATCH 01/81] feat: implement basic proto file --- .../backend/wrapper/proto/wrapper.proto | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 Tokenization/backend/wrapper/proto/wrapper.proto diff --git a/Tokenization/backend/wrapper/proto/wrapper.proto b/Tokenization/backend/wrapper/proto/wrapper.proto new file mode 100644 index 000000000..e65311c6e --- /dev/null +++ b/Tokenization/backend/wrapper/proto/wrapper.proto @@ -0,0 +1,27 @@ +syntax = "proto3"; +package wrapper; + +// ====================================== +// MESSAGES +// ====================================== +// Simulates an empty message because protobuffer doesn't support void +message EmptyMessage {} + +// Message with newly generated token and target address binded to it +message NewToken { + string token = 1; + string targetAddress = 2; +} + + +// ====================================== +// SERVICES +// ====================================== + +service ClientService { + // Sends newly generated token to the client for specific Connection + rpc sendNewToken(NewToken) returns(EmptyMessage) {} + + // Informs the client about token revocation + rpc revokeToken(NewToken) returns(EmptyMessage) {} +} \ No newline at end of file From 268a99af92653627994fd90a247fbca5d77265a3 Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Tue, 10 Jun 2025 11:14:57 +0200 Subject: [PATCH 02/81] feat: Duplex stream with specific payload message events. --- .../backend/wrapper/proto/wrapper.proto | 37 +++++++++++++++---- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/Tokenization/backend/wrapper/proto/wrapper.proto b/Tokenization/backend/wrapper/proto/wrapper.proto index e65311c6e..bdb4d1560 100644 --- a/Tokenization/backend/wrapper/proto/wrapper.proto +++ b/Tokenization/backend/wrapper/proto/wrapper.proto @@ -1,27 +1,48 @@ syntax = "proto3"; package wrapper; +// ====================================== +// ENUMS +// ====================================== +enum MessageEvent { + // Default value, represents an empty event + EMPTY_EVENT = 0; + + // New token message type, contains a new token and target address + NEW_TOKEN = 1; + + // Revoke token message type, contains a token to be revoked + REVOKE_TOKEN = 2; +} + + // ====================================== // MESSAGES // ====================================== // Simulates an empty message because protobuffer doesn't support void message EmptyMessage {} -// Message with newly generated token and target address binded to it -message NewToken { +// Message with token and target address binded to it +message Token { string token = 1; string targetAddress = 2; } +// Stream message that can contain one of specific messages +message Payload { + MessageEvent event = 1; + oneof { + EmptyMessage emptyMessage = 2; + Token newToken = 3; + Token revokeToken = 4; + } +} + // ====================================== // SERVICES // ====================================== -service ClientService { - // Sends newly generated token to the client for specific Connection - rpc sendNewToken(NewToken) returns(EmptyMessage) {} - - // Informs the client about token revocation - rpc revokeToken(NewToken) returns(EmptyMessage) {} +service CentralSystem { + rpc ClientStream(stream Payload) returns (stream Payload); } \ No newline at end of file From 7b7888aebf0201151af33bac558c56ed635b5b1a Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Tue, 10 Jun 2025 11:18:57 +0200 Subject: [PATCH 03/81] feat: Duplex stream with specific payload message events. --- .../backend/wrapper/proto/wrapper.proto | 37 +++++++++++++++---- Tokenization/backend/wrapper/wrapper.proto | 27 ++++++++++++++ 2 files changed, 56 insertions(+), 8 deletions(-) create mode 100644 Tokenization/backend/wrapper/wrapper.proto diff --git a/Tokenization/backend/wrapper/proto/wrapper.proto b/Tokenization/backend/wrapper/proto/wrapper.proto index e65311c6e..bdb4d1560 100644 --- a/Tokenization/backend/wrapper/proto/wrapper.proto +++ b/Tokenization/backend/wrapper/proto/wrapper.proto @@ -1,27 +1,48 @@ syntax = "proto3"; package wrapper; +// ====================================== +// ENUMS +// ====================================== +enum MessageEvent { + // Default value, represents an empty event + EMPTY_EVENT = 0; + + // New token message type, contains a new token and target address + NEW_TOKEN = 1; + + // Revoke token message type, contains a token to be revoked + REVOKE_TOKEN = 2; +} + + // ====================================== // MESSAGES // ====================================== // Simulates an empty message because protobuffer doesn't support void message EmptyMessage {} -// Message with newly generated token and target address binded to it -message NewToken { +// Message with token and target address binded to it +message Token { string token = 1; string targetAddress = 2; } +// Stream message that can contain one of specific messages +message Payload { + MessageEvent event = 1; + oneof { + EmptyMessage emptyMessage = 2; + Token newToken = 3; + Token revokeToken = 4; + } +} + // ====================================== // SERVICES // ====================================== -service ClientService { - // Sends newly generated token to the client for specific Connection - rpc sendNewToken(NewToken) returns(EmptyMessage) {} - - // Informs the client about token revocation - rpc revokeToken(NewToken) returns(EmptyMessage) {} +service CentralSystem { + rpc ClientStream(stream Payload) returns (stream Payload); } \ No newline at end of file diff --git a/Tokenization/backend/wrapper/wrapper.proto b/Tokenization/backend/wrapper/wrapper.proto new file mode 100644 index 000000000..e65311c6e --- /dev/null +++ b/Tokenization/backend/wrapper/wrapper.proto @@ -0,0 +1,27 @@ +syntax = "proto3"; +package wrapper; + +// ====================================== +// MESSAGES +// ====================================== +// Simulates an empty message because protobuffer doesn't support void +message EmptyMessage {} + +// Message with newly generated token and target address binded to it +message NewToken { + string token = 1; + string targetAddress = 2; +} + + +// ====================================== +// SERVICES +// ====================================== + +service ClientService { + // Sends newly generated token to the client for specific Connection + rpc sendNewToken(NewToken) returns(EmptyMessage) {} + + // Informs the client about token revocation + rpc revokeToken(NewToken) returns(EmptyMessage) {} +} \ No newline at end of file From 637354826f21cf43cdc4712d6cc7bebca2e2ef2f Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Tue, 10 Jun 2025 11:25:16 +0200 Subject: [PATCH 04/81] feat: Create typescript interfaces for duplex stream message model --- .../backend/wrapper/models/message.model.ts | 15 +++++++++++ Tokenization/backend/wrapper/wrapper.proto | 27 ------------------- 2 files changed, 15 insertions(+), 27 deletions(-) create mode 100644 Tokenization/backend/wrapper/models/message.model.ts delete mode 100644 Tokenization/backend/wrapper/wrapper.proto diff --git a/Tokenization/backend/wrapper/models/message.model.ts b/Tokenization/backend/wrapper/models/message.model.ts new file mode 100644 index 000000000..36cdf5e12 --- /dev/null +++ b/Tokenization/backend/wrapper/models/message.model.ts @@ -0,0 +1,15 @@ +enum DuplexMessageEvent { + EMPTY_EVENT, + NEW_TOKEN, + REVOKE_TOKEN, +} + +interface TokenMessage { + token: string; + targetAddress: string; +} + +interface DuplexMessageModel { + event: DuplexMessageEvent; + data?: TokenMessage; +} diff --git a/Tokenization/backend/wrapper/wrapper.proto b/Tokenization/backend/wrapper/wrapper.proto deleted file mode 100644 index e65311c6e..000000000 --- a/Tokenization/backend/wrapper/wrapper.proto +++ /dev/null @@ -1,27 +0,0 @@ -syntax = "proto3"; -package wrapper; - -// ====================================== -// MESSAGES -// ====================================== -// Simulates an empty message because protobuffer doesn't support void -message EmptyMessage {} - -// Message with newly generated token and target address binded to it -message NewToken { - string token = 1; - string targetAddress = 2; -} - - -// ====================================== -// SERVICES -// ====================================== - -service ClientService { - // Sends newly generated token to the client for specific Connection - rpc sendNewToken(NewToken) returns(EmptyMessage) {} - - // Informs the client about token revocation - rpc revokeToken(NewToken) returns(EmptyMessage) {} -} \ No newline at end of file From 255e56751ecbe2e44ae8e10bd76bfc86de88e4ee Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Tue, 10 Jun 2025 11:37:36 +0200 Subject: [PATCH 05/81] feat: Add description to typescript interfaces --- .../backend/wrapper/models/message.model.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Tokenization/backend/wrapper/models/message.model.ts b/Tokenization/backend/wrapper/models/message.model.ts index 36cdf5e12..3a90dd739 100644 --- a/Tokenization/backend/wrapper/models/message.model.ts +++ b/Tokenization/backend/wrapper/models/message.model.ts @@ -1,14 +1,30 @@ +/** + * @description Enum for duplex message events. + * EMPTY_EVENT: No event, used for initialization or no response. + * NEW_TOKEN: Event for replacing with newly generated token. + * REVOKE_TOKEN: Event for revoking an existing token. + */ enum DuplexMessageEvent { EMPTY_EVENT, NEW_TOKEN, REVOKE_TOKEN, } +/** + * @description Model for token generation and revocation messages. + * @property {string} token - The token to be replaced or revoked. + * @property {string} targetAddress - The address of connection binded to this token. + */ interface TokenMessage { token: string; targetAddress: string; } +/** + * @description Model for duplex stream messages between client and central system. + * @property {DuplexMessageEvent} event - The event type of the message. + * @property {TokenMessage} [data] - The data associated with the event, it may be undefined for some events. + */ interface DuplexMessageModel { event: DuplexMessageEvent; data?: TokenMessage; From 7edca58abd7e5b8434c449ceefd03ed02e00c2df Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Tue, 10 Jun 2025 11:43:36 +0200 Subject: [PATCH 06/81] feat: implement serialization and deserialization methods --- .../__tests__/serialization.utils.test.ts | 0 .../wrapper/utils/serialization.utils.ts | 64 +++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 Tokenization/backend/wrapper/utils/__tests__/serialization.utils.test.ts create mode 100644 Tokenization/backend/wrapper/utils/serialization.utils.ts diff --git a/Tokenization/backend/wrapper/utils/__tests__/serialization.utils.test.ts b/Tokenization/backend/wrapper/utils/__tests__/serialization.utils.test.ts new file mode 100644 index 000000000..e69de29bb diff --git a/Tokenization/backend/wrapper/utils/serialization.utils.ts b/Tokenization/backend/wrapper/utils/serialization.utils.ts new file mode 100644 index 000000000..2f1e9bf41 --- /dev/null +++ b/Tokenization/backend/wrapper/utils/serialization.utils.ts @@ -0,0 +1,64 @@ +/** + * @description Serializes Json formatted request into binary payload with specific endpoint + * @param url - The endpoint URL to which the request is made + * @param options - Optional request options, such as headers or body + * @return {ArrayBuffer} - The serialized binary payload containing the URL and options + */ +export const serializeRequest = (url: string, options?: any) => { + const encoder = new TextEncoder(); + const contentTypeBytes = encoder.encode("application/json"); + + // build JSON data + const jsonData = options + ? { + url: url, + options: options, + } + : { + url: url, + }; + + // encode JSON + const jsonString = JSON.stringify(jsonData); + const jsonBytes = encoder.encode(jsonString); + + // Buffer following structure: + // 1 byte -> type length + // N bytes -> types + // rest -> JSON data + const buffer = new Uint8Array(1 + contentTypeBytes.length + jsonBytes.length); + + buffer[0] = contentTypeBytes.length; + buffer.set(contentTypeBytes, 1); + buffer.set(jsonBytes, 1 + contentTypeBytes.length); + + return buffer.buffer; +}; + +/** + * @description Deserializes binary payload to Json formated request + * @param payload - The binary payload to deserialize + * @return {any} - The deserialized request object containing the URL and options + */ +export const deserializeRequest = (payload: any) => { + const view = new Uint8Array(payload); + const decoder = new TextDecoder(); + + const contentTypeLength = view[0]; + const contentTypeBytes = view.slice(1, 1 + contentTypeLength); + const contentType = decoder.decode(contentTypeBytes); + + const dataBytes = view.slice(1 + contentTypeLength); + let data; + + // deserialization based on content type + switch (contentType) { + case "application/json": + data = JSON.parse(decoder.decode(dataBytes)); + break; + default: + throw new Error(`Unsupported content type: ${contentType}`); + } + + return data; +}; From 0485204c2a8147befef400b48c9d8943b3e4ff94 Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Tue, 10 Jun 2025 22:29:50 +0200 Subject: [PATCH 07/81] feat: add package dependencies and write unit tests for serialization utils --- Tokenization/backend/wrapper/jest.config.js | 13 + .../backend/wrapper/package-lock.json | 4007 +++++++++++++++++ Tokenization/backend/wrapper/package.json | 20 + .../__tests__/serialization.utils.test.ts | 71 + 4 files changed, 4111 insertions(+) create mode 100644 Tokenization/backend/wrapper/jest.config.js create mode 100644 Tokenization/backend/wrapper/package-lock.json create mode 100644 Tokenization/backend/wrapper/package.json diff --git a/Tokenization/backend/wrapper/jest.config.js b/Tokenization/backend/wrapper/jest.config.js new file mode 100644 index 000000000..3a5675ad5 --- /dev/null +++ b/Tokenization/backend/wrapper/jest.config.js @@ -0,0 +1,13 @@ +/** @type {import('jest').Config} */ +const config = { + verbose: true, + transform: { + "^.+\\.ts$": ["ts-jest", { useESM: true }], + }, + extensionsToTreatAsEsm: [".ts"], + moduleNameMapper: { + "^(\\.{1,2}/.*)\\.js$": "$1", + }, +}; + +export default config; diff --git a/Tokenization/backend/wrapper/package-lock.json b/Tokenization/backend/wrapper/package-lock.json new file mode 100644 index 000000000..500302d37 --- /dev/null +++ b/Tokenization/backend/wrapper/package-lock.json @@ -0,0 +1,4007 @@ +{ + "name": "grpc-wrapper", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "grpc-wrapper", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "ts-node": "^10.9.2", + "typescript": "^5.8.3" + }, + "devDependencies": { + "@types/jest": "^29.5.14", + "jest": "^29.7.0", + "ts-jest": "^29.3.4" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.27.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.5.tgz", + "integrity": "sha512-KiRAp/VoJaWkkte84TvUd9qjdbZAdiqyvMxrGl1N6vzFogKmaLgoM3L1kgtLicp2HP5fBJS8JrZKLVIZGVJAVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.27.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.4.tgz", + "integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.27.3", + "@babel/helpers": "^7.27.4", + "@babel/parser": "^7.27.4", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.27.4", + "@babel/types": "^7.27.3", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.27.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.5.tgz", + "integrity": "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.27.5", + "@babel/types": "^7.27.3", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", + "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz", + "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.27.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.5.tgz", + "integrity": "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.27.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.4.tgz", + "integrity": "sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.3", + "@babel/parser": "^7.27.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.3", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.6.tgz", + "integrity": "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", + "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/node": { + "version": "24.0.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.0.tgz", + "integrity": "sha512-yZQa2zm87aRVcqDyH5+4Hv9KYgSdgwX1rFnGvpbzMaC7YAljmhBET93TPiTd3ObwTL+gSpIzPKg5BqVxdCvxKg==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.8.0" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "license": "MIT" + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", + "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.0.tgz", + "integrity": "sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001718", + "electron-to-chromium": "^1.5.160", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001721", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001721.tgz", + "integrity": "sha512-cOuvmUVtKrtEaoKiO0rSc29jcjwMwX5tOHDy4MgVFEWiUXj4uBMJkwI8MDySkgXidpMiHUcviogAvFi4pA2hDQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", + "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.166", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.166.tgz", + "integrity": "sha512-QPWqHL0BglzPYyJJ1zSSmwFFL6MFXhbACOCcsCdUMCkzPdS9/OIBVxg516X/Ado2qwAq8k0nJJ7phQPCqiaFAw==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "license": "ISC" + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-jest": { + "version": "29.3.4", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.3.4.tgz", + "integrity": "sha512-Iqbrm8IXOmV+ggWHOTEbjwyCf2xZlUMv5npExksXohL+tk8va4Fjhb+X2+Rt9NBmgO7bJ8WpnMLOwih/DnMlFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "ejs": "^3.1.10", + "fast-json-stable-stringify": "^2.1.0", + "jest-util": "^29.0.0", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.7.2", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0", + "@jest/types": "^29.0.0", + "babel-jest": "^29.0.0", + "jest": "^29.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "license": "MIT" + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/Tokenization/backend/wrapper/package.json b/Tokenization/backend/wrapper/package.json new file mode 100644 index 000000000..a8b9f98a6 --- /dev/null +++ b/Tokenization/backend/wrapper/package.json @@ -0,0 +1,20 @@ +{ + "name": "grpc-wrapper", + "version": "1.0.0", + "type": "module", + "scripts": { + "test": "mocha --loader ts-node/esm \"./**/*.test.ts\"" + }, + "author": "", + "license": "ISC", + "description": "", + "devDependencies": { + "@types/jest": "^29.5.14", + "jest": "^29.7.0", + "ts-jest": "^29.3.4" + }, + "dependencies": { + "ts-node": "^10.9.2", + "typescript": "^5.8.3" + } +} diff --git a/Tokenization/backend/wrapper/utils/__tests__/serialization.utils.test.ts b/Tokenization/backend/wrapper/utils/__tests__/serialization.utils.test.ts index e69de29bb..655b0ccef 100644 --- a/Tokenization/backend/wrapper/utils/__tests__/serialization.utils.test.ts +++ b/Tokenization/backend/wrapper/utils/__tests__/serialization.utils.test.ts @@ -0,0 +1,71 @@ +import { deserializeRequest, serializeRequest } from "../serialization.utils"; +import { describe, expect, test } from "@jest/globals"; + +describe("serializeRequest", () => { + test("serializes URL and options correctly", () => { + const url = "/api/test"; + const options = { + method: "POST", + headers: { "Content-Type": "application/json" }, + }; + + const buffer = serializeRequest(url, options); + const view = new Uint8Array(buffer); + + const typeLength = view[0]; + const typeBytes = view.slice(1, 1 + typeLength); + const jsonBytes = view.slice(1 + typeLength); + + const contentType = new TextDecoder().decode(typeBytes); + const json = JSON.parse(new TextDecoder().decode(jsonBytes)); + + expect(contentType).toBe("application/json"); + expect(json.url).toBe(url); + expect(json.options.method).toBe(options.method); + }); + + test("serializes URL only if options are not provided", () => { + const url = "/api/simple"; + const buffer = serializeRequest(url); + const view = new Uint8Array(buffer); + + const typeLength = view[0]; + const typeBytes = view.slice(1, 1 + typeLength); + const jsonBytes = view.slice(1 + typeLength); + + const contentType = new TextDecoder().decode(typeBytes); + const json = JSON.parse(new TextDecoder().decode(jsonBytes)); + + expect(contentType).toBe("application/json"); + expect(json.url).toBe(url); + expect(json).not.toHaveProperty("options"); + }); +}); + +describe("deserializeRequest", () => { + test("deserializes payload into correct request object", () => { + const url = "/api/test"; + const options = { method: "GET" }; + + const buffer = serializeRequest(url, options); + const result = deserializeRequest(buffer); + + expect(result.url).toBe(url); + expect(result.options.method).toBe("GET"); + }); + + test("throws error on unsupported content type", () => { + const encoder = new TextEncoder(); + const badType = encoder.encode("text/plain"); + const json = encoder.encode(JSON.stringify({ url: "/x" })); + + const buffer = new Uint8Array(1 + badType.length + json.length); + buffer[0] = badType.length; + buffer.set(badType, 1); + buffer.set(json, 1 + badType.length); + + expect(() => { + deserializeRequest(buffer.buffer); + }).toThrow("Unsupported content type: text/plain"); + }); +}); From 58666d5796c0d811091dcc00687677ba9ea5973a Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Tue, 10 Jun 2025 23:55:34 +0200 Subject: [PATCH 08/81] fix: fixed oneof variable naming --- Tokenization/backend/wrapper/proto/wrapper.proto | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tokenization/backend/wrapper/proto/wrapper.proto b/Tokenization/backend/wrapper/proto/wrapper.proto index bdb4d1560..fa2f9ba07 100644 --- a/Tokenization/backend/wrapper/proto/wrapper.proto +++ b/Tokenization/backend/wrapper/proto/wrapper.proto @@ -31,7 +31,7 @@ message Token { // Stream message that can contain one of specific messages message Payload { MessageEvent event = 1; - oneof { + oneof data { EmptyMessage emptyMessage = 2; Token newToken = 3; Token revokeToken = 4; From 01182719baa7dc2ba799db8b229ff6372903d7b1 Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Tue, 10 Jun 2025 11:18:57 +0200 Subject: [PATCH 09/81] feat: Duplex stream with specific payload message events. --- Tokenization/backend/wrapper/wrapper.proto | 27 ++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 Tokenization/backend/wrapper/wrapper.proto diff --git a/Tokenization/backend/wrapper/wrapper.proto b/Tokenization/backend/wrapper/wrapper.proto new file mode 100644 index 000000000..e65311c6e --- /dev/null +++ b/Tokenization/backend/wrapper/wrapper.proto @@ -0,0 +1,27 @@ +syntax = "proto3"; +package wrapper; + +// ====================================== +// MESSAGES +// ====================================== +// Simulates an empty message because protobuffer doesn't support void +message EmptyMessage {} + +// Message with newly generated token and target address binded to it +message NewToken { + string token = 1; + string targetAddress = 2; +} + + +// ====================================== +// SERVICES +// ====================================== + +service ClientService { + // Sends newly generated token to the client for specific Connection + rpc sendNewToken(NewToken) returns(EmptyMessage) {} + + // Informs the client about token revocation + rpc revokeToken(NewToken) returns(EmptyMessage) {} +} \ No newline at end of file From 7c5d552eb8660e04c3b225ce9d6a9e83ca07a3c1 Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Tue, 10 Jun 2025 11:25:16 +0200 Subject: [PATCH 10/81] feat: Create typescript interfaces for duplex stream message model --- .../backend/wrapper/models/message.model.ts | 15 +++++++++++ Tokenization/backend/wrapper/wrapper.proto | 27 ------------------- 2 files changed, 15 insertions(+), 27 deletions(-) create mode 100644 Tokenization/backend/wrapper/models/message.model.ts delete mode 100644 Tokenization/backend/wrapper/wrapper.proto diff --git a/Tokenization/backend/wrapper/models/message.model.ts b/Tokenization/backend/wrapper/models/message.model.ts new file mode 100644 index 000000000..36cdf5e12 --- /dev/null +++ b/Tokenization/backend/wrapper/models/message.model.ts @@ -0,0 +1,15 @@ +enum DuplexMessageEvent { + EMPTY_EVENT, + NEW_TOKEN, + REVOKE_TOKEN, +} + +interface TokenMessage { + token: string; + targetAddress: string; +} + +interface DuplexMessageModel { + event: DuplexMessageEvent; + data?: TokenMessage; +} diff --git a/Tokenization/backend/wrapper/wrapper.proto b/Tokenization/backend/wrapper/wrapper.proto deleted file mode 100644 index e65311c6e..000000000 --- a/Tokenization/backend/wrapper/wrapper.proto +++ /dev/null @@ -1,27 +0,0 @@ -syntax = "proto3"; -package wrapper; - -// ====================================== -// MESSAGES -// ====================================== -// Simulates an empty message because protobuffer doesn't support void -message EmptyMessage {} - -// Message with newly generated token and target address binded to it -message NewToken { - string token = 1; - string targetAddress = 2; -} - - -// ====================================== -// SERVICES -// ====================================== - -service ClientService { - // Sends newly generated token to the client for specific Connection - rpc sendNewToken(NewToken) returns(EmptyMessage) {} - - // Informs the client about token revocation - rpc revokeToken(NewToken) returns(EmptyMessage) {} -} \ No newline at end of file From 4b1845969dbdc5c747c6a67ecff1ca528df8b76a Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Tue, 10 Jun 2025 11:43:36 +0200 Subject: [PATCH 11/81] feat: implement serialization and deserialization methods --- .../__tests__/serialization.utils.test.ts | 0 .../wrapper/utils/serialization.utils.ts | 64 +++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 Tokenization/backend/wrapper/utils/__tests__/serialization.utils.test.ts create mode 100644 Tokenization/backend/wrapper/utils/serialization.utils.ts diff --git a/Tokenization/backend/wrapper/utils/__tests__/serialization.utils.test.ts b/Tokenization/backend/wrapper/utils/__tests__/serialization.utils.test.ts new file mode 100644 index 000000000..e69de29bb diff --git a/Tokenization/backend/wrapper/utils/serialization.utils.ts b/Tokenization/backend/wrapper/utils/serialization.utils.ts new file mode 100644 index 000000000..2f1e9bf41 --- /dev/null +++ b/Tokenization/backend/wrapper/utils/serialization.utils.ts @@ -0,0 +1,64 @@ +/** + * @description Serializes Json formatted request into binary payload with specific endpoint + * @param url - The endpoint URL to which the request is made + * @param options - Optional request options, such as headers or body + * @return {ArrayBuffer} - The serialized binary payload containing the URL and options + */ +export const serializeRequest = (url: string, options?: any) => { + const encoder = new TextEncoder(); + const contentTypeBytes = encoder.encode("application/json"); + + // build JSON data + const jsonData = options + ? { + url: url, + options: options, + } + : { + url: url, + }; + + // encode JSON + const jsonString = JSON.stringify(jsonData); + const jsonBytes = encoder.encode(jsonString); + + // Buffer following structure: + // 1 byte -> type length + // N bytes -> types + // rest -> JSON data + const buffer = new Uint8Array(1 + contentTypeBytes.length + jsonBytes.length); + + buffer[0] = contentTypeBytes.length; + buffer.set(contentTypeBytes, 1); + buffer.set(jsonBytes, 1 + contentTypeBytes.length); + + return buffer.buffer; +}; + +/** + * @description Deserializes binary payload to Json formated request + * @param payload - The binary payload to deserialize + * @return {any} - The deserialized request object containing the URL and options + */ +export const deserializeRequest = (payload: any) => { + const view = new Uint8Array(payload); + const decoder = new TextDecoder(); + + const contentTypeLength = view[0]; + const contentTypeBytes = view.slice(1, 1 + contentTypeLength); + const contentType = decoder.decode(contentTypeBytes); + + const dataBytes = view.slice(1 + contentTypeLength); + let data; + + // deserialization based on content type + switch (contentType) { + case "application/json": + data = JSON.parse(decoder.decode(dataBytes)); + break; + default: + throw new Error(`Unsupported content type: ${contentType}`); + } + + return data; +}; From dba904c82bdb7985d3645f96d34c8f815bf59868 Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Tue, 10 Jun 2025 22:29:50 +0200 Subject: [PATCH 12/81] feat: add package dependencies and write unit tests for serialization utils --- Tokenization/backend/wrapper/jest.config.js | 13 + .../backend/wrapper/package-lock.json | 4007 +++++++++++++++++ Tokenization/backend/wrapper/package.json | 20 + .../__tests__/serialization.utils.test.ts | 71 + 4 files changed, 4111 insertions(+) create mode 100644 Tokenization/backend/wrapper/jest.config.js create mode 100644 Tokenization/backend/wrapper/package-lock.json create mode 100644 Tokenization/backend/wrapper/package.json diff --git a/Tokenization/backend/wrapper/jest.config.js b/Tokenization/backend/wrapper/jest.config.js new file mode 100644 index 000000000..3a5675ad5 --- /dev/null +++ b/Tokenization/backend/wrapper/jest.config.js @@ -0,0 +1,13 @@ +/** @type {import('jest').Config} */ +const config = { + verbose: true, + transform: { + "^.+\\.ts$": ["ts-jest", { useESM: true }], + }, + extensionsToTreatAsEsm: [".ts"], + moduleNameMapper: { + "^(\\.{1,2}/.*)\\.js$": "$1", + }, +}; + +export default config; diff --git a/Tokenization/backend/wrapper/package-lock.json b/Tokenization/backend/wrapper/package-lock.json new file mode 100644 index 000000000..500302d37 --- /dev/null +++ b/Tokenization/backend/wrapper/package-lock.json @@ -0,0 +1,4007 @@ +{ + "name": "grpc-wrapper", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "grpc-wrapper", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "ts-node": "^10.9.2", + "typescript": "^5.8.3" + }, + "devDependencies": { + "@types/jest": "^29.5.14", + "jest": "^29.7.0", + "ts-jest": "^29.3.4" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.27.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.5.tgz", + "integrity": "sha512-KiRAp/VoJaWkkte84TvUd9qjdbZAdiqyvMxrGl1N6vzFogKmaLgoM3L1kgtLicp2HP5fBJS8JrZKLVIZGVJAVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.27.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.4.tgz", + "integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.27.3", + "@babel/helpers": "^7.27.4", + "@babel/parser": "^7.27.4", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.27.4", + "@babel/types": "^7.27.3", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.27.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.5.tgz", + "integrity": "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.27.5", + "@babel/types": "^7.27.3", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", + "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz", + "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.27.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.5.tgz", + "integrity": "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.27.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.4.tgz", + "integrity": "sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.3", + "@babel/parser": "^7.27.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.3", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.6.tgz", + "integrity": "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", + "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/node": { + "version": "24.0.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.0.tgz", + "integrity": "sha512-yZQa2zm87aRVcqDyH5+4Hv9KYgSdgwX1rFnGvpbzMaC7YAljmhBET93TPiTd3ObwTL+gSpIzPKg5BqVxdCvxKg==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.8.0" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "license": "MIT" + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", + "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.0.tgz", + "integrity": "sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001718", + "electron-to-chromium": "^1.5.160", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001721", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001721.tgz", + "integrity": "sha512-cOuvmUVtKrtEaoKiO0rSc29jcjwMwX5tOHDy4MgVFEWiUXj4uBMJkwI8MDySkgXidpMiHUcviogAvFi4pA2hDQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", + "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.166", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.166.tgz", + "integrity": "sha512-QPWqHL0BglzPYyJJ1zSSmwFFL6MFXhbACOCcsCdUMCkzPdS9/OIBVxg516X/Ado2qwAq8k0nJJ7phQPCqiaFAw==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "license": "ISC" + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-jest": { + "version": "29.3.4", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.3.4.tgz", + "integrity": "sha512-Iqbrm8IXOmV+ggWHOTEbjwyCf2xZlUMv5npExksXohL+tk8va4Fjhb+X2+Rt9NBmgO7bJ8WpnMLOwih/DnMlFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "ejs": "^3.1.10", + "fast-json-stable-stringify": "^2.1.0", + "jest-util": "^29.0.0", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.7.2", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0", + "@jest/types": "^29.0.0", + "babel-jest": "^29.0.0", + "jest": "^29.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "license": "MIT" + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/Tokenization/backend/wrapper/package.json b/Tokenization/backend/wrapper/package.json new file mode 100644 index 000000000..a8b9f98a6 --- /dev/null +++ b/Tokenization/backend/wrapper/package.json @@ -0,0 +1,20 @@ +{ + "name": "grpc-wrapper", + "version": "1.0.0", + "type": "module", + "scripts": { + "test": "mocha --loader ts-node/esm \"./**/*.test.ts\"" + }, + "author": "", + "license": "ISC", + "description": "", + "devDependencies": { + "@types/jest": "^29.5.14", + "jest": "^29.7.0", + "ts-jest": "^29.3.4" + }, + "dependencies": { + "ts-node": "^10.9.2", + "typescript": "^5.8.3" + } +} diff --git a/Tokenization/backend/wrapper/utils/__tests__/serialization.utils.test.ts b/Tokenization/backend/wrapper/utils/__tests__/serialization.utils.test.ts index e69de29bb..655b0ccef 100644 --- a/Tokenization/backend/wrapper/utils/__tests__/serialization.utils.test.ts +++ b/Tokenization/backend/wrapper/utils/__tests__/serialization.utils.test.ts @@ -0,0 +1,71 @@ +import { deserializeRequest, serializeRequest } from "../serialization.utils"; +import { describe, expect, test } from "@jest/globals"; + +describe("serializeRequest", () => { + test("serializes URL and options correctly", () => { + const url = "/api/test"; + const options = { + method: "POST", + headers: { "Content-Type": "application/json" }, + }; + + const buffer = serializeRequest(url, options); + const view = new Uint8Array(buffer); + + const typeLength = view[0]; + const typeBytes = view.slice(1, 1 + typeLength); + const jsonBytes = view.slice(1 + typeLength); + + const contentType = new TextDecoder().decode(typeBytes); + const json = JSON.parse(new TextDecoder().decode(jsonBytes)); + + expect(contentType).toBe("application/json"); + expect(json.url).toBe(url); + expect(json.options.method).toBe(options.method); + }); + + test("serializes URL only if options are not provided", () => { + const url = "/api/simple"; + const buffer = serializeRequest(url); + const view = new Uint8Array(buffer); + + const typeLength = view[0]; + const typeBytes = view.slice(1, 1 + typeLength); + const jsonBytes = view.slice(1 + typeLength); + + const contentType = new TextDecoder().decode(typeBytes); + const json = JSON.parse(new TextDecoder().decode(jsonBytes)); + + expect(contentType).toBe("application/json"); + expect(json.url).toBe(url); + expect(json).not.toHaveProperty("options"); + }); +}); + +describe("deserializeRequest", () => { + test("deserializes payload into correct request object", () => { + const url = "/api/test"; + const options = { method: "GET" }; + + const buffer = serializeRequest(url, options); + const result = deserializeRequest(buffer); + + expect(result.url).toBe(url); + expect(result.options.method).toBe("GET"); + }); + + test("throws error on unsupported content type", () => { + const encoder = new TextEncoder(); + const badType = encoder.encode("text/plain"); + const json = encoder.encode(JSON.stringify({ url: "/x" })); + + const buffer = new Uint8Array(1 + badType.length + json.length); + buffer[0] = badType.length; + buffer.set(badType, 1); + buffer.set(json, 1 + badType.length); + + expect(() => { + deserializeRequest(buffer.buffer); + }).toThrow("Unsupported content type: text/plain"); + }); +}); From 273b8e2c046da6bcc40ce3af23f14b418b529958 Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Tue, 10 Jun 2025 23:58:09 +0200 Subject: [PATCH 13/81] feat: Create grpc wrapper with basic connection manager and central system wrapper --- .../backend/wrapper/central/CentralSystem.ts | 69 ++++++++ .../ConnectionManager/ConnectionManager.ts | 94 +++++++++++ .../backend/wrapper/client/gRPCWrapper.ts | 11 ++ .../backend/wrapper/package-lock.json | 159 ++++++++++++++++-- Tokenization/backend/wrapper/package.json | 5 +- Tokenization/backend/wrapper/tsconfig.json | 15 ++ 6 files changed, 336 insertions(+), 17 deletions(-) create mode 100644 Tokenization/backend/wrapper/central/CentralSystem.ts create mode 100644 Tokenization/backend/wrapper/client/ConnectionManager/ConnectionManager.ts create mode 100644 Tokenization/backend/wrapper/client/gRPCWrapper.ts create mode 100644 Tokenization/backend/wrapper/tsconfig.json diff --git a/Tokenization/backend/wrapper/central/CentralSystem.ts b/Tokenization/backend/wrapper/central/CentralSystem.ts new file mode 100644 index 000000000..f22a8adf9 --- /dev/null +++ b/Tokenization/backend/wrapper/central/CentralSystem.ts @@ -0,0 +1,69 @@ +import * as grpc from "@grpc/grpc-js"; +import * as protoLoader from "@grpc/proto-loader"; +import path from "path"; +import { fileURLToPath } from "url"; + +export class CentralSystem { + private server: grpc.Server; + + constructor(private port: number) { + this.server = new grpc.Server(); + this.setupService(); + this.start(); + } + + private setupService() { + const __filename = fileURLToPath(import.meta.url); + const __dirname = path.dirname(__filename); + const PROTO_PATH = path.join(__dirname, "../proto/wrapper.proto"); + const packageDef = protoLoader.loadSync(PROTO_PATH, { + keepCase: true, + longs: String, + enums: String, + defaults: true, + oneofs: true, + }); + + const proto = grpc.loadPackageDefinition(packageDef) as any; + const wrapper = proto.wrapper; + + this.server.addService(wrapper.CentralSystem.service, { + ClientStream: this.clientStreamHandler.bind(this), + }); + } + + private clientStreamHandler(call: grpc.ServerDuplexStream) { + console.log("Client connected to duplex stream"); + + // hartbeat message + call.write({ event: "EMPTY_EVENT", emptyMessage: {} }); + + call.on("data", (payload: any) => { + // TODO: Implement data handling logic + }); + + call.on("end", () => { + console.log("Client ended stream"); + call.end(); + }); + + call.on("error", (err) => console.error("Stream error:", err)); + } + + private start() { + const addr = `0.0.0.0:${this.port}`; + this.server.bindAsync( + addr, + grpc.ServerCredentials.createInsecure(), + (err, port) => { + if (err) { + console.error("Server bind error:", err); + return; + } + console.log(`Server listening on ${addr}`); + } + ); + } +} + +const cs = new CentralSystem(50051); diff --git a/Tokenization/backend/wrapper/client/ConnectionManager/ConnectionManager.ts b/Tokenization/backend/wrapper/client/ConnectionManager/ConnectionManager.ts new file mode 100644 index 000000000..93d3cf6dc --- /dev/null +++ b/Tokenization/backend/wrapper/client/ConnectionManager/ConnectionManager.ts @@ -0,0 +1,94 @@ +import * as grpc from "@grpc/grpc-js"; +import * as protoLoader from "@grpc/proto-loader"; +import path from "path"; +import { fileURLToPath } from "url"; + +/** + * @description Manages all the connection between clients and central system. + */ +export class ConnectionManager { + private client: any; + private stream?: grpc.ClientDuplexStream; + private readonly address: string; + private reconnectAttempts = 0; + + constructor(centralAddress = "localhost:50051") { + this.address = centralAddress; + + const __filename = fileURLToPath(import.meta.url); + const __dirname = path.dirname(__filename); + const PROTO_PATH = path.join(__dirname, "./wrapper.proto"); + const packageDef = protoLoader.loadSync(PROTO_PATH, { + keepCase: true, + longs: String, + enums: String, + defaults: true, + oneofs: true, + }); + + const proto = grpc.loadPackageDefinition(packageDef) as any; + const wrapper = proto.wrapper; + + // Create gRPC client + this.client = new wrapper.CentralSystem( + this.address, + grpc.credentials.createInsecure() + ); + + // Initial connection + this.connect(); + console.log(`ConnectionManager: connected to ${this.address}`); + } + + /** + * @description Initializes the duplex stream and sets up handlers. + */ + private connect() { + this.stream = this.client.ClientStream(); + + if (this.stream) { + this.stream.on("data", (payload) => { + // handle data received from the stream + }); + + this.stream.on("end", () => { + console.warn("Stream ended, attempting to reconnect..."); + this.scheduleReconnect(); + }); + + this.stream.on("error", (err: any) => { + console.error("Wrapper stream error:", err); + this.scheduleReconnect(); + }); + } + } + + /** + * @description Schedules a reconnect with exponential backoff. + */ + private scheduleReconnect() { + this.reconnectAttempts++; + const delay = Math.min(1000 * 2 ** this.reconnectAttempts, 30000); + setTimeout(() => { + console.log(`Reconnecting (attempt ${this.reconnectAttempts})...`); + this.connect(); + }, delay); + } + + /** + * @description Disconnects from the gRPC stream and resets attempts. + */ + disconnect() { + if (this.stream) { + this.stream.end(); + this.stream = undefined; + } + this.reconnectAttempts = 0; + console.log("Disconnected from central"); + } +} + +// Usage example: +// const mgr = new ConnectionManager(); +// mgr.on('message', payload => console.log('Received:', payload)); +// mgr.disconnect(); diff --git a/Tokenization/backend/wrapper/client/gRPCWrapper.ts b/Tokenization/backend/wrapper/client/gRPCWrapper.ts new file mode 100644 index 000000000..a4a02b19a --- /dev/null +++ b/Tokenization/backend/wrapper/client/gRPCWrapper.ts @@ -0,0 +1,11 @@ +import { ConnectionManager } from "./ConnectionManager/ConnectionManager.ts"; + +export class gRPCWrapper { + private ConnectionManager: ConnectionManager; + + constructor() { + this.ConnectionManager = new ConnectionManager(); + } +} + +const grpc = new gRPCWrapper(); diff --git a/Tokenization/backend/wrapper/package-lock.json b/Tokenization/backend/wrapper/package-lock.json index 500302d37..ba080a16e 100644 --- a/Tokenization/backend/wrapper/package-lock.json +++ b/Tokenization/backend/wrapper/package-lock.json @@ -9,6 +9,8 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@grpc/grpc-js": "^1.13.4", + "@grpc/proto-loader": "^0.7.15", "ts-node": "^10.9.2", "typescript": "^5.8.3" }, @@ -540,6 +542,37 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@grpc/grpc-js": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.13.4.tgz", + "integrity": "sha512-GsFaMXCkMqkKIvwCQjCrwH+GHbPKBjhwo/8ZuUkWHqbI73Kky9I+pQltrlT0+MWpedCoosda53lgjYfyEPgxBg==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.7.13", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", + "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -910,6 +943,80 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -1131,7 +1238,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -1141,7 +1247,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -1495,7 +1600,6 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, "license": "ISC", "dependencies": { "string-width": "^4.2.0", @@ -1528,7 +1632,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -1541,7 +1644,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, "node_modules/concat-map": { @@ -1713,7 +1815,6 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, "license": "MIT" }, "node_modules/error-ex": { @@ -1730,7 +1831,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -1923,7 +2023,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" @@ -2107,7 +2206,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -2946,6 +3044,12 @@ "node": ">=8" } }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -2953,6 +3057,12 @@ "dev": true, "license": "MIT" }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -3325,6 +3435,30 @@ "node": ">= 6" } }, + "node_modules/protobufjs": { + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.3.tgz", + "integrity": "sha512-sildjKwVqOI2kmFDiXQ6aEB0fjYTafpEvIBs8tOR8qI4spuL9OPROLVu2qZqi/xgCfsHIwVqlaF8JBjWFHnKbw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/pure-rand": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", @@ -3353,7 +3487,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -3539,7 +3672,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -3554,7 +3686,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -3900,7 +4031,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -3939,7 +4069,6 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, "license": "ISC", "engines": { "node": ">=10" @@ -3956,7 +4085,6 @@ "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, "license": "MIT", "dependencies": { "cliui": "^8.0.1", @@ -3975,7 +4103,6 @@ "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, "license": "ISC", "engines": { "node": ">=12" diff --git a/Tokenization/backend/wrapper/package.json b/Tokenization/backend/wrapper/package.json index a8b9f98a6..b65f10173 100644 --- a/Tokenization/backend/wrapper/package.json +++ b/Tokenization/backend/wrapper/package.json @@ -3,7 +3,8 @@ "version": "1.0.0", "type": "module", "scripts": { - "test": "mocha --loader ts-node/esm \"./**/*.test.ts\"" + "test": "mocha --loader ts-node/esm \"./**/*.test.ts\"", + "start": "node --loader ts-node/esm ./central/CentralSystem.ts" }, "author": "", "license": "ISC", @@ -14,6 +15,8 @@ "ts-jest": "^29.3.4" }, "dependencies": { + "@grpc/grpc-js": "^1.13.4", + "@grpc/proto-loader": "^0.7.15", "ts-node": "^10.9.2", "typescript": "^5.8.3" } diff --git a/Tokenization/backend/wrapper/tsconfig.json b/Tokenization/backend/wrapper/tsconfig.json new file mode 100644 index 000000000..32d70e3e9 --- /dev/null +++ b/Tokenization/backend/wrapper/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "module": "ESNext", + "target": "ES2022", + "moduleResolution": "node", + "esModuleInterop": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "allowImportingTsExtensions": true, + "noEmit": true + }, + "ts-node": { + "esm": true + } +} From 6467c55a118355e810706534f2a5b36590e1ca58 Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Wed, 11 Jun 2025 00:04:33 +0200 Subject: [PATCH 14/81] fix: fix grpc addresses to properly connect both client and central system --- Tokenization/backend/wrapper/central/CentralSystem.ts | 4 ++-- .../wrapper/client/ConnectionManager/ConnectionManager.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Tokenization/backend/wrapper/central/CentralSystem.ts b/Tokenization/backend/wrapper/central/CentralSystem.ts index f22a8adf9..5ec776524 100644 --- a/Tokenization/backend/wrapper/central/CentralSystem.ts +++ b/Tokenization/backend/wrapper/central/CentralSystem.ts @@ -51,7 +51,7 @@ export class CentralSystem { } private start() { - const addr = `0.0.0.0:${this.port}`; + const addr = `localhost:${this.port}`; this.server.bindAsync( addr, grpc.ServerCredentials.createInsecure(), @@ -66,4 +66,4 @@ export class CentralSystem { } } -const cs = new CentralSystem(50051); +const centralSystem = new CentralSystem(50051); diff --git a/Tokenization/backend/wrapper/client/ConnectionManager/ConnectionManager.ts b/Tokenization/backend/wrapper/client/ConnectionManager/ConnectionManager.ts index 93d3cf6dc..f2936fb0c 100644 --- a/Tokenization/backend/wrapper/client/ConnectionManager/ConnectionManager.ts +++ b/Tokenization/backend/wrapper/client/ConnectionManager/ConnectionManager.ts @@ -17,7 +17,7 @@ export class ConnectionManager { const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); - const PROTO_PATH = path.join(__dirname, "./wrapper.proto"); + const PROTO_PATH = path.join(__dirname, "../../proto/wrapper.proto"); const packageDef = protoLoader.loadSync(PROTO_PATH, { keepCase: true, longs: String, From 1a86d4949815ee8e12bd4b634b85d3d04e513d33 Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Wed, 11 Jun 2025 12:57:12 +0200 Subject: [PATCH 15/81] fix: refactor comments --- Tokenization/backend/wrapper/central/CentralSystem.ts | 3 +++ .../wrapper/client/ConnectionManager/ConnectionManager.ts | 5 ----- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/Tokenization/backend/wrapper/central/CentralSystem.ts b/Tokenization/backend/wrapper/central/CentralSystem.ts index 5ec776524..e30673b89 100644 --- a/Tokenization/backend/wrapper/central/CentralSystem.ts +++ b/Tokenization/backend/wrapper/central/CentralSystem.ts @@ -3,6 +3,9 @@ import * as protoLoader from "@grpc/proto-loader"; import path from "path"; import { fileURLToPath } from "url"; +/** + * @description Central System gRPC wrapper that manages client connections and handles gRPC streams with them. + */ export class CentralSystem { private server: grpc.Server; diff --git a/Tokenization/backend/wrapper/client/ConnectionManager/ConnectionManager.ts b/Tokenization/backend/wrapper/client/ConnectionManager/ConnectionManager.ts index f2936fb0c..ae8f2cb1b 100644 --- a/Tokenization/backend/wrapper/client/ConnectionManager/ConnectionManager.ts +++ b/Tokenization/backend/wrapper/client/ConnectionManager/ConnectionManager.ts @@ -87,8 +87,3 @@ export class ConnectionManager { console.log("Disconnected from central"); } } - -// Usage example: -// const mgr = new ConnectionManager(); -// mgr.on('message', payload => console.log('Received:', payload)); -// mgr.disconnect(); From bec195ccc51d694da824c5b0f55698f4f09e73e8 Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Wed, 11 Jun 2025 12:58:55 +0200 Subject: [PATCH 16/81] fix: change central system class name to avoid conflicts with central system --- Tokenization/backend/wrapper/central/CentralSystem.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tokenization/backend/wrapper/central/CentralSystem.ts b/Tokenization/backend/wrapper/central/CentralSystem.ts index e30673b89..3058ce7bd 100644 --- a/Tokenization/backend/wrapper/central/CentralSystem.ts +++ b/Tokenization/backend/wrapper/central/CentralSystem.ts @@ -6,7 +6,7 @@ import { fileURLToPath } from "url"; /** * @description Central System gRPC wrapper that manages client connections and handles gRPC streams with them. */ -export class CentralSystem { +export class CentralSystemWrapper { private server: grpc.Server; constructor(private port: number) { @@ -69,4 +69,4 @@ export class CentralSystem { } } -const centralSystem = new CentralSystem(50051); +const centralSystem = new CentralSystemWrapper(50051); From c6a3f014319f7270a37c712e70942587f919e7ea Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Wed, 11 Jun 2025 13:38:19 +0200 Subject: [PATCH 17/81] feat: implement Connection class and a map storing all receiving and sending connections in ConnectionManager --- .../wrapper/client/Connection/Connection.ts | 16 ++++++++++++++++ .../ConnectionManager/ConnectionManager.ts | 17 ++++++++++++++++- Tokenization/backend/wrapper/package.json | 5 +++-- 3 files changed, 35 insertions(+), 3 deletions(-) create mode 100644 Tokenization/backend/wrapper/client/Connection/Connection.ts diff --git a/Tokenization/backend/wrapper/client/Connection/Connection.ts b/Tokenization/backend/wrapper/client/Connection/Connection.ts new file mode 100644 index 000000000..00ec4f381 --- /dev/null +++ b/Tokenization/backend/wrapper/client/Connection/Connection.ts @@ -0,0 +1,16 @@ +/** + * @description This class represents a connection to a target client and manages sending messages to it. + */ +export class Connection { + private token: string; + private targetAddress: string; + + constructor(token: string, targetAddress: string) { + this.token = token; + this.targetAddress = targetAddress; + } + + public handleNewToken(token: string): void { + this.token = token; + } +} diff --git a/Tokenization/backend/wrapper/client/ConnectionManager/ConnectionManager.ts b/Tokenization/backend/wrapper/client/ConnectionManager/ConnectionManager.ts index ae8f2cb1b..60c994f37 100644 --- a/Tokenization/backend/wrapper/client/ConnectionManager/ConnectionManager.ts +++ b/Tokenization/backend/wrapper/client/ConnectionManager/ConnectionManager.ts @@ -2,6 +2,7 @@ import * as grpc from "@grpc/grpc-js"; import * as protoLoader from "@grpc/proto-loader"; import path from "path"; import { fileURLToPath } from "url"; +import { Connection } from "../Connection/Connection.ts"; /** * @description Manages all the connection between clients and central system. @@ -12,6 +13,12 @@ export class ConnectionManager { private readonly address: string; private reconnectAttempts = 0; + // Map to store sending connections by target address + private sendingConnections: Map = new Map(); + + // Map to store receiving connections by target address + private receivingConnections: Map = new Map(); + constructor(centralAddress = "localhost:50051") { this.address = centralAddress; @@ -38,6 +45,9 @@ export class ConnectionManager { // Initial connection this.connect(); console.log(`ConnectionManager: connected to ${this.address}`); + + this.sendingConnections.set("a", new Connection("1", "a")); + this.sendingConnections.set("b", new Connection("2", "b")); } /** @@ -48,7 +58,12 @@ export class ConnectionManager { if (this.stream) { this.stream.on("data", (payload) => { - // handle data received from the stream + switch (payload.event) { + // Central system replacing a new token for existing connection + case "EMPTY_EVENT": + console.log("Empty event: ", payload?.data); + break; + } }); this.stream.on("end", () => { diff --git a/Tokenization/backend/wrapper/package.json b/Tokenization/backend/wrapper/package.json index b65f10173..f77932ebf 100644 --- a/Tokenization/backend/wrapper/package.json +++ b/Tokenization/backend/wrapper/package.json @@ -3,8 +3,9 @@ "version": "1.0.0", "type": "module", "scripts": { - "test": "mocha --loader ts-node/esm \"./**/*.test.ts\"", - "start": "node --loader ts-node/esm ./central/CentralSystem.ts" + "test": "jest", + "start-central": "node --loader ts-node/esm ./central/CentralSystem.ts", + "start-client": "node --loader ts-node/esm ./client/gRPCWrapper.ts" }, "author": "", "license": "ISC", From fb12e553ed46c463653cfb475f64512c5c4d8ef0 Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Wed, 11 Jun 2025 14:26:35 +0200 Subject: [PATCH 18/81] feat: implement replacing new token for client from central system --- .../backend/wrapper/central/CentralSystem.ts | 53 +++++++++++++++++-- .../wrapper/client/Connection/Connection.ts | 4 ++ .../ConnectionManager/ConnectionManager.ts | 30 ++++++++++- .../backend/wrapper/client/gRPCWrapper.ts | 1 + .../backend/wrapper/models/message.model.ts | 15 +++--- 5 files changed, 91 insertions(+), 12 deletions(-) diff --git a/Tokenization/backend/wrapper/central/CentralSystem.ts b/Tokenization/backend/wrapper/central/CentralSystem.ts index 3058ce7bd..626dfbbbe 100644 --- a/Tokenization/backend/wrapper/central/CentralSystem.ts +++ b/Tokenization/backend/wrapper/central/CentralSystem.ts @@ -2,12 +2,17 @@ import * as grpc from "@grpc/grpc-js"; import * as protoLoader from "@grpc/proto-loader"; import path from "path"; import { fileURLToPath } from "url"; +import { + DuplexMessageEvent, + DuplexMessageModel, +} from "../models/message.model.ts"; /** * @description Central System gRPC wrapper that manages client connections and handles gRPC streams with them. */ export class CentralSystemWrapper { private server: grpc.Server; + private clientStreams = new Map>(); constructor(private port: number) { this.server = new grpc.Server(); @@ -38,19 +43,28 @@ export class CentralSystemWrapper { private clientStreamHandler(call: grpc.ServerDuplexStream) { console.log("Client connected to duplex stream"); + const clientAddress = call.getPeer(); + + this.clientStreams.set(clientAddress, call); + console.log(`Registered client stream for: ${clientAddress}`); + // hartbeat message - call.write({ event: "EMPTY_EVENT", emptyMessage: {} }); + call.write({ event: "EMPTY_EVENT", data: "registered in central system." }); call.on("data", (payload: any) => { - // TODO: Implement data handling logic + console.log(`Received from ${clientAddress}:`, payload); }); call.on("end", () => { - console.log("Client ended stream"); + console.log(`Client ${clientAddress} ended stream`); + this.clientStreams.delete(clientAddress); call.end(); }); - call.on("error", (err) => console.error("Stream error:", err)); + call.on("error", (err) => { + console.error(`Stream error for ${clientAddress}:`, err); + this.clientStreams.delete(clientAddress); + }); } private start() { @@ -67,6 +81,37 @@ export class CentralSystemWrapper { } ); } + + /** + * @description Returns all client addresses + */ + public getClients() { + return this.clientStreams.keys(); + } + + /** + * @description Sends message event to specific client + */ + public clientSend(clientAddress: string, message: DuplexMessageModel) { + const stream = this.clientStreams.get(clientAddress); + if (!stream) { + console.warn(`No active stream for client ${clientAddress}`); + return; + } + stream.write(message); + } } +// tests const centralSystem = new CentralSystemWrapper(50051); +setTimeout(() => { + const client = Array.from(centralSystem.getClients())[0]; + console.log(client); + centralSystem.clientSend(client, { + event: DuplexMessageEvent.NEW_TOKEN, + newToken: { + token: "new token", + targetAddress: "a", + }, + }); +}, 5000); diff --git a/Tokenization/backend/wrapper/client/Connection/Connection.ts b/Tokenization/backend/wrapper/client/Connection/Connection.ts index 00ec4f381..6579a8de6 100644 --- a/Tokenization/backend/wrapper/client/Connection/Connection.ts +++ b/Tokenization/backend/wrapper/client/Connection/Connection.ts @@ -13,4 +13,8 @@ export class Connection { public handleNewToken(token: string): void { this.token = token; } + + public getToken(): string { + return this.token; + } } diff --git a/Tokenization/backend/wrapper/client/ConnectionManager/ConnectionManager.ts b/Tokenization/backend/wrapper/client/ConnectionManager/ConnectionManager.ts index 60c994f37..f1b33b88b 100644 --- a/Tokenization/backend/wrapper/client/ConnectionManager/ConnectionManager.ts +++ b/Tokenization/backend/wrapper/client/ConnectionManager/ConnectionManager.ts @@ -3,6 +3,7 @@ import * as protoLoader from "@grpc/proto-loader"; import path from "path"; import { fileURLToPath } from "url"; import { Connection } from "../Connection/Connection.ts"; +import { DuplexMessageEvent } from "../../models/message.model.ts"; /** * @description Manages all the connection between clients and central system. @@ -60,9 +61,18 @@ export class ConnectionManager { this.stream.on("data", (payload) => { switch (payload.event) { // Central system replacing a new token for existing connection - case "EMPTY_EVENT": + case DuplexMessageEvent.EMPTY_EVENT: console.log("Empty event: ", payload?.data); break; + case DuplexMessageEvent.NEW_TOKEN: + console.log(payload); + this.handleNewToken( + payload.newToken.token, + payload.newToken.targetAddress + ); + break; + default: + console.warn(`Unhandled event: ${payload.event}`); } }); @@ -78,6 +88,24 @@ export class ConnectionManager { } } + /** + * @description Handles a new token received from the central system and replaces it in proper Connection object. + * @param newToken + */ + private handleNewToken(newToken: string, targetAddress: string) { + console.log(`Received new token for ${targetAddress}: ${newToken}`); + + // Check if we have a sending connection for this target address + const sendingConnection = this.sendingConnections.get(targetAddress); + if (sendingConnection) { + sendingConnection.handleNewToken(newToken); + console.log(`Updated sending connection for ${targetAddress}`); + console.log(sendingConnection.getToken()); + } else { + console.warn(`No sending connection found for ${targetAddress}`); + } + } + /** * @description Schedules a reconnect with exponential backoff. */ diff --git a/Tokenization/backend/wrapper/client/gRPCWrapper.ts b/Tokenization/backend/wrapper/client/gRPCWrapper.ts index a4a02b19a..6400bf04a 100644 --- a/Tokenization/backend/wrapper/client/gRPCWrapper.ts +++ b/Tokenization/backend/wrapper/client/gRPCWrapper.ts @@ -8,4 +8,5 @@ export class gRPCWrapper { } } +// tests const grpc = new gRPCWrapper(); diff --git a/Tokenization/backend/wrapper/models/message.model.ts b/Tokenization/backend/wrapper/models/message.model.ts index 36cdf5e12..cdcc15eb1 100644 --- a/Tokenization/backend/wrapper/models/message.model.ts +++ b/Tokenization/backend/wrapper/models/message.model.ts @@ -1,15 +1,16 @@ -enum DuplexMessageEvent { - EMPTY_EVENT, - NEW_TOKEN, - REVOKE_TOKEN, +export enum DuplexMessageEvent { + EMPTY_EVENT = "EMPTY_EVENT", + NEW_TOKEN = "NEW_TOKEN", + REVOKE_TOKEN = "REVOKE_TOKEN", } -interface TokenMessage { +export interface TokenMessage { token: string; targetAddress: string; } -interface DuplexMessageModel { +export interface DuplexMessageModel { event: DuplexMessageEvent; - data?: TokenMessage; + newToken?: TokenMessage; + revokeToken?: TokenMessage; } From 63faee36a647d86d700f71a166c2b0d5ba251306 Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Wed, 11 Jun 2025 14:55:20 +0200 Subject: [PATCH 19/81] feat: implement token revokation by changing connection status --- .../backend/wrapper/central/CentralSystem.ts | 11 ++++++++ .../wrapper/client/Connection/Connection.ts | 26 +++++++++++++++++++ .../ConnectionManager/ConnectionManager.ts | 21 ++++++++++++++- .../wrapper/models/connection.model.ts | 17 ++++++++++++ 4 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 Tokenization/backend/wrapper/models/connection.model.ts diff --git a/Tokenization/backend/wrapper/central/CentralSystem.ts b/Tokenization/backend/wrapper/central/CentralSystem.ts index 626dfbbbe..283532ba5 100644 --- a/Tokenization/backend/wrapper/central/CentralSystem.ts +++ b/Tokenization/backend/wrapper/central/CentralSystem.ts @@ -107,6 +107,8 @@ const centralSystem = new CentralSystemWrapper(50051); setTimeout(() => { const client = Array.from(centralSystem.getClients())[0]; console.log(client); + + // send new token centralSystem.clientSend(client, { event: DuplexMessageEvent.NEW_TOKEN, newToken: { @@ -114,4 +116,13 @@ setTimeout(() => { targetAddress: "a", }, }); + + // revoke token + centralSystem.clientSend(client, { + event: DuplexMessageEvent.REVOKE_TOKEN, + revokeToken: { + token: "new token", + targetAddress: "a", + }, + }); }, 5000); diff --git a/Tokenization/backend/wrapper/client/Connection/Connection.ts b/Tokenization/backend/wrapper/client/Connection/Connection.ts index 6579a8de6..8fb2d6ca4 100644 --- a/Tokenization/backend/wrapper/client/Connection/Connection.ts +++ b/Tokenization/backend/wrapper/client/Connection/Connection.ts @@ -1,20 +1,46 @@ +import { ConnectionStatus } from "../../models/connection.model.ts"; + /** * @description This class represents a connection to a target client and manages sending messages to it. */ export class Connection { private token: string; private targetAddress: string; + private status: ConnectionStatus; constructor(token: string, targetAddress: string) { this.token = token; this.targetAddress = targetAddress; + + this.status = ConnectionStatus.CONNECTED; } + /** + * @description Replace newly generated token + * @param token New token to be replaced + */ public handleNewToken(token: string): void { this.token = token; } + public handleRevokeToken(): void { + this.token = ""; + this.status = ConnectionStatus.UNAUTHORIZED; + } + + /** + * @description Returns token for this Connection object + * @returns Connection token + */ public getToken(): string { return this.token; } + + /** + * @description Returns status for specific + * @returns Connection status + */ + public getStatus(): string { + return this.status; + } } diff --git a/Tokenization/backend/wrapper/client/ConnectionManager/ConnectionManager.ts b/Tokenization/backend/wrapper/client/ConnectionManager/ConnectionManager.ts index f1b33b88b..73184263b 100644 --- a/Tokenization/backend/wrapper/client/ConnectionManager/ConnectionManager.ts +++ b/Tokenization/backend/wrapper/client/ConnectionManager/ConnectionManager.ts @@ -65,12 +65,17 @@ export class ConnectionManager { console.log("Empty event: ", payload?.data); break; case DuplexMessageEvent.NEW_TOKEN: - console.log(payload); this.handleNewToken( payload.newToken.token, payload.newToken.targetAddress ); break; + case DuplexMessageEvent.REVOKE_TOKEN: + this.handleRevokeToken( + payload.revokeToken.token, + payload.revokeToken.targetAddress + ); + break; default: console.warn(`Unhandled event: ${payload.event}`); } @@ -106,6 +111,20 @@ export class ConnectionManager { } } + private handleRevokeToken(newToken: string, targetAddress: string) { + console.log(`Revoke token for ${targetAddress}`); + + const sendingConnection = this.sendingConnections.get(targetAddress); + if (sendingConnection) { + sendingConnection.handleRevokeToken(); + } + + const receivingConnection = this.receivingConnections.get(targetAddress); + if (receivingConnection) { + receivingConnection.handleRevokeToken(); + } + } + /** * @description Schedules a reconnect with exponential backoff. */ diff --git a/Tokenization/backend/wrapper/models/connection.model.ts b/Tokenization/backend/wrapper/models/connection.model.ts new file mode 100644 index 000000000..474a2964c --- /dev/null +++ b/Tokenization/backend/wrapper/models/connection.model.ts @@ -0,0 +1,17 @@ +export enum ConnectionStatus { + // The connection is in the process of being established + CONNECTING = "CONNECTING", + // The connection has been successfully established + CONNECTED = "CONNECTED", + // The connection attempt failed due to authorization issues + // or token has expired/been revoked + UNAUTHORIZED = "UNAUTHORIZED", + // The connection has been closed + CLOSED = "CLOSED", + // An error occurred with the connection + ERROR = "ERROR", + // The connection is attempting to re-establish after a disruption + RECONNECTING = "RECONNECTING", + // The connection is refreshing its authentication token + TOKEN_REFRESH = "TOKEN_REFRESH", +} From 456bf84f3cc2ed6f5350dc8602ad782277745cc8 Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Wed, 11 Jun 2025 15:01:01 +0200 Subject: [PATCH 20/81] fix: remove unnecessary params --- .../wrapper/client/ConnectionManager/ConnectionManager.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Tokenization/backend/wrapper/client/ConnectionManager/ConnectionManager.ts b/Tokenization/backend/wrapper/client/ConnectionManager/ConnectionManager.ts index 73184263b..261f0f456 100644 --- a/Tokenization/backend/wrapper/client/ConnectionManager/ConnectionManager.ts +++ b/Tokenization/backend/wrapper/client/ConnectionManager/ConnectionManager.ts @@ -71,10 +71,7 @@ export class ConnectionManager { ); break; case DuplexMessageEvent.REVOKE_TOKEN: - this.handleRevokeToken( - payload.revokeToken.token, - payload.revokeToken.targetAddress - ); + this.handleRevokeToken(payload.revokeToken.token); break; default: console.warn(`Unhandled event: ${payload.event}`); @@ -111,7 +108,7 @@ export class ConnectionManager { } } - private handleRevokeToken(newToken: string, targetAddress: string) { + private handleRevokeToken(targetAddress: string) { console.log(`Revoke token for ${targetAddress}`); const sendingConnection = this.sendingConnections.get(targetAddress); From 93c3a51a44a09f36d73e59d91c58d9fcc75fb8f0 Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Tue, 17 Jun 2025 10:49:54 +0200 Subject: [PATCH 21/81] fix: fix enum naming --- Tokenization/backend/wrapper/proto/wrapper.proto | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Tokenization/backend/wrapper/proto/wrapper.proto b/Tokenization/backend/wrapper/proto/wrapper.proto index fa2f9ba07..ce1ec1c11 100644 --- a/Tokenization/backend/wrapper/proto/wrapper.proto +++ b/Tokenization/backend/wrapper/proto/wrapper.proto @@ -1,18 +1,18 @@ syntax = "proto3"; -package wrapper; +package webui.tokenization; // ====================================== // ENUMS // ====================================== enum MessageEvent { // Default value, represents an empty event - EMPTY_EVENT = 0; + MESSAGE_EVENT_EMPTY = 0; // New token message type, contains a new token and target address - NEW_TOKEN = 1; + MESSAGE_EVENT_NEW_TOKEN = 1; // Revoke token message type, contains a token to be revoked - REVOKE_TOKEN = 2; + MESSAGE_EVENT_REVOKE_TOKEN = 2; } @@ -30,7 +30,9 @@ message Token { // Stream message that can contain one of specific messages message Payload { + // Message event type MessageEvent event = 1; + // Data related to specific event type oneof data { EmptyMessage emptyMessage = 2; Token newToken = 3; @@ -43,6 +45,7 @@ message Payload { // SERVICES // ====================================== +// Central System service handling duplex communication with wrapper client service CentralSystem { rpc ClientStream(stream Payload) returns (stream Payload); } \ No newline at end of file From ff0faf0d89c73fa7024ab9f1e899b81cf463dd07 Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Tue, 17 Jun 2025 11:02:20 +0200 Subject: [PATCH 22/81] fix: fix proto types order based on Bookkeeping standards --- .../backend/wrapper/proto/wrapper.proto | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/Tokenization/backend/wrapper/proto/wrapper.proto b/Tokenization/backend/wrapper/proto/wrapper.proto index ce1ec1c11..ee50f75f8 100644 --- a/Tokenization/backend/wrapper/proto/wrapper.proto +++ b/Tokenization/backend/wrapper/proto/wrapper.proto @@ -1,24 +1,20 @@ syntax = "proto3"; + package webui.tokenization; // ====================================== -// ENUMS +// SERVICES // ====================================== -enum MessageEvent { - // Default value, represents an empty event - MESSAGE_EVENT_EMPTY = 0; - - // New token message type, contains a new token and target address - MESSAGE_EVENT_NEW_TOKEN = 1; - // Revoke token message type, contains a token to be revoked - MESSAGE_EVENT_REVOKE_TOKEN = 2; +// Central System service handling duplex communication with wrapper client +service CentralSystem { + rpc ClientStream(stream Payload) returns (stream Payload); } - // ====================================== // MESSAGES // ====================================== + // Simulates an empty message because protobuffer doesn't support void message EmptyMessage {} @@ -40,12 +36,17 @@ message Payload { } } - // ====================================== -// SERVICES +// ENUMS // ====================================== -// Central System service handling duplex communication with wrapper client -service CentralSystem { - rpc ClientStream(stream Payload) returns (stream Payload); -} \ No newline at end of file +enum MessageEvent { + // Default value, represents an empty event + MESSAGE_EVENT_EMPTY = 0; + + // New token message type, contains a new token and target address + MESSAGE_EVENT_NEW_TOKEN = 1; + + // Revoke token message type, contains a token to be revoked + MESSAGE_EVENT_REVOKE_TOKEN = 2; +} From fb6c39cafd87f200224ec71257010b221d567dbd Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Tue, 17 Jun 2025 11:14:00 +0200 Subject: [PATCH 23/81] fix: add copyright banner --- Tokenization/backend/wrapper/proto/wrapper.proto | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Tokenization/backend/wrapper/proto/wrapper.proto b/Tokenization/backend/wrapper/proto/wrapper.proto index ee50f75f8..9cb03f258 100644 --- a/Tokenization/backend/wrapper/proto/wrapper.proto +++ b/Tokenization/backend/wrapper/proto/wrapper.proto @@ -1,3 +1,16 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. +*/ syntax = "proto3"; package webui.tokenization; From 72042c809050984a19cbeac5aabda5dcc1b2d8cc Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Tue, 17 Jun 2025 11:38:24 +0200 Subject: [PATCH 24/81] fix: add copyright banner and change enum naming convention --- .../backend/wrapper/models/message.model.ts | 39 +++++++++++++++---- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/Tokenization/backend/wrapper/models/message.model.ts b/Tokenization/backend/wrapper/models/message.model.ts index 3a90dd739..8a2a82a3d 100644 --- a/Tokenization/backend/wrapper/models/message.model.ts +++ b/Tokenization/backend/wrapper/models/message.model.ts @@ -1,15 +1,37 @@ /** - * @description Enum for duplex message events. - * EMPTY_EVENT: No event, used for initialization or no response. - * NEW_TOKEN: Event for replacing with newly generated token. - * REVOKE_TOKEN: Event for revoking an existing token. + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +// ====================================== +// ENUMS +// ====================================== + +/** + * @enum Represents the types of events that can occur in a duplex message exchange. + * @property MESSAGE_EVENT_EMPTY: No event, used for initialization or no response. + * @property MESSAGE_EVENT_NEW_TOKEN: Event for replacing with newly generated token. + * @property MESSAGE_EVENT_REVOKE_TOKEN: Event for revoking an existing token. */ enum DuplexMessageEvent { - EMPTY_EVENT, - NEW_TOKEN, - REVOKE_TOKEN, + MESSAGE_EVENT_EMPTY = "MESSAGE_EVENT_EMPTY", + MESSAGE_EVENT_NEW_TOKEN = "MESSAGE_EVENT_NEW_TOKEN", + MESSAGE_EVENT_REVOKE_TOKEN = "MESSAGE_EVENT_REVOKE_TOKEN", } +// ====================================== +// INTERFACES +// ====================================== + /** * @description Model for token generation and revocation messages. * @property {string} token - The token to be replaced or revoked. @@ -23,7 +45,8 @@ interface TokenMessage { /** * @description Model for duplex stream messages between client and central system. * @property {DuplexMessageEvent} event - The event type of the message. - * @property {TokenMessage} [data] - The data associated with the event, it may be undefined for some events. + * @property {TokenMessage} data - The data associated with the event, it may be undefined for some events. + * @example {event: DuplexMessageEvent.MESSAGE_EVENT_NEW_TOKEN, data: {token: '', targetAddress: ''}} */ interface DuplexMessageModel { event: DuplexMessageEvent; From b70cf9e57ea819c2a0e1e98e16a2f9464034b8ba Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Tue, 17 Jun 2025 11:59:18 +0200 Subject: [PATCH 25/81] fix: fix serialization comments and tests --- Tokenization/backend/wrapper/package.json | 6 +-- .../__tests__/serialization.utils.test.ts | 31 ++++++------ .../wrapper/utils/serialization.utils.ts | 48 ++++++++++--------- 3 files changed, 42 insertions(+), 43 deletions(-) diff --git a/Tokenization/backend/wrapper/package.json b/Tokenization/backend/wrapper/package.json index a8b9f98a6..5bb96d468 100644 --- a/Tokenization/backend/wrapper/package.json +++ b/Tokenization/backend/wrapper/package.json @@ -3,11 +3,9 @@ "version": "1.0.0", "type": "module", "scripts": { - "test": "mocha --loader ts-node/esm \"./**/*.test.ts\"" + "test": "jest" }, - "author": "", - "license": "ISC", - "description": "", + "author": "ALICEO2", "devDependencies": { "@types/jest": "^29.5.14", "jest": "^29.7.0", diff --git a/Tokenization/backend/wrapper/utils/__tests__/serialization.utils.test.ts b/Tokenization/backend/wrapper/utils/__tests__/serialization.utils.test.ts index 655b0ccef..4c2194784 100644 --- a/Tokenization/backend/wrapper/utils/__tests__/serialization.utils.test.ts +++ b/Tokenization/backend/wrapper/utils/__tests__/serialization.utils.test.ts @@ -1,3 +1,17 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + import { deserializeRequest, serializeRequest } from "../serialization.utils"; import { describe, expect, test } from "@jest/globals"; @@ -23,23 +37,6 @@ describe("serializeRequest", () => { expect(json.url).toBe(url); expect(json.options.method).toBe(options.method); }); - - test("serializes URL only if options are not provided", () => { - const url = "/api/simple"; - const buffer = serializeRequest(url); - const view = new Uint8Array(buffer); - - const typeLength = view[0]; - const typeBytes = view.slice(1, 1 + typeLength); - const jsonBytes = view.slice(1 + typeLength); - - const contentType = new TextDecoder().decode(typeBytes); - const json = JSON.parse(new TextDecoder().decode(jsonBytes)); - - expect(contentType).toBe("application/json"); - expect(json.url).toBe(url); - expect(json).not.toHaveProperty("options"); - }); }); describe("deserializeRequest", () => { diff --git a/Tokenization/backend/wrapper/utils/serialization.utils.ts b/Tokenization/backend/wrapper/utils/serialization.utils.ts index 2f1e9bf41..57ca8bcd2 100644 --- a/Tokenization/backend/wrapper/utils/serialization.utils.ts +++ b/Tokenization/backend/wrapper/utils/serialization.utils.ts @@ -1,24 +1,33 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + /** * @description Serializes Json formatted request into binary payload with specific endpoint * @param url - The endpoint URL to which the request is made - * @param options - Optional request options, such as headers or body + * @param options - Request options, such as headers or body * @return {ArrayBuffer} - The serialized binary payload containing the URL and options */ -export const serializeRequest = (url: string, options?: any) => { +export const serializeRequest = (url: string, options: any): ArrayBuffer => { const encoder = new TextEncoder(); const contentTypeBytes = encoder.encode("application/json"); // build JSON data - const jsonData = options - ? { - url: url, - options: options, - } - : { - url: url, - }; + const jsonData = { + url: url, + options: options, + }; - // encode JSON const jsonString = JSON.stringify(jsonData); const jsonBytes = encoder.encode(jsonString); @@ -40,7 +49,7 @@ export const serializeRequest = (url: string, options?: any) => { * @param payload - The binary payload to deserialize * @return {any} - The deserialized request object containing the URL and options */ -export const deserializeRequest = (payload: any) => { +export const deserializeRequest = (payload: ArrayBuffer): any => { const view = new Uint8Array(payload); const decoder = new TextDecoder(); @@ -49,16 +58,11 @@ export const deserializeRequest = (payload: any) => { const contentType = decoder.decode(contentTypeBytes); const dataBytes = view.slice(1 + contentTypeLength); - let data; - // deserialization based on content type - switch (contentType) { - case "application/json": - data = JSON.parse(decoder.decode(dataBytes)); - break; - default: - throw new Error(`Unsupported content type: ${contentType}`); + // deserialization of JSON content + if (contentType === "application/json") { + return JSON.parse(decoder.decode(dataBytes)); + } else { + throw new Error(`Unsupported content type: ${contentType}`); } - - return data; }; From bdba63de6f09804f1556a4f22dbeacfe94579386 Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Tue, 8 Jul 2025 20:32:36 +0200 Subject: [PATCH 26/81] feat: add description comments and implement .listen() instead of automatic start of listener --- .../backend/wrapper/central/CentralSystem.ts | 37 +++- .../ConnectionManager/ConnectionManager.ts | 51 +++-- .../backend/wrapper/client/gRPCWrapper.ts | 34 +++- .../utils/__tests__/presentation.test.ts | 180 ++++++++++++++++++ 4 files changed, 278 insertions(+), 24 deletions(-) create mode 100644 Tokenization/backend/wrapper/utils/__tests__/presentation.test.ts diff --git a/Tokenization/backend/wrapper/central/CentralSystem.ts b/Tokenization/backend/wrapper/central/CentralSystem.ts index 3058ce7bd..0130635cf 100644 --- a/Tokenization/backend/wrapper/central/CentralSystem.ts +++ b/Tokenization/backend/wrapper/central/CentralSystem.ts @@ -9,16 +9,24 @@ import { fileURLToPath } from "url"; export class CentralSystemWrapper { private server: grpc.Server; + /** + * Initializes the Wrapper for CentralSystem. + * @param port The port number to bind the gRPC server to. + */ constructor(private port: number) { this.server = new grpc.Server(); this.setupService(); - this.start(); } - private setupService() { + /** + * @description Loads the gRPC proto definition and sets up the CentralSystem service. + */ + private setupService(): void { const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const PROTO_PATH = path.join(__dirname, "../proto/wrapper.proto"); + + // Load the proto definition with options const packageDef = protoLoader.loadSync(PROTO_PATH, { keepCase: true, longs: String, @@ -27,38 +35,46 @@ export class CentralSystemWrapper { oneofs: true, }); + // Load the package definition into a gRPC object const proto = grpc.loadPackageDefinition(packageDef) as any; const wrapper = proto.wrapper; + // Add the CentralSystem service and bind the stream handler this.server.addService(wrapper.CentralSystem.service, { ClientStream: this.clientStreamHandler.bind(this), }); } - private clientStreamHandler(call: grpc.ServerDuplexStream) { + /** + * @description Handles the duplex stream from the client. + * @param call The duplex stream call object. + */ + private clientStreamHandler(call: grpc.ServerDuplexStream): void { console.log("Client connected to duplex stream"); - - // hartbeat message - call.write({ event: "EMPTY_EVENT", emptyMessage: {} }); - + // Listen for data events from the client call.on("data", (payload: any) => { // TODO: Implement data handling logic }); + // Handle stream end event call.on("end", () => { console.log("Client ended stream"); call.end(); }); + // Handle stream error event call.on("error", (err) => console.error("Stream error:", err)); } - private start() { + /** + * @desciprion Starts the gRPC server and binds it to the specified in class port. + */ + public listen() { const addr = `localhost:${this.port}`; this.server.bindAsync( addr, grpc.ServerCredentials.createInsecure(), - (err, port) => { + (err, _port) => { if (err) { console.error("Server bind error:", err); return; @@ -69,4 +85,7 @@ export class CentralSystemWrapper { } } +// Instantiate the CentralSystemWrapper on port 50051, but don't start automatically const centralSystem = new CentralSystemWrapper(50051); +// Start listening explicitly +centralSystem.listen(); diff --git a/Tokenization/backend/wrapper/client/ConnectionManager/ConnectionManager.ts b/Tokenization/backend/wrapper/client/ConnectionManager/ConnectionManager.ts index ae8f2cb1b..b14f65d85 100644 --- a/Tokenization/backend/wrapper/client/ConnectionManager/ConnectionManager.ts +++ b/Tokenization/backend/wrapper/client/ConnectionManager/ConnectionManager.ts @@ -1,24 +1,42 @@ import * as grpc from "@grpc/grpc-js"; import * as protoLoader from "@grpc/proto-loader"; -import path from "path"; -import { fileURLToPath } from "url"; /** * @description Manages all the connection between clients and central system. */ +/** + * Manages the lifecycle and connection logic for a gRPC client communicating with the central system. + * + * This class is responsible for: + * - Initializing the gRPC client using the provided proto definition and address. + * - Managing a duplex stream (`stream`) for bidirectional communication. + * - Handling automatic reconnection with exponential backoff on stream errors or disconnects. + * - Providing methods to start (`connectToCentralSystem`) and stop (`disconnect`) the connection with central system. + * + * @remarks + * - `client`: The gRPC client instance for communicating with the central system. + * - `stream`: The active duplex stream for sending and receiving messages (optional). + * - `address`: The address of the central gRPC server. + * - `reconnectAttempts`: The number of consecutive reconnection attempts made after a disconnect or error. + */ export class ConnectionManager { private client: any; private stream?: grpc.ClientDuplexStream; private readonly address: string; private reconnectAttempts = 0; - constructor(centralAddress = "localhost:50051") { + /** + * @description Initializes a new instance of the ConnectionManager class. + * + * This constructor sets up the gRPC client for communication with the central system. + * + * @param protoPath - The file path to the gRPC proto definition. + * @param centralAddress - The address of the central gRPC server (default: "localhost:50051"). + */ + constructor(protoPath: string, centralAddress: string = "localhost:50051") { this.address = centralAddress; - const __filename = fileURLToPath(import.meta.url); - const __dirname = path.dirname(__filename); - const PROTO_PATH = path.join(__dirname, "../../proto/wrapper.proto"); - const packageDef = protoLoader.loadSync(PROTO_PATH, { + const packageDef = protoLoader.loadSync(protoPath, { keepCase: true, longs: String, enums: String, @@ -34,16 +52,13 @@ export class ConnectionManager { this.address, grpc.credentials.createInsecure() ); - - // Initial connection - this.connect(); - console.log(`ConnectionManager: connected to ${this.address}`); } /** * @description Initializes the duplex stream and sets up handlers. */ private connect() { + if (this.stream) return; this.stream = this.client.ClientStream(); if (this.stream) { @@ -53,11 +68,13 @@ export class ConnectionManager { this.stream.on("end", () => { console.warn("Stream ended, attempting to reconnect..."); + this.stream = undefined; this.scheduleReconnect(); }); this.stream.on("error", (err: any) => { console.error("Wrapper stream error:", err); + this.stream = undefined; this.scheduleReconnect(); }); } @@ -75,10 +92,20 @@ export class ConnectionManager { }, delay); } + /** + * @description Starts the connection to the central system. + */ + public connectToCentralSystem() { + if (!this.stream) { + this.connect(); + console.log(`ConnectionManager: connected to ${this.address}`); + } + } + /** * @description Disconnects from the gRPC stream and resets attempts. */ - disconnect() { + public disconnect() { if (this.stream) { this.stream.end(); this.stream = undefined; diff --git a/Tokenization/backend/wrapper/client/gRPCWrapper.ts b/Tokenization/backend/wrapper/client/gRPCWrapper.ts index a4a02b19a..898785caf 100644 --- a/Tokenization/backend/wrapper/client/gRPCWrapper.ts +++ b/Tokenization/backend/wrapper/client/gRPCWrapper.ts @@ -1,11 +1,39 @@ import { ConnectionManager } from "./ConnectionManager/ConnectionManager.ts"; +/** + * @description Wrapper class for managing secure gRPC wrapper. + * + * @remarks + * This class serves as a high-level abstraction over the underlying + * `ConnectionManager`, providing a simplified interface for establishing + * and managing gRPC connections within the application. + * + * @example + * ```typescript + * const grpcWrapper = new gRPCWrapper(); + * // Use grpcWrapper to interact with gRPC services + * ``` + */ export class gRPCWrapper { private ConnectionManager: ConnectionManager; - constructor() { - this.ConnectionManager = new ConnectionManager(); + /** + * @description Initializes an instance of gRPCWrapper class. + * + * @param protoPath - The file path to the gRPC proto definition. + * @param centralAddress - The address of the central gRPC server (default: "localhost:50051"). + */ + constructor(protoPath: string, centralAddress: string = "localhost:50051") { + this.ConnectionManager = new ConnectionManager(protoPath, centralAddress); + } + + /** + * @description Starts the Connection Manager stream connection with Central System + */ + public connectToCentralSystem(): void { + this.ConnectionManager.connectToCentralSystem(); } } -const grpc = new gRPCWrapper(); +const grpc = new gRPCWrapper("../proto/wrapper.proto", "localhost:50051"); +grpc.connectToCentralSystem(); diff --git a/Tokenization/backend/wrapper/utils/__tests__/presentation.test.ts b/Tokenization/backend/wrapper/utils/__tests__/presentation.test.ts new file mode 100644 index 000000000..039cb3c46 --- /dev/null +++ b/Tokenization/backend/wrapper/utils/__tests__/presentation.test.ts @@ -0,0 +1,180 @@ +import { serializeRequest } from "../serialization.utils"; +// 1. describe(), test(), it() +// test suite "Basic Jest methods" +describe("Basic Jest methods", () => { + test("adds numbers correctly", () => { + expect(1 + 2).toBe(3); + }); + + it("subtracts numbers correctly", () => { + expect(5 - 2).toBe(3); + }); +}); + +// 2. expect() + matchers +describe("expect + matchers", () => { + it("should present expect + matchers methods", () => { + expect(2 + 2).toBe(4); // strict equality + expect([1, 2]).toEqual([1, 2]); // deep equality for arrays + expect("hello world").toContain("hello"); + expect(() => JSON.parse("{")).toThrow(); // expects function to throw error + }); + + const add = (a, b) => { + return a + b; + }; + it.each([ + [1, 1, 2], + [2, 2, 4], + [1, 2, 3], + ])("should correctly add numbers", (a, b, result) => { + expect(add(a, b)).toBe(result); + }); +}); + +// 3. Mocking + SpyOn + +// SpyOn +class importedClass { + constructor() {} + + private testFunc = () => { + return "hello from test"; + }; + + public test() { + return this.testFunc(); + } +} + +// mock +jest.mock("../serialization.utils.ts", () => ({ + serializeRequest: jest.fn().mockResolvedValue("Hi"), +})); + +describe("Mocking + SpyOn", () => { + const testClass = new importedClass(); + const spy = jest.spyOn(testClass as any, "testFunc"); + + it("should mock return value of serializeRequest", async () => { + const result = await serializeRequest("/test", {}); + expect(result).toEqual("Hi"); + }); + + it("testFuncCall should be executed with spyon mock", () => { + expect(testClass.test()).toEqual("hello from test"); + spy.mockReturnValue("Hi"); + expect(testClass.test()).toEqual("Hi"); + expect(spy).toHaveBeenCalled(); + }); +}); + +// 4. Resets and cleanups +describe("Resets + mocks", () => { + it("should not call mock after cleanup", () => { + const mock = jest.fn(); + mock(); + jest.clearAllMocks(); + expect(mock).not.toHaveBeenCalled(); + }); + + it("should restore mocks", () => { + const obj = { + greet: () => "hi", + }; + + jest.spyOn(obj, "greet").mockImplementation(() => "mock"); + jest.restoreAllMocks(); + expect(obj.greet()).toBe("hi"); + }); +}); + +// 5. beforeEach +const mockFn = jest.fn(); + +describe("mockFn test suite", () => { + beforeEach(() => { + mockFn.mockClear(); + }); + + test("mockFn is called once", () => { + mockFn(); + expect(mockFn).toHaveBeenCalledTimes(1); + }); + + test("mockFn is clean before this test", () => { + expect(mockFn).not.toHaveBeenCalled(); + }); +}); + +// +// +// ConnectionManager.test.ts + +jest.mock("@grpc/grpc-js", () => ({ + credentials: { createInsecure: jest.fn() }, + loadPackageDefinition: jest.fn(), +})); + +jest.mock("@grpc/proto-loader", () => ({ + loadSync: jest.fn(() => ({})), +})); + +import * as grpc from "@grpc/grpc-js"; +import { ConnectionManager } from "../../client/ConnectionManager/ConnectionManager"; + +describe("ConnectionManager", () => { + let connectionManager: ConnectionManager; + const fakeStream = { + on: jest.fn(), + end: jest.fn(), + }; + + beforeAll(() => { + const fakeWrapper = { + CentralSystem: jest.fn().mockImplementation(() => ({ + ClientStream: jest.fn(() => fakeStream), + })), + }; + + (grpc.loadPackageDefinition as jest.Mock).mockReturnValue({ + wrapper: fakeWrapper, + }); + }); + + beforeEach(() => { + jest.clearAllMocks(); + connectionManager = new ConnectionManager( + "./proto/wrapper.proto", + "localhost:50051" + ); + (connectionManager as any).client = { + ClientStream: jest.fn(() => fakeStream), + }; + + (connectionManager as any).scheduleReconnect = jest.fn(); + }); + + it("should correctly set up data listener on ClientStream", () => { + connectionManager.connectToCentralSystem(); + expect(fakeStream.on).toHaveBeenCalledWith("data", expect.any(Function)); + }); + + it("should warn and reset stream on end event", () => { + connectionManager.connectToCentralSystem(); + const endHandler = fakeStream.on.mock.calls.find( + ([event]) => event === "end" + )![1]; + endHandler(); + expect((connectionManager as any).scheduleReconnect).toHaveBeenCalled(); + }); + + it("should handle error event and reset stream", () => { + connectionManager.connectToCentralSystem(); + const errorHandler = fakeStream.on.mock.calls.find( + ([event]) => event === "error" + )![1]; + errorHandler(new Error("test error")); + expect((connectionManager as any).scheduleReconnect).toHaveBeenCalled(); + }); +}); From 326eea5defe5d8cbc232adb23ff680589197ae84 Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Tue, 8 Jul 2025 20:33:13 +0200 Subject: [PATCH 27/81] fix: change enum values to numbers --- Tokenization/backend/wrapper/models/message.model.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tokenization/backend/wrapper/models/message.model.ts b/Tokenization/backend/wrapper/models/message.model.ts index 8a2a82a3d..d74c98872 100644 --- a/Tokenization/backend/wrapper/models/message.model.ts +++ b/Tokenization/backend/wrapper/models/message.model.ts @@ -23,9 +23,9 @@ * @property MESSAGE_EVENT_REVOKE_TOKEN: Event for revoking an existing token. */ enum DuplexMessageEvent { - MESSAGE_EVENT_EMPTY = "MESSAGE_EVENT_EMPTY", - MESSAGE_EVENT_NEW_TOKEN = "MESSAGE_EVENT_NEW_TOKEN", - MESSAGE_EVENT_REVOKE_TOKEN = "MESSAGE_EVENT_REVOKE_TOKEN", + MESSAGE_EVENT_EMPTY = 0, + MESSAGE_EVENT_NEW_TOKEN = 1, + MESSAGE_EVENT_REVOKE_TOKEN = 2, } // ====================================== From 58576521187923a233a23992d94554722cb3601e Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Tue, 8 Jul 2025 20:44:43 +0200 Subject: [PATCH 28/81] fix: move test directory --- .../{utils/__tests__ => test/utils}/presentation.test.ts | 2 +- .../__tests__ => test/utils}/serialization.utils.test.ts | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) rename Tokenization/backend/wrapper/{utils/__tests__ => test/utils}/presentation.test.ts (98%) rename Tokenization/backend/wrapper/{utils/__tests__ => test/utils}/serialization.utils.test.ts (95%) diff --git a/Tokenization/backend/wrapper/utils/__tests__/presentation.test.ts b/Tokenization/backend/wrapper/test/utils/presentation.test.ts similarity index 98% rename from Tokenization/backend/wrapper/utils/__tests__/presentation.test.ts rename to Tokenization/backend/wrapper/test/utils/presentation.test.ts index 039cb3c46..f90e445ba 100644 --- a/Tokenization/backend/wrapper/utils/__tests__/presentation.test.ts +++ b/Tokenization/backend/wrapper/test/utils/presentation.test.ts @@ -1,4 +1,4 @@ -import { serializeRequest } from "../serialization.utils"; +import { serializeRequest } from "../../utils/serialization.utils"; // 1. describe(), test(), it() // test suite "Basic Jest methods" describe("Basic Jest methods", () => { diff --git a/Tokenization/backend/wrapper/utils/__tests__/serialization.utils.test.ts b/Tokenization/backend/wrapper/test/utils/serialization.utils.test.ts similarity index 95% rename from Tokenization/backend/wrapper/utils/__tests__/serialization.utils.test.ts rename to Tokenization/backend/wrapper/test/utils/serialization.utils.test.ts index 4c2194784..93d5c7d7c 100644 --- a/Tokenization/backend/wrapper/utils/__tests__/serialization.utils.test.ts +++ b/Tokenization/backend/wrapper/test/utils/serialization.utils.test.ts @@ -12,7 +12,10 @@ * or submit itself to any jurisdiction. */ -import { deserializeRequest, serializeRequest } from "../serialization.utils"; +import { + deserializeRequest, + serializeRequest, +} from "../../utils/serialization.utils"; import { describe, expect, test } from "@jest/globals"; describe("serializeRequest", () => { From 54447ea00ea07fdce40b9b65f4fa90f0f409ac9d Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Tue, 8 Jul 2025 21:29:32 +0200 Subject: [PATCH 29/81] feat: add webui logger instead of console logs and fix paths --- .../backend/wrapper/central/CentralSystem.ts | 25 ++- .../ConnectionManager/ConnectionManager.ts | 25 ++- .../backend/wrapper/client/gRPCWrapper.ts | 7 +- .../wrapper/test/utils/presentation.test.ts | 180 ------------------ 4 files changed, 44 insertions(+), 193 deletions(-) delete mode 100644 Tokenization/backend/wrapper/test/utils/presentation.test.ts diff --git a/Tokenization/backend/wrapper/central/CentralSystem.ts b/Tokenization/backend/wrapper/central/CentralSystem.ts index 0130635cf..baa5fbe46 100644 --- a/Tokenization/backend/wrapper/central/CentralSystem.ts +++ b/Tokenization/backend/wrapper/central/CentralSystem.ts @@ -2,11 +2,16 @@ import * as grpc from "@grpc/grpc-js"; import * as protoLoader from "@grpc/proto-loader"; import path from "path"; import { fileURLToPath } from "url"; +import { LogManager } from "@aliceo2/web-ui"; /** * @description Central System gRPC wrapper that manages client connections and handles gRPC streams with them. */ export class CentralSystemWrapper { + // utilities + private logger = LogManager.getLogger("CentralSystemWrapper"); + + // class properties private server: grpc.Server; /** @@ -37,7 +42,7 @@ export class CentralSystemWrapper { // Load the package definition into a gRPC object const proto = grpc.loadPackageDefinition(packageDef) as any; - const wrapper = proto.wrapper; + const wrapper = proto.webui.tokenization; // Add the CentralSystem service and bind the stream handler this.server.addService(wrapper.CentralSystem.service, { @@ -50,7 +55,10 @@ export class CentralSystemWrapper { * @param call The duplex stream call object. */ private clientStreamHandler(call: grpc.ServerDuplexStream): void { - console.log("Client connected to duplex stream"); + this.logger.infoMessage( + `Client ${call.getPeer()} connected to CentralSystem stream stream` + ); + // Listen for data events from the client call.on("data", (payload: any) => { // TODO: Implement data handling logic @@ -58,12 +66,17 @@ export class CentralSystemWrapper { // Handle stream end event call.on("end", () => { - console.log("Client ended stream"); + this.logger.infoMessage(`Client ${call.getPeer()} ended stream.`); call.end(); }); // Handle stream error event - call.on("error", (err) => console.error("Stream error:", err)); + call.on("error", (err) => + this.logger.infoMessage( + `Stream error from client ${call.getPeer()}:`, + err + ) + ); } /** @@ -76,10 +89,10 @@ export class CentralSystemWrapper { grpc.ServerCredentials.createInsecure(), (err, _port) => { if (err) { - console.error("Server bind error:", err); + this.logger.infoMessage("Server bind error:", err); return; } - console.log(`Server listening on ${addr}`); + this.logger.infoMessage(`CentralSytem started listening on ${addr}`); } ); } diff --git a/Tokenization/backend/wrapper/client/ConnectionManager/ConnectionManager.ts b/Tokenization/backend/wrapper/client/ConnectionManager/ConnectionManager.ts index b14f65d85..ed6ddb48b 100644 --- a/Tokenization/backend/wrapper/client/ConnectionManager/ConnectionManager.ts +++ b/Tokenization/backend/wrapper/client/ConnectionManager/ConnectionManager.ts @@ -1,5 +1,6 @@ import * as grpc from "@grpc/grpc-js"; import * as protoLoader from "@grpc/proto-loader"; +import { LogManager } from "@aliceo2/web-ui"; /** * @description Manages all the connection between clients and central system. @@ -20,6 +21,10 @@ import * as protoLoader from "@grpc/proto-loader"; * - `reconnectAttempts`: The number of consecutive reconnection attempts made after a disconnect or error. */ export class ConnectionManager { + // utilities + private logger = LogManager.getLogger("ConnectionManager"); + + // class properties private client: any; private stream?: grpc.ClientDuplexStream; private readonly address: string; @@ -45,7 +50,7 @@ export class ConnectionManager { }); const proto = grpc.loadPackageDefinition(packageDef) as any; - const wrapper = proto.wrapper; + const wrapper = proto.webui.tokenization; // Create gRPC client this.client = new wrapper.CentralSystem( @@ -67,13 +72,17 @@ export class ConnectionManager { }); this.stream.on("end", () => { - console.warn("Stream ended, attempting to reconnect..."); + this.logger.infoMessage(`Stream ended, attempting to reconnect...`); this.stream = undefined; this.scheduleReconnect(); }); this.stream.on("error", (err: any) => { - console.error("Wrapper stream error:", err); + this.logger.infoMessage( + `Stream error:`, + err, + " attempting to reconnect..." + ); this.stream = undefined; this.scheduleReconnect(); }); @@ -87,7 +96,9 @@ export class ConnectionManager { this.reconnectAttempts++; const delay = Math.min(1000 * 2 ** this.reconnectAttempts, 30000); setTimeout(() => { - console.log(`Reconnecting (attempt ${this.reconnectAttempts})...`); + this.logger.infoMessage( + `Reconnecting (attempt ${this.reconnectAttempts})...` + ); this.connect(); }, delay); } @@ -98,7 +109,9 @@ export class ConnectionManager { public connectToCentralSystem() { if (!this.stream) { this.connect(); - console.log(`ConnectionManager: connected to ${this.address}`); + this.logger.infoMessage( + `Connected to CentralSystem service at ${this.address}` + ); } } @@ -111,6 +124,6 @@ export class ConnectionManager { this.stream = undefined; } this.reconnectAttempts = 0; - console.log("Disconnected from central"); + this.logger.infoMessage(`Disconnected from CentralSystem service`); } } diff --git a/Tokenization/backend/wrapper/client/gRPCWrapper.ts b/Tokenization/backend/wrapper/client/gRPCWrapper.ts index 898785caf..f7bc48c9a 100644 --- a/Tokenization/backend/wrapper/client/gRPCWrapper.ts +++ b/Tokenization/backend/wrapper/client/gRPCWrapper.ts @@ -1,4 +1,6 @@ +import path from "path"; import { ConnectionManager } from "./ConnectionManager/ConnectionManager.ts"; +import { fileURLToPath } from "url"; /** * @description Wrapper class for managing secure gRPC wrapper. @@ -35,5 +37,8 @@ export class gRPCWrapper { } } -const grpc = new gRPCWrapper("../proto/wrapper.proto", "localhost:50051"); +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const PROTO_PATH = path.join(__dirname, "../proto/wrapper.proto"); +const grpc = new gRPCWrapper(PROTO_PATH, "localhost:50051"); grpc.connectToCentralSystem(); diff --git a/Tokenization/backend/wrapper/test/utils/presentation.test.ts b/Tokenization/backend/wrapper/test/utils/presentation.test.ts deleted file mode 100644 index f90e445ba..000000000 --- a/Tokenization/backend/wrapper/test/utils/presentation.test.ts +++ /dev/null @@ -1,180 +0,0 @@ -import { serializeRequest } from "../../utils/serialization.utils"; -// 1. describe(), test(), it() -// test suite "Basic Jest methods" -describe("Basic Jest methods", () => { - test("adds numbers correctly", () => { - expect(1 + 2).toBe(3); - }); - - it("subtracts numbers correctly", () => { - expect(5 - 2).toBe(3); - }); -}); - -// 2. expect() + matchers -describe("expect + matchers", () => { - it("should present expect + matchers methods", () => { - expect(2 + 2).toBe(4); // strict equality - expect([1, 2]).toEqual([1, 2]); // deep equality for arrays - expect("hello world").toContain("hello"); - expect(() => JSON.parse("{")).toThrow(); // expects function to throw error - }); - - const add = (a, b) => { - return a + b; - }; - it.each([ - [1, 1, 2], - [2, 2, 4], - [1, 2, 3], - ])("should correctly add numbers", (a, b, result) => { - expect(add(a, b)).toBe(result); - }); -}); - -// 3. Mocking + SpyOn - -// SpyOn -class importedClass { - constructor() {} - - private testFunc = () => { - return "hello from test"; - }; - - public test() { - return this.testFunc(); - } -} - -// mock -jest.mock("../serialization.utils.ts", () => ({ - serializeRequest: jest.fn().mockResolvedValue("Hi"), -})); - -describe("Mocking + SpyOn", () => { - const testClass = new importedClass(); - const spy = jest.spyOn(testClass as any, "testFunc"); - - it("should mock return value of serializeRequest", async () => { - const result = await serializeRequest("/test", {}); - expect(result).toEqual("Hi"); - }); - - it("testFuncCall should be executed with spyon mock", () => { - expect(testClass.test()).toEqual("hello from test"); - spy.mockReturnValue("Hi"); - expect(testClass.test()).toEqual("Hi"); - expect(spy).toHaveBeenCalled(); - }); -}); - -// 4. Resets and cleanups -describe("Resets + mocks", () => { - it("should not call mock after cleanup", () => { - const mock = jest.fn(); - mock(); - jest.clearAllMocks(); - expect(mock).not.toHaveBeenCalled(); - }); - - it("should restore mocks", () => { - const obj = { - greet: () => "hi", - }; - - jest.spyOn(obj, "greet").mockImplementation(() => "mock"); - jest.restoreAllMocks(); - expect(obj.greet()).toBe("hi"); - }); -}); - -// 5. beforeEach -const mockFn = jest.fn(); - -describe("mockFn test suite", () => { - beforeEach(() => { - mockFn.mockClear(); - }); - - test("mockFn is called once", () => { - mockFn(); - expect(mockFn).toHaveBeenCalledTimes(1); - }); - - test("mockFn is clean before this test", () => { - expect(mockFn).not.toHaveBeenCalled(); - }); -}); - -// -// -// ConnectionManager.test.ts - -jest.mock("@grpc/grpc-js", () => ({ - credentials: { createInsecure: jest.fn() }, - loadPackageDefinition: jest.fn(), -})); - -jest.mock("@grpc/proto-loader", () => ({ - loadSync: jest.fn(() => ({})), -})); - -import * as grpc from "@grpc/grpc-js"; -import { ConnectionManager } from "../../client/ConnectionManager/ConnectionManager"; - -describe("ConnectionManager", () => { - let connectionManager: ConnectionManager; - const fakeStream = { - on: jest.fn(), - end: jest.fn(), - }; - - beforeAll(() => { - const fakeWrapper = { - CentralSystem: jest.fn().mockImplementation(() => ({ - ClientStream: jest.fn(() => fakeStream), - })), - }; - - (grpc.loadPackageDefinition as jest.Mock).mockReturnValue({ - wrapper: fakeWrapper, - }); - }); - - beforeEach(() => { - jest.clearAllMocks(); - connectionManager = new ConnectionManager( - "./proto/wrapper.proto", - "localhost:50051" - ); - (connectionManager as any).client = { - ClientStream: jest.fn(() => fakeStream), - }; - - (connectionManager as any).scheduleReconnect = jest.fn(); - }); - - it("should correctly set up data listener on ClientStream", () => { - connectionManager.connectToCentralSystem(); - expect(fakeStream.on).toHaveBeenCalledWith("data", expect.any(Function)); - }); - - it("should warn and reset stream on end event", () => { - connectionManager.connectToCentralSystem(); - const endHandler = fakeStream.on.mock.calls.find( - ([event]) => event === "end" - )![1]; - endHandler(); - expect((connectionManager as any).scheduleReconnect).toHaveBeenCalled(); - }); - - it("should handle error event and reset stream", () => { - connectionManager.connectToCentralSystem(); - const errorHandler = fakeStream.on.mock.calls.find( - ([event]) => event === "error" - )![1]; - errorHandler(new Error("test error")); - expect((connectionManager as any).scheduleReconnect).toHaveBeenCalled(); - }); -}); From 9db612e159c0c4b8314be4586626e895eecd189d Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Wed, 9 Jul 2025 19:53:51 +0200 Subject: [PATCH 30/81] feat: write tests for central system and connection manager --- .../backend/wrapper/central/CentralSystem.ts | 12 +- .../backend/wrapper/client/gRPCWrapper.ts | 2 - .../test/central/CentralSystem.test.ts | 117 ++++++++++++++++++ .../ConnectionManager.test.ts | 108 ++++++++++++++++ 4 files changed, 229 insertions(+), 10 deletions(-) create mode 100644 Tokenization/backend/wrapper/test/central/CentralSystem.test.ts create mode 100644 Tokenization/backend/wrapper/test/client/ConnectionManager/ConnectionManager.test.ts diff --git a/Tokenization/backend/wrapper/central/CentralSystem.ts b/Tokenization/backend/wrapper/central/CentralSystem.ts index baa5fbe46..350984751 100644 --- a/Tokenization/backend/wrapper/central/CentralSystem.ts +++ b/Tokenization/backend/wrapper/central/CentralSystem.ts @@ -1,7 +1,6 @@ import * as grpc from "@grpc/grpc-js"; import * as protoLoader from "@grpc/proto-loader"; import path from "path"; -import { fileURLToPath } from "url"; import { LogManager } from "@aliceo2/web-ui"; /** @@ -18,7 +17,7 @@ export class CentralSystemWrapper { * Initializes the Wrapper for CentralSystem. * @param port The port number to bind the gRPC server to. */ - constructor(private port: number) { + constructor(private protoPath: string, private port: number) { this.server = new grpc.Server(); this.setupService(); } @@ -27,12 +26,8 @@ export class CentralSystemWrapper { * @description Loads the gRPC proto definition and sets up the CentralSystem service. */ private setupService(): void { - const __filename = fileURLToPath(import.meta.url); - const __dirname = path.dirname(__filename); - const PROTO_PATH = path.join(__dirname, "../proto/wrapper.proto"); - // Load the proto definition with options - const packageDef = protoLoader.loadSync(PROTO_PATH, { + const packageDef = protoLoader.loadSync(this.protoPath, { keepCase: true, longs: String, enums: String, @@ -99,6 +94,7 @@ export class CentralSystemWrapper { } // Instantiate the CentralSystemWrapper on port 50051, but don't start automatically -const centralSystem = new CentralSystemWrapper(50051); +const PROTO_PATH = path.join(__dirname, "../proto/wrapper.proto"); +const centralSystem = new CentralSystemWrapper(PROTO_PATH, 50051); // Start listening explicitly centralSystem.listen(); diff --git a/Tokenization/backend/wrapper/client/gRPCWrapper.ts b/Tokenization/backend/wrapper/client/gRPCWrapper.ts index f7bc48c9a..486f30e6c 100644 --- a/Tokenization/backend/wrapper/client/gRPCWrapper.ts +++ b/Tokenization/backend/wrapper/client/gRPCWrapper.ts @@ -37,8 +37,6 @@ export class gRPCWrapper { } } -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); const PROTO_PATH = path.join(__dirname, "../proto/wrapper.proto"); const grpc = new gRPCWrapper(PROTO_PATH, "localhost:50051"); grpc.connectToCentralSystem(); diff --git a/Tokenization/backend/wrapper/test/central/CentralSystem.test.ts b/Tokenization/backend/wrapper/test/central/CentralSystem.test.ts new file mode 100644 index 000000000..1599e0ccd --- /dev/null +++ b/Tokenization/backend/wrapper/test/central/CentralSystem.test.ts @@ -0,0 +1,117 @@ +const mockAddService = jest.fn(); +const mockBindAsync = jest.fn(); +const mockServerInstance = { + addService: mockAddService, + bindAsync: mockBindAsync, +}; + +const logger = { + infoMessage: jest.fn(), +}; + +jest.mock("@aliceo2/web-ui", () => ({ + LogManager: { + getLogger: () => logger, + }, +})); + +jest.mock("@grpc/proto-loader", () => ({ + loadSync: jest.fn(() => { + return {}; + }), +})); + +jest.mock("@grpc/grpc-js", () => { + const original = jest.requireActual("@grpc/grpc-js"); + return { + ...original, + Server: jest.fn(() => mockServerInstance), + ServerCredentials: { + createInsecure: jest.fn(() => "mock-credentials"), + }, + loadPackageDefinition: jest.fn(() => ({ + webui: { + tokenization: { + CentralSystem: { + service: "mock-service", + }, + }, + }, + })), + }; +}); + +import { CentralSystemWrapper } from "../../central/CentralSystem"; +import * as grpc from "@grpc/grpc-js"; + +describe("CentralSystemWrapper", () => { + let wrapper: CentralSystemWrapper; + + beforeEach(() => { + jest.clearAllMocks(); + wrapper = new CentralSystemWrapper("dummy.proto", 12345); + }); + + test("should set up gRPC service and add it to the server", () => { + expect(grpc.Server).toHaveBeenCalled(); + expect(grpc.loadPackageDefinition).toHaveBeenCalled(); + expect(grpc.ServerCredentials.createInsecure).not.toHaveBeenCalled(); + expect(wrapper).toBeDefined(); + }); + + test("should call listen and bind the server", () => { + mockBindAsync.mockImplementation((_addr, _creds, cb) => cb(null, 12345)); + + wrapper.listen(); + + expect(mockBindAsync).toHaveBeenCalledWith( + "localhost:12345", + "mock-credentials", + expect.any(Function) + ); + }); + + test("should log error if bind fails", () => { + const error = new Error("bind failed"); + mockBindAsync.mockImplementation((_addr, _creds, cb) => cb(error, null)); + + wrapper.listen(); + + expect(logger.infoMessage).toHaveBeenCalledWith( + "Server bind error:", + error + ); + }); + + test("should handle client stream events", () => { + const logger = require("@aliceo2/web-ui").LogManager.getLogger(); + + const mockCall = { + getPeer: jest.fn(() => "client123"), + on: jest.fn((event, cb) => { + if (event === "end") cb(); + if (event === "error") cb(new Error("stream error")); + }), + end: jest.fn(), + }; + + const handler = (wrapper as any).clientStreamHandler.bind(wrapper); + handler(mockCall); + + expect(mockCall.on).toHaveBeenCalledWith("data", expect.any(Function)); + expect(mockCall.on).toHaveBeenCalledWith("end", expect.any(Function)); + expect(mockCall.on).toHaveBeenCalledWith("error", expect.any(Function)); + + expect(mockCall.end).toHaveBeenCalled(); + expect(logger.infoMessage).toHaveBeenCalledWith( + "Client client123 connected to CentralSystem stream stream" + ); + expect(logger.infoMessage).toHaveBeenCalledWith( + "Client client123 ended stream." + ); + expect(logger.infoMessage).toHaveBeenCalledWith( + "Stream error from client client123:", + expect.any(Error) + ); + }); +}); diff --git a/Tokenization/backend/wrapper/test/client/ConnectionManager/ConnectionManager.test.ts b/Tokenization/backend/wrapper/test/client/ConnectionManager/ConnectionManager.test.ts new file mode 100644 index 000000000..7f55df35e --- /dev/null +++ b/Tokenization/backend/wrapper/test/client/ConnectionManager/ConnectionManager.test.ts @@ -0,0 +1,108 @@ +import * as grpc from "@grpc/grpc-js"; +import { ConnectionManager } from "../../../client/ConnectionManager/ConnectionManager"; + +// Mock of client and stream +const mockStream = { + on: jest.fn(), + end: jest.fn(), +}; + +const mockClient = { + ClientStream: jest.fn(() => mockStream), +}; + +jest.mock("@aliceo2/web-ui", () => ({ + LogManager: { + getLogger: () => ({ + infoMessage: jest.fn(), + }), + }, +})); + +jest.mock("@grpc/proto-loader", () => ({ + loadSync: jest.fn(() => { + return {}; + }), +})); + +jest.mock("@grpc/grpc-js", () => { + const original = jest.requireActual("@grpc/grpc-js"); + return { + ...original, + credentials: { + createInsecure: jest.fn(), + }, + loadPackageDefinition: jest.fn(() => ({ + webui: { + tokenization: { + CentralSystem: jest.fn(() => mockClient), + }, + }, + })), + }; +}); + +describe("ConnectionManager", () => { + let conn: ConnectionManager; + + beforeEach(() => { + jest.clearAllMocks(); + conn = new ConnectionManager("dummy.proto", "localhost:12345"); + }); + + test("should initialize client with correct address", () => { + expect(conn).toBeDefined(); + expect(grpc.loadPackageDefinition).toHaveBeenCalled(); + }); + + test("connectToCentralSystem() should create stream and log message", () => { + conn.connectToCentralSystem(); + + expect(mockClient.ClientStream).toHaveBeenCalled(); + expect(mockStream.on).toHaveBeenCalledWith("data", expect.any(Function)); + expect(mockStream.on).toHaveBeenCalledWith("end", expect.any(Function)); + expect(mockStream.on).toHaveBeenCalledWith("error", expect.any(Function)); + }); + + test("disconnect() should end stream and reset reconnectAttempts", () => { + conn.connectToCentralSystem(); + conn.disconnect(); + + expect(mockStream.end).toHaveBeenCalled(); + }); + + test("scheduleReconnect() should call connect after delay", () => { + jest.useFakeTimers(); + const spy = jest.spyOn(conn as any, "connect"); + + (conn as any).scheduleReconnect(); + + jest.advanceTimersByTime(1000 * 2); // pierwszy delay = 2^1 * 1000 + expect(spy).toHaveBeenCalled(); + jest.useRealTimers(); + }); + + test("should reconnect on stream 'end'", () => { + conn.connectToCentralSystem(); + const onEnd = mockStream.on.mock.calls.find( + ([event]) => event === "end" + )[1]; + + const reconnectSpy = jest.spyOn(conn as any, "scheduleReconnect"); + onEnd(); + + expect(reconnectSpy).toHaveBeenCalled(); + }); + + test("should reconnect on stream 'error'", () => { + conn.connectToCentralSystem(); + const onError = mockStream.on.mock.calls.find( + ([event]) => event === "error" + )[1]; + + const reconnectSpy = jest.spyOn(conn as any, "scheduleReconnect"); + onError(new Error("Stream failed")); + + expect(reconnectSpy).toHaveBeenCalled(); + }); +}); From d0f4daddc9f4d60f0c326038c83d5fe23c34d705 Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Thu, 10 Jul 2025 19:32:18 +0200 Subject: [PATCH 31/81] feat: muldularize connection management. Add project building. --- Tokenization/backend/wrapper/.gitignore | 2 + .../backend/wrapper/central/CentralSystem.ts | 3 + .../wrapper/client/Connection/Connection.ts | 13 + .../ConnectionManager/CentralConnection.ts | 91 + .../ConnectionManager/ConnectionManager.ts | 130 +- .../EventManagement/EventDispatcher.ts | 37 + .../backend/wrapper/client/gRPCWrapper.ts | 4 +- .../backend/wrapper/models/events.model.ts | 23 + .../backend/wrapper/package-lock.json | 4399 ++--------------- Tokenization/backend/wrapper/package.json | 10 +- Tokenization/backend/wrapper/tsconfig.json | 15 +- .../backend/wrapper/utils/types/webui.d.ts | 10 + 12 files changed, 619 insertions(+), 4118 deletions(-) create mode 100644 Tokenization/backend/wrapper/.gitignore create mode 100644 Tokenization/backend/wrapper/client/ConnectionManager/CentralConnection.ts create mode 100644 Tokenization/backend/wrapper/client/ConnectionManager/EventManagement/EventDispatcher.ts create mode 100644 Tokenization/backend/wrapper/models/events.model.ts create mode 100644 Tokenization/backend/wrapper/utils/types/webui.d.ts diff --git a/Tokenization/backend/wrapper/.gitignore b/Tokenization/backend/wrapper/.gitignore new file mode 100644 index 000000000..763301fc0 --- /dev/null +++ b/Tokenization/backend/wrapper/.gitignore @@ -0,0 +1,2 @@ +dist/ +node_modules/ \ No newline at end of file diff --git a/Tokenization/backend/wrapper/central/CentralSystem.ts b/Tokenization/backend/wrapper/central/CentralSystem.ts index 350984751..a0a7a7858 100644 --- a/Tokenization/backend/wrapper/central/CentralSystem.ts +++ b/Tokenization/backend/wrapper/central/CentralSystem.ts @@ -2,6 +2,7 @@ import * as grpc from "@grpc/grpc-js"; import * as protoLoader from "@grpc/proto-loader"; import path from "path"; import { LogManager } from "@aliceo2/web-ui"; +import { fileURLToPath } from "url"; /** * @description Central System gRPC wrapper that manages client connections and handles gRPC streams with them. @@ -94,6 +95,8 @@ export class CentralSystemWrapper { } // Instantiate the CentralSystemWrapper on port 50051, but don't start automatically +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); const PROTO_PATH = path.join(__dirname, "../proto/wrapper.proto"); const centralSystem = new CentralSystemWrapper(PROTO_PATH, 50051); // Start listening explicitly diff --git a/Tokenization/backend/wrapper/client/Connection/Connection.ts b/Tokenization/backend/wrapper/client/Connection/Connection.ts index 00ec4f381..b45dd354e 100644 --- a/Tokenization/backend/wrapper/client/Connection/Connection.ts +++ b/Tokenization/backend/wrapper/client/Connection/Connection.ts @@ -1,3 +1,16 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ /** * @description This class represents a connection to a target client and manages sending messages to it. */ diff --git a/Tokenization/backend/wrapper/client/ConnectionManager/CentralConnection.ts b/Tokenization/backend/wrapper/client/ConnectionManager/CentralConnection.ts new file mode 100644 index 000000000..3a06c33fe --- /dev/null +++ b/Tokenization/backend/wrapper/client/ConnectionManager/CentralConnection.ts @@ -0,0 +1,91 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ +import * as grpc from "@grpc/grpc-js"; +import { LogManager } from "@aliceo2/web-ui"; +import type { MessageHandler } from "../../models/events.model"; + +/** + * @description This class manages the duplex stream with the CentralSystem gRPC service. + * It is responsible for connecting, reconnecting with backoff, and delegating received messages. + */ +export class CentralConnection { + private logger = LogManager.getLogger("CentralConnection"); + private stream?: grpc.ClientDuplexStream; + private reconnectAttempts = 0; + + constructor(private client: any, private handler: MessageHandler) {} + + /** + * @description Initializes the duplex stream and sets up event handlers. + */ + connect() { + if (this.stream) return; + + this.stream = this.client.ClientStream(); + + this.stream!.on("data", (payload) => { + this.handler.handle(payload); + }); + + this.stream!.on("end", () => { + this.logger.infoMessage(`Stream ended, attempting to reconnect...`); + this.stream = undefined; + this.scheduleReconnect(); + }); + + this.stream!.on("error", (err: any) => { + this.logger.infoMessage( + "Stream error:", + err, + " attempting to reconnect..." + ); + this.stream = undefined; + this.scheduleReconnect(); + }); + } + + /** + * @description Schedules a reconnect with exponential backoff. + */ + private scheduleReconnect() { + this.reconnectAttempts++; + const delay = Math.min(1000 * 2 ** this.reconnectAttempts, 30000); + setTimeout(() => { + this.logger.infoMessage( + `Reconnecting (attempt ${this.reconnectAttempts})...` + ); + this.connect(); + }, delay); + } + + /** + * @description Starts the connection to the central system. + */ + start() { + this.connect(); + this.logger.infoMessage(`Connected to CentralSystem`); + } + + /** + * @description Disconnects from the gRPC stream and resets attempts. + */ + disconnect() { + if (this.stream) { + this.stream.end(); + this.stream = undefined; + } + this.reconnectAttempts = 0; + this.logger.infoMessage(`Disconnected from CentralSystem`); + } +} diff --git a/Tokenization/backend/wrapper/client/ConnectionManager/ConnectionManager.ts b/Tokenization/backend/wrapper/client/ConnectionManager/ConnectionManager.ts index 50a2a9b67..7d5bb415c 100644 --- a/Tokenization/backend/wrapper/client/ConnectionManager/ConnectionManager.ts +++ b/Tokenization/backend/wrapper/client/ConnectionManager/ConnectionManager.ts @@ -1,7 +1,22 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ import * as grpc from "@grpc/grpc-js"; import * as protoLoader from "@grpc/proto-loader"; +import { CentralConnection } from "./CentralConnection"; +import { EventDispatcher } from "../ConnectionManager/EventManagement/EventDispatcher"; +import { Connection } from "../Connection/Connection"; import { LogManager } from "@aliceo2/web-ui"; -import { Connection } from "../Connection/Connection.ts"; /** * @description Manages all the connection between clients and central system. @@ -11,37 +26,19 @@ import { Connection } from "../Connection/Connection.ts"; * * This class is responsible for: * - Initializing the gRPC client using the provided proto definition and address. - * - Managing a duplex stream (`stream`) for bidirectional communication. - * - Handling automatic reconnection with exponential backoff on stream errors or disconnects. - * - Providing methods to start (`connectToCentralSystem`) and stop (`disconnect`) the connection with central system. + * - Delegating stream handling to CentralConnection. + * - Managing sending/receiving connections to other clients. * * @remarks - * - `client`: The gRPC client instance for communicating with the central system. - * - `stream`: The active duplex stream for sending and receiving messages (optional). - * - `address`: The address of the central gRPC server. - * - `reconnectAttempts`: The number of consecutive reconnection attempts made after a disconnect or error. + * - `centralConnection`: Handles the duplex stream with the central gRPC server. + * - `sendingConnections`: Map of active outbound connections. + * - `receivingConnections`: Map of active inbound connections. */ export class ConnectionManager { - // utilities private logger = LogManager.getLogger("ConnectionManager"); - - // class properties - private client: any; - private stream?: grpc.ClientDuplexStream; - private readonly address: string; - private reconnectAttempts = 0; - - // Map to store sending connections by target address - private sendingConnections: Map = new Map(); - - // Map to store receiving connections by target address - private receivingConnections: Map = new Map(); - - // Map to store sending connections by target address - private sendingConnections: Map = new Map(); - - // Map to store receiving connections by target address - private receivingConnections: Map = new Map(); + private centralConnection: CentralConnection; + private sendingConnections = new Map(); + private receivingConnections = new Map(); /** * @description Initializes a new instance of the ConnectionManager class. @@ -52,8 +49,6 @@ export class ConnectionManager { * @param centralAddress - The address of the central gRPC server (default: "localhost:50051"). */ constructor(protoPath: string, centralAddress: string = "localhost:50051") { - this.address = centralAddress; - const packageDef = protoLoader.loadSync(protoPath, { keepCase: true, longs: String, @@ -65,86 +60,29 @@ export class ConnectionManager { const proto = grpc.loadPackageDefinition(packageDef) as any; const wrapper = proto.webui.tokenization; - // Create gRPC client - this.client = new wrapper.CentralSystem( - this.address, + const client = new wrapper.CentralSystem( + centralAddress, grpc.credentials.createInsecure() ); + const dispatcher = new EventDispatcher(); + this.centralConnection = new CentralConnection(client, dispatcher); + this.sendingConnections.set("a", new Connection("1", "a")); this.sendingConnections.set("b", new Connection("2", "b")); } - /** - * @description Initializes the duplex stream and sets up handlers. - */ - private connect() { - if (this.stream) return; - this.stream = this.client.ClientStream(); - - if (this.stream) { - this.stream.on("data", (payload) => { - switch (payload.event) { - // Central system replacing a new token for existing connection - case "EMPTY_EVENT": - console.log("Empty event: ", payload?.data); - break; - } - }); - - this.stream.on("end", () => { - this.logger.infoMessage(`Stream ended, attempting to reconnect...`); - this.stream = undefined; - this.scheduleReconnect(); - }); - - this.stream.on("error", (err: any) => { - this.logger.infoMessage( - `Stream error:`, - err, - " attempting to reconnect..." - ); - this.stream = undefined; - this.scheduleReconnect(); - }); - } - } - - /** - * @description Schedules a reconnect with exponential backoff. - */ - private scheduleReconnect() { - this.reconnectAttempts++; - const delay = Math.min(1000 * 2 ** this.reconnectAttempts, 30000); - setTimeout(() => { - this.logger.infoMessage( - `Reconnecting (attempt ${this.reconnectAttempts})...` - ); - this.connect(); - }, delay); - } - /** * @description Starts the connection to the central system. */ - public connectToCentralSystem() { - if (!this.stream) { - this.connect(); - this.logger.infoMessage( - `Connected to CentralSystem service at ${this.address}` - ); - } + connectToCentralSystem() { + this.centralConnection.start(); } /** - * @description Disconnects from the gRPC stream and resets attempts. + * @description Disconnects from the central system. */ - public disconnect() { - if (this.stream) { - this.stream.end(); - this.stream = undefined; - } - this.reconnectAttempts = 0; - this.logger.infoMessage(`Disconnected from CentralSystem service`); + disconnectFromCentralSystem() { + this.centralConnection.disconnect(); } } diff --git a/Tokenization/backend/wrapper/client/ConnectionManager/EventManagement/EventDispatcher.ts b/Tokenization/backend/wrapper/client/ConnectionManager/EventManagement/EventDispatcher.ts new file mode 100644 index 000000000..1f9d17be0 --- /dev/null +++ b/Tokenization/backend/wrapper/client/ConnectionManager/EventManagement/EventDispatcher.ts @@ -0,0 +1,37 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ +import type { MessageHandler } from "../../../models/events.model"; +import { LogManager } from "@aliceo2/web-ui"; + +/** + * @description Dispatches gRPC stream events received from CentralSystem. + */ +export class EventDispatcher implements MessageHandler { + private logger = LogManager.getLogger("ConnectionManager"); + + /** + * @description Handles incoming events from the gRPC stream. + * + * @param event - The event object received from the stream. + */ + handle(event: any): void { + switch (event.event) { + case "EMPTY_EVENT": + // handle empty event + break; + default: + this.logger.infoMessage("Unknown event type:", event.event); + } + } +} diff --git a/Tokenization/backend/wrapper/client/gRPCWrapper.ts b/Tokenization/backend/wrapper/client/gRPCWrapper.ts index 486f30e6c..66cfb6c48 100644 --- a/Tokenization/backend/wrapper/client/gRPCWrapper.ts +++ b/Tokenization/backend/wrapper/client/gRPCWrapper.ts @@ -1,5 +1,5 @@ import path from "path"; -import { ConnectionManager } from "./ConnectionManager/ConnectionManager.ts"; +import { ConnectionManager } from "./ConnectionManager/ConnectionManager"; import { fileURLToPath } from "url"; /** @@ -37,6 +37,8 @@ export class gRPCWrapper { } } +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); const PROTO_PATH = path.join(__dirname, "../proto/wrapper.proto"); const grpc = new gRPCWrapper(PROTO_PATH, "localhost:50051"); grpc.connectToCentralSystem(); diff --git a/Tokenization/backend/wrapper/models/events.model.ts b/Tokenization/backend/wrapper/models/events.model.ts new file mode 100644 index 000000000..ff20b71ff --- /dev/null +++ b/Tokenization/backend/wrapper/models/events.model.ts @@ -0,0 +1,23 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +/** + * Interface representing a handler for processing events. + * + * @remarks + * The `handle` method receives an event object and performs the necessary processing. + */ +export interface MessageHandler { + handle(event: any): void; +} diff --git a/Tokenization/backend/wrapper/package-lock.json b/Tokenization/backend/wrapper/package-lock.json index ef28121ab..6c5036d55 100644 --- a/Tokenization/backend/wrapper/package-lock.json +++ b/Tokenization/backend/wrapper/package-lock.json @@ -7,4015 +7,6 @@ "": { "name": "grpc-wrapper", "version": "1.0.0", - "license": "ISC", - "dependencies": { - "ts-node": "^10.9.2", - "typescript": "^5.8.3" - }, - "devDependencies": { - "@types/jest": "^29.5.14", - "jest": "^29.7.0", - "ts-jest": "^29.3.4" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.27.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.5.tgz", - "integrity": "sha512-KiRAp/VoJaWkkte84TvUd9qjdbZAdiqyvMxrGl1N6vzFogKmaLgoM3L1kgtLicp2HP5fBJS8JrZKLVIZGVJAVg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.27.4", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.4.tgz", - "integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.27.3", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.27.3", - "@babel/helpers": "^7.27.4", - "@babel/parser": "^7.27.4", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.27.4", - "@babel/types": "^7.27.3", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/generator": { - "version": "7.27.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.5.tgz", - "integrity": "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.27.5", - "@babel/types": "^7.27.3", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.27.2", - "@babel/helper-validator-option": "^7.27.1", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", - "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.27.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.27.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz", - "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.27.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.27.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.5.tgz", - "integrity": "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.27.3" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-bigint": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", - "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.12.13" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-static-block": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", - "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", - "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", - "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", - "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", - "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.27.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.4.tgz", - "integrity": "sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.27.3", - "@babel/parser": "^7.27.4", - "@babel/template": "^7.27.2", - "@babel/types": "^7.27.3", - "debug": "^4.3.1", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.27.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.6.tgz", - "integrity": "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, - "node_modules/@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/console": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", - "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/core": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", - "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "^29.7.0", - "@jest/reporters": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "jest-changed-files": "^29.7.0", - "jest-config": "^29.7.0", - "jest-haste-map": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-resolve-dependencies": "^29.7.0", - "jest-runner": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "jest-watcher": "^29.7.0", - "micromatch": "^4.0.4", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/@jest/environment": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", - "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-mock": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/expect": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", - "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "expect": "^29.7.0", - "jest-snapshot": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/expect-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", - "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "jest-get-type": "^29.6.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/fake-timers": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", - "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "@sinonjs/fake-timers": "^10.0.2", - "@types/node": "*", - "jest-message-util": "^29.7.0", - "jest-mock": "^29.7.0", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/globals": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", - "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/expect": "^29.7.0", - "@jest/types": "^29.6.3", - "jest-mock": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/reporters": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", - "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@jridgewell/trace-mapping": "^0.3.18", - "@types/node": "*", - "chalk": "^4.0.0", - "collect-v8-coverage": "^1.0.0", - "exit": "^0.1.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^6.0.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.0", - "istanbul-reports": "^3.1.3", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "jest-worker": "^29.7.0", - "slash": "^3.0.0", - "string-length": "^4.0.1", - "strip-ansi": "^6.0.0", - "v8-to-istanbul": "^9.0.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.27.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/source-map": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", - "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.18", - "callsites": "^3.0.0", - "graceful-fs": "^4.2.9" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/test-result": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", - "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "collect-v8-coverage": "^1.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/test-sequencer": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", - "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/test-result": "^29.7.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/transform": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", - "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.11.6", - "@jest/types": "^29.6.3", - "@jridgewell/trace-mapping": "^0.3.18", - "babel-plugin-istanbul": "^6.1.1", - "chalk": "^4.0.0", - "convert-source-map": "^2.0.0", - "fast-json-stable-stringify": "^2.1.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-util": "^29.7.0", - "micromatch": "^4.0.4", - "pirates": "^4.0.4", - "slash": "^3.0.0", - "write-file-atomic": "^4.0.2" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/types": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", - "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sinonjs/commons": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", - "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "type-detect": "4.0.8" - } - }, - "node_modules/@sinonjs/fake-timers": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", - "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^3.0.0" - } - }, - "node_modules/@tsconfig/node10": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", - "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", - "license": "MIT" - }, - "node_modules/@tsconfig/node12": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "license": "MIT" - }, - "node_modules/@tsconfig/node14": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "license": "MIT" - }, - "node_modules/@tsconfig/node16": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", - "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "license": "MIT" - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", - "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.20.7" - } - }, - "node_modules/@types/graceful-fs": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", - "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", - "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/istanbul-lib-report": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", - "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/istanbul-lib-coverage": "*" - } - }, - "node_modules/@types/istanbul-reports": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", - "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/istanbul-lib-report": "*" - } - }, - "node_modules/@types/jest": { - "version": "29.5.14", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", - "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "expect": "^29.0.0", - "pretty-format": "^29.0.0" - } - }, - "node_modules/@types/node": { - "version": "24.0.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.0.tgz", - "integrity": "sha512-yZQa2zm87aRVcqDyH5+4Hv9KYgSdgwX1rFnGvpbzMaC7YAljmhBET93TPiTd3ObwTL+gSpIzPKg5BqVxdCvxKg==", - "license": "MIT", - "dependencies": { - "undici-types": "~7.8.0" - } - }, - "node_modules/@types/stack-utils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", - "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/yargs": { - "version": "17.0.33", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", - "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@types/yargs-parser": { - "version": "21.0.3", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", - "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-walk": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", - "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", - "license": "MIT", - "dependencies": { - "acorn": "^8.11.0" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.21.3" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "license": "MIT" - }, - "node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "dev": true, - "license": "MIT" - }, - "node_modules/babel-jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", - "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/transform": "^29.7.0", - "@types/babel__core": "^7.1.14", - "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^29.6.3", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.8.0" - } - }, - "node_modules/babel-plugin-istanbul": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", - "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-instrument": "^5.0.4", - "test-exclude": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", - "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/core": "^7.12.3", - "@babel/parser": "^7.14.7", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/babel-plugin-jest-hoist": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", - "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.3.3", - "@babel/types": "^7.3.3", - "@types/babel__core": "^7.1.14", - "@types/babel__traverse": "^7.0.6" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/babel-preset-current-node-syntax": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", - "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-bigint": "^7.8.3", - "@babel/plugin-syntax-class-properties": "^7.12.13", - "@babel/plugin-syntax-class-static-block": "^7.14.5", - "@babel/plugin-syntax-import-attributes": "^7.24.7", - "@babel/plugin-syntax-import-meta": "^7.10.4", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5", - "@babel/plugin-syntax-top-level-await": "^7.14.5" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/babel-preset-jest": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", - "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", - "dev": true, - "license": "MIT", - "dependencies": { - "babel-plugin-jest-hoist": "^29.6.3", - "babel-preset-current-node-syntax": "^1.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.25.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.0.tgz", - "integrity": "sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "caniuse-lite": "^1.0.30001718", - "electron-to-chromium": "^1.5.160", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.3" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/bs-logger": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", - "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-json-stable-stringify": "2.x" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/bser": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", - "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "node-int64": "^0.4.0" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001721", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001721.tgz", - "integrity": "sha512-cOuvmUVtKrtEaoKiO0rSc29jcjwMwX5tOHDy4MgVFEWiUXj4uBMJkwI8MDySkgXidpMiHUcviogAvFi4pA2hDQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/char-regex": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", - "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/ci-info": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cjs-module-lexer": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", - "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">= 1.0.0", - "node": ">= 0.12.0" - } - }, - "node_modules/collect-v8-coverage": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", - "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/create-jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", - "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "jest-config": "^29.7.0", - "jest-util": "^29.7.0", - "prompts": "^2.0.1" - }, - "bin": { - "create-jest": "bin/create-jest.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "license": "MIT" - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/dedent": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", - "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "babel-plugin-macros": "^3.1.0" - }, - "peerDependenciesMeta": { - "babel-plugin-macros": { - "optional": true - } - } - }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/detect-newline": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", - "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/ejs": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", - "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "jake": "^10.8.5" - }, - "bin": { - "ejs": "bin/cli.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.5.166", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.166.tgz", - "integrity": "sha512-QPWqHL0BglzPYyJJ1zSSmwFFL6MFXhbACOCcsCdUMCkzPdS9/OIBVxg516X/Ado2qwAq8k0nJJ7phQPCqiaFAw==", - "dev": true, - "license": "ISC" - }, - "node_modules/emittery": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", - "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sindresorhus/emittery?sponsor=1" - } - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/expect": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", - "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/expect-utils": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fb-watchman": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", - "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "bser": "2.1.1" - } - }, - "node_modules/filelist": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", - "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "minimatch": "^5.0.1" - } - }, - "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-package-type": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true, - "license": "MIT" - }, - "node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/import-local": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", - "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - }, - "bin": { - "import-local-fixture": "fixtures/cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-generator-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", - "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-instrument": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", - "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/core": "^7.23.9", - "@babel/parser": "^7.23.9", - "@istanbuljs/schema": "^0.1.3", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-instrument/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-source-maps": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", - "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-reports": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", - "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jake": { - "version": "10.9.2", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", - "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "async": "^3.2.3", - "chalk": "^4.0.2", - "filelist": "^1.0.4", - "minimatch": "^3.1.2" - }, - "bin": { - "jake": "bin/cli.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", - "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/core": "^29.7.0", - "@jest/types": "^29.6.3", - "import-local": "^3.0.2", - "jest-cli": "^29.7.0" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/jest-changed-files": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", - "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", - "dev": true, - "license": "MIT", - "dependencies": { - "execa": "^5.0.0", - "jest-util": "^29.7.0", - "p-limit": "^3.1.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-circus": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", - "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/expect": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "co": "^4.6.0", - "dedent": "^1.0.0", - "is-generator-fn": "^2.0.0", - "jest-each": "^29.7.0", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", - "p-limit": "^3.1.0", - "pretty-format": "^29.7.0", - "pure-rand": "^6.0.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-cli": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", - "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/core": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "create-jest": "^29.7.0", - "exit": "^0.1.2", - "import-local": "^3.0.2", - "jest-config": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "yargs": "^17.3.1" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/jest-config": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", - "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.11.6", - "@jest/test-sequencer": "^29.7.0", - "@jest/types": "^29.6.3", - "babel-jest": "^29.7.0", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "deepmerge": "^4.2.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-circus": "^29.7.0", - "jest-environment-node": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-runner": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "micromatch": "^4.0.4", - "parse-json": "^5.2.0", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@types/node": "*", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/jest-diff": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", - "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^29.6.3", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-docblock": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", - "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "detect-newline": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-each": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", - "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "jest-get-type": "^29.6.3", - "jest-util": "^29.7.0", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-environment-node": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", - "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-mock": "^29.7.0", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-get-type": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", - "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-haste-map": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", - "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "@types/graceful-fs": "^4.1.3", - "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "graceful-fs": "^4.2.9", - "jest-regex-util": "^29.6.3", - "jest-util": "^29.7.0", - "jest-worker": "^29.7.0", - "micromatch": "^4.0.4", - "walker": "^1.0.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "optionalDependencies": { - "fsevents": "^2.3.2" - } - }, - "node_modules/jest-leak-detector": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", - "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", - "dev": true, - "license": "MIT", - "dependencies": { - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-matcher-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", - "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.0.0", - "jest-diff": "^29.7.0", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-message-util": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", - "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^29.6.3", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-mock": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", - "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-pnp-resolver": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", - "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "peerDependencies": { - "jest-resolve": "*" - }, - "peerDependenciesMeta": { - "jest-resolve": { - "optional": true - } - } - }, - "node_modules/jest-regex-util": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", - "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-resolve": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", - "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-pnp-resolver": "^1.2.2", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "resolve": "^1.20.0", - "resolve.exports": "^2.0.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-resolve-dependencies": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", - "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", - "dev": true, - "license": "MIT", - "dependencies": { - "jest-regex-util": "^29.6.3", - "jest-snapshot": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-runner": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", - "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "^29.7.0", - "@jest/environment": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "emittery": "^0.13.1", - "graceful-fs": "^4.2.9", - "jest-docblock": "^29.7.0", - "jest-environment-node": "^29.7.0", - "jest-haste-map": "^29.7.0", - "jest-leak-detector": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-resolve": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-util": "^29.7.0", - "jest-watcher": "^29.7.0", - "jest-worker": "^29.7.0", - "p-limit": "^3.1.0", - "source-map-support": "0.5.13" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-runtime": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", - "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/fake-timers": "^29.7.0", - "@jest/globals": "^29.7.0", - "@jest/source-map": "^29.6.3", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "cjs-module-lexer": "^1.0.0", - "collect-v8-coverage": "^1.0.0", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-mock": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", - "slash": "^3.0.0", - "strip-bom": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-snapshot": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", - "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.11.6", - "@babel/generator": "^7.7.2", - "@babel/plugin-syntax-jsx": "^7.7.2", - "@babel/plugin-syntax-typescript": "^7.7.2", - "@babel/types": "^7.3.3", - "@jest/expect-utils": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "babel-preset-current-node-syntax": "^1.0.0", - "chalk": "^4.0.0", - "expect": "^29.7.0", - "graceful-fs": "^4.2.9", - "jest-diff": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "natural-compare": "^1.4.0", - "pretty-format": "^29.7.0", - "semver": "^7.5.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-snapshot/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/jest-util": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", - "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-validate": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", - "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "camelcase": "^6.2.0", - "chalk": "^4.0.0", - "jest-get-type": "^29.6.3", - "leven": "^3.1.0", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-validate/node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/jest-watcher": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", - "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "emittery": "^0.13.1", - "jest-util": "^29.7.0", - "string-length": "^4.0.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-worker": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", - "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "jest-util": "^29.7.0", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true, - "license": "MIT" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, - "license": "MIT" - }, - "node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/lodash.memoize": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", - "dev": true, - "license": "MIT" - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/make-dir/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "license": "ISC" - }, - "node_modules/makeerror": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", - "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "tmpl": "1.0.5" - } - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true, - "license": "MIT" - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", - "dev": true, - "license": "MIT" - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-locate/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pirates": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", - "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/prompts": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", - "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/pure-rand": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", - "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ], - "license": "MIT" - }, - "node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve.exports": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", - "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "dev": true, - "license": "MIT" - }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", - "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/stack-utils": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", - "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "escape-string-regexp": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/stack-utils/node_modules/escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-length": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", - "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "char-regex": "^1.0.2", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-bom": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "dev": true, - "license": "ISC", - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/tmpl": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", - "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/ts-jest": { - "version": "29.3.4", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.3.4.tgz", - "integrity": "sha512-Iqbrm8IXOmV+ggWHOTEbjwyCf2xZlUMv5npExksXohL+tk8va4Fjhb+X2+Rt9NBmgO7bJ8WpnMLOwih/DnMlFA==", - "dev": true, - "license": "MIT", - "dependencies": { - "bs-logger": "^0.2.6", - "ejs": "^3.1.10", - "fast-json-stable-stringify": "^2.1.0", - "jest-util": "^29.0.0", - "json5": "^2.2.3", - "lodash.memoize": "^4.1.2", - "make-error": "^1.3.6", - "semver": "^7.7.2", - "type-fest": "^4.41.0", - "yargs-parser": "^21.1.1" - }, - "bin": { - "ts-jest": "cli.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" - }, - "peerDependencies": { - "@babel/core": ">=7.0.0-beta.0 <8", - "@jest/transform": "^29.0.0", - "@jest/types": "^29.0.0", - "babel-jest": "^29.0.0", - "jest": "^29.0.0", - "typescript": ">=4.3 <6" - }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - }, - "@jest/transform": { - "optional": true - }, - "@jest/types": { - "optional": true - }, - "babel-jest": { - "optional": true - }, - "esbuild": { - "optional": true - } - } - }, - "node_modules/ts-jest/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/ts-jest/node_modules/type-fest": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", - "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ts-node": { - "version": "10.9.2", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", - "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", - "license": "MIT", - "dependencies": { - "@cspotcode/source-map-support": "^0.8.0", - "@tsconfig/node10": "^1.0.7", - "@tsconfig/node12": "^1.0.7", - "@tsconfig/node14": "^1.0.0", - "@tsconfig/node16": "^1.0.2", - "acorn": "^8.4.1", - "acorn-walk": "^8.1.1", - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.1", - "yn": "3.1.1" - }, - "bin": { - "ts-node": "dist/bin.js", - "ts-node-cwd": "dist/bin-cwd.js", - "ts-node-esm": "dist/bin-esm.js", - "ts-node-script": "dist/bin-script.js", - "ts-node-transpile-only": "dist/bin-transpile.js", - "ts-script": "dist/bin-script-deprecated.js" - }, - "peerDependencies": { - "@swc/core": ">=1.2.50", - "@swc/wasm": ">=1.2.50", - "@types/node": "*", - "typescript": ">=2.7" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "@swc/wasm": { - "optional": true - } - } - }, - "node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", - "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", - "license": "MIT" - }, - "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/v8-compile-cache-lib": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "license": "MIT" - }, - "node_modules/v8-to-istanbul": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", - "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", - "dev": true, - "license": "ISC", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.12", - "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^2.0.0" - }, - "engines": { - "node": ">=10.12.0" - } - }, - "node_modules/walker": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", - "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "makeerror": "1.0.12" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/write-file-atomic": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", - "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", - "dev": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.7" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "license": "ISC" - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - } -} - -{ - "name": "grpc-wrapper", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "grpc-wrapper", - "version": "1.0.0", - "license": "ISC", "dependencies": { "@grpc/grpc-js": "^1.13.4", "@grpc/proto-loader": "^0.7.15", @@ -4025,7 +16,8 @@ "devDependencies": { "@types/jest": "^29.5.14", "jest": "^29.7.0", - "ts-jest": "^29.3.4" + "ts-jest": "^29.3.4", + "tsc-alias": "^1.8.16" } }, "node_modules/@ampproject/remapping": { @@ -4961,6 +953,44 @@ "url": "https://opencollective.com/js-sdsl" } }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -5296,6 +1326,16 @@ "sprintf-js": "~1.0.2" } }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/async": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", @@ -5426,6 +1466,19 @@ "dev": true, "license": "MIT" }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -5581,6 +1634,31 @@ "node": ">=10" } }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, "node_modules/ci-info": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", @@ -5654,6 +1732,16 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || >=14" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -5783,6 +1871,19 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/ejs": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", @@ -5908,6 +2009,23 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -5915,6 +2033,16 @@ "dev": true, "license": "MIT" }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, "node_modules/fb-watchman": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", @@ -6059,6 +2187,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-tsconfig": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", + "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -6081,6 +2222,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -6091,6 +2245,27 @@ "node": ">=4" } }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -6138,6 +2313,16 @@ "node": ">=10.17.0" } }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/import-local": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", @@ -6194,6 +2379,19 @@ "dev": true, "license": "MIT" }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/is-core-module": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", @@ -6210,6 +2408,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -6229,6 +2437,19 @@ "node": ">=6" } }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -7133,6 +3354,16 @@ "dev": true, "license": "MIT" }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -7177,6 +3408,20 @@ "dev": true, "license": "MIT" }, + "node_modules/mylas": { + "version": "2.1.13", + "resolved": "https://registry.npmjs.org/mylas/-/mylas-2.1.13.tgz", + "integrity": "sha512-+MrqnJRtxdF+xngFfUUkIMQrUUL0KsxbADUkn23Z/4ibGg192Q+z+CQyiYwvWTsYjJygmMR8+w3ZDa98Zh6ESg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/raouldeheer" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -7358,6 +3603,16 @@ "dev": true, "license": "MIT" }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -7401,6 +3656,19 @@ "node": ">=8" } }, + "node_modules/plimit-lit": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/plimit-lit/-/plimit-lit-1.6.1.tgz", + "integrity": "sha512-B7+VDyb8Tl6oMJT9oSO2CW8XC/T4UcJGrwOVoNGwOQsQYhlpfajmrMj5xeejqaASq3V/EqThyOeATEOMuSEXiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "queue-lit": "^1.5.1" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/pretty-format": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", @@ -7484,6 +3752,37 @@ ], "license": "MIT" }, + "node_modules/queue-lit": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/queue-lit/-/queue-lit-1.5.2.tgz", + "integrity": "sha512-tLc36IOPeMAubu8BkW8YDBV+WyIgKlYU7zUNs0J5Vk9skSZ4JfGlPOqplP0aHdfv7HL0B2Pg6nwiq60Qc6M2Hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -7491,6 +3790,19 @@ "dev": true, "license": "MIT" }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -7544,6 +3856,16 @@ "node": ">=8" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/resolve.exports": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", @@ -7554,6 +3876,41 @@ "node": ">=10" } }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -7915,6 +4272,28 @@ } } }, + "node_modules/tsc-alias": { + "version": "1.8.16", + "resolved": "https://registry.npmjs.org/tsc-alias/-/tsc-alias-1.8.16.tgz", + "integrity": "sha512-QjCyu55NFyRSBAl6+MTFwplpFcnm2Pq01rR/uxfqJoLMm6X3O14KEGtaSDZpJYaE1bJBGDjD0eSuiIWPe2T58g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.3", + "commander": "^9.0.0", + "get-tsconfig": "^4.10.0", + "globby": "^11.0.4", + "mylas": "^2.1.9", + "normalize-path": "^3.0.0", + "plimit-lit": "^1.2.6" + }, + "bin": { + "tsc-alias": "dist/bin/index.js" + }, + "engines": { + "node": ">=16.20.2" + } + }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", diff --git a/Tokenization/backend/wrapper/package.json b/Tokenization/backend/wrapper/package.json index 3247319db..02ce46ed5 100644 --- a/Tokenization/backend/wrapper/package.json +++ b/Tokenization/backend/wrapper/package.json @@ -4,14 +4,18 @@ "type": "module", "scripts": { "test": "jest", - "start-central": "node --loader ts-node/esm ./central/CentralSystem.ts", - "start-client": "node --loader ts-node/esm ./client/gRPCWrapper.ts" + "build": "tsc && tsc-alias --resolve-full-paths && cp -r proto dist", + "start-client": "node dist/client/gRPCWrapper.js", + "start-central": "node dist/central/CentralSystem.js", + "dev-client": "ts-node-esm client/gRPCWrapper.ts", + "dev-central": "ts-node-esm central/CentralSystem.ts" }, "author": "ALICEO2", "devDependencies": { "@types/jest": "^29.5.14", "jest": "^29.7.0", - "ts-jest": "^29.3.4" + "ts-jest": "^29.3.4", + "tsc-alias": "^1.8.16" }, "dependencies": { "@grpc/grpc-js": "^1.13.4", diff --git a/Tokenization/backend/wrapper/tsconfig.json b/Tokenization/backend/wrapper/tsconfig.json index 32d70e3e9..d2e69b3a7 100644 --- a/Tokenization/backend/wrapper/tsconfig.json +++ b/Tokenization/backend/wrapper/tsconfig.json @@ -1,15 +1,14 @@ { "compilerOptions": { - "module": "ESNext", "target": "ES2022", + "module": "ESNext", "moduleResolution": "node", + "outDir": "./dist", + "rootDir": "./", "esModuleInterop": true, - "skipLibCheck": true, - "resolveJsonModule": true, - "allowImportingTsExtensions": true, - "noEmit": true + "forceConsistentCasingInFileNames": true, + "verbatimModuleSyntax": true, + "strict": true }, - "ts-node": { - "esm": true - } + "include": ["client", "central", "utils", "types"] } diff --git a/Tokenization/backend/wrapper/utils/types/webui.d.ts b/Tokenization/backend/wrapper/utils/types/webui.d.ts new file mode 100644 index 000000000..1f2aeee51 --- /dev/null +++ b/Tokenization/backend/wrapper/utils/types/webui.d.ts @@ -0,0 +1,10 @@ +declare module "@aliceo2/web-ui" { + export const LogManager: { + getLogger: (name: string) => { + infoMessage: (...args: any[]) => void; + errorMessage: (...args: any[]) => void; + warnMessage: (...args: any[]) => void; + debugMessage: (...args: any[]) => void; + }; + }; +} From b4a0bf9fecfffdbf06752a306fc93bb6713267d7 Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Thu, 10 Jul 2025 20:00:19 +0200 Subject: [PATCH 32/81] fix: fix connection and remove exponential backoff for simplified version --- .../client/ConnectionManager/CentralConnection.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/Tokenization/backend/wrapper/client/ConnectionManager/CentralConnection.ts b/Tokenization/backend/wrapper/client/ConnectionManager/CentralConnection.ts index 3a06c33fe..fee5a82b5 100644 --- a/Tokenization/backend/wrapper/client/ConnectionManager/CentralConnection.ts +++ b/Tokenization/backend/wrapper/client/ConnectionManager/CentralConnection.ts @@ -22,7 +22,6 @@ import type { MessageHandler } from "../../models/events.model"; export class CentralConnection { private logger = LogManager.getLogger("CentralConnection"); private stream?: grpc.ClientDuplexStream; - private reconnectAttempts = 0; constructor(private client: any, private handler: MessageHandler) {} @@ -59,14 +58,10 @@ export class CentralConnection { * @description Schedules a reconnect with exponential backoff. */ private scheduleReconnect() { - this.reconnectAttempts++; - const delay = Math.min(1000 * 2 ** this.reconnectAttempts, 30000); setTimeout(() => { - this.logger.infoMessage( - `Reconnecting (attempt ${this.reconnectAttempts})...` - ); + this.logger.infoMessage(`Trying to reconnect...`); this.connect(); - }, delay); + }, 2000); } /** @@ -85,7 +80,6 @@ export class CentralConnection { this.stream.end(); this.stream = undefined; } - this.reconnectAttempts = 0; this.logger.infoMessage(`Disconnected from CentralSystem`); } } From 3b499234a1572cb7316ca93979aa7a8295829949 Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki <42175519+OmegaCreations@users.noreply.github.com> Date: Sun, 13 Jul 2025 14:09:52 +0200 Subject: [PATCH 33/81] Fix: Unused function import Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- Tokenization/backend/wrapper/client/gRPCWrapper.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/Tokenization/backend/wrapper/client/gRPCWrapper.ts b/Tokenization/backend/wrapper/client/gRPCWrapper.ts index 486f30e6c..d77b8fa33 100644 --- a/Tokenization/backend/wrapper/client/gRPCWrapper.ts +++ b/Tokenization/backend/wrapper/client/gRPCWrapper.ts @@ -1,6 +1,5 @@ import path from "path"; import { ConnectionManager } from "./ConnectionManager/ConnectionManager.ts"; -import { fileURLToPath } from "url"; /** * @description Wrapper class for managing secure gRPC wrapper. From 8d9c89a31ef973eb8801f3cb2f62f843afc1ea86 Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Sun, 20 Jul 2025 15:06:04 +0200 Subject: [PATCH 34/81] fix: test setup fixes --- Tokenization/backend/wrapper/jest.config.js | 13 ------------ Tokenization/backend/wrapper/jest.config.mjs | 11 ++++++++++ .../backend/wrapper/package-lock.json | 21 +++++++++++-------- Tokenization/backend/wrapper/package.json | 2 +- .../ConnectionManager.test.ts | 2 +- Tokenization/backend/wrapper/tsconfig.json | 8 +++---- .../backend/wrapper/tsconfig.test.json | 13 ++++++++++++ Tokenization/package-lock.json | 6 ++++++ 8 files changed, 48 insertions(+), 28 deletions(-) delete mode 100644 Tokenization/backend/wrapper/jest.config.js create mode 100644 Tokenization/backend/wrapper/jest.config.mjs create mode 100644 Tokenization/backend/wrapper/tsconfig.test.json create mode 100644 Tokenization/package-lock.json diff --git a/Tokenization/backend/wrapper/jest.config.js b/Tokenization/backend/wrapper/jest.config.js deleted file mode 100644 index 3a5675ad5..000000000 --- a/Tokenization/backend/wrapper/jest.config.js +++ /dev/null @@ -1,13 +0,0 @@ -/** @type {import('jest').Config} */ -const config = { - verbose: true, - transform: { - "^.+\\.ts$": ["ts-jest", { useESM: true }], - }, - extensionsToTreatAsEsm: [".ts"], - moduleNameMapper: { - "^(\\.{1,2}/.*)\\.js$": "$1", - }, -}; - -export default config; diff --git a/Tokenization/backend/wrapper/jest.config.mjs b/Tokenization/backend/wrapper/jest.config.mjs new file mode 100644 index 000000000..d74308377 --- /dev/null +++ b/Tokenization/backend/wrapper/jest.config.mjs @@ -0,0 +1,11 @@ +export default { + preset: "ts-jest/presets/default-esm", + transform: { + "^.+\\.ts$": ["ts-jest", { useESM: true, tsconfig: "tsconfig.test.json" }], + }, + extensionsToTreatAsEsm: [".ts"], + testEnvironment: "node", + moduleNameMapper: { + "^(\\.{1,2}/.*)\\.js$": "$1", + }, +}; diff --git a/Tokenization/backend/wrapper/package-lock.json b/Tokenization/backend/wrapper/package-lock.json index 6c5036d55..1a24c0038 100644 --- a/Tokenization/backend/wrapper/package-lock.json +++ b/Tokenization/backend/wrapper/package-lock.json @@ -16,7 +16,7 @@ "devDependencies": { "@types/jest": "^29.5.14", "jest": "^29.7.0", - "ts-jest": "^29.3.4", + "ts-jest": "^29.4.0", "tsc-alias": "^1.8.16" } }, @@ -4154,16 +4154,15 @@ } }, "node_modules/ts-jest": { - "version": "29.3.4", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.3.4.tgz", - "integrity": "sha512-Iqbrm8IXOmV+ggWHOTEbjwyCf2xZlUMv5npExksXohL+tk8va4Fjhb+X2+Rt9NBmgO7bJ8WpnMLOwih/DnMlFA==", + "version": "29.4.0", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.0.tgz", + "integrity": "sha512-d423TJMnJGu80/eSgfQ5w/R+0zFJvdtTxwtF9KzFFunOpSeD+79lHJQIiAhluJoyGRbvj9NZJsl9WjCUo0ND7Q==", "dev": true, "license": "MIT", "dependencies": { "bs-logger": "^0.2.6", "ejs": "^3.1.10", "fast-json-stable-stringify": "^2.1.0", - "jest-util": "^29.0.0", "json5": "^2.2.3", "lodash.memoize": "^4.1.2", "make-error": "^1.3.6", @@ -4179,10 +4178,11 @@ }, "peerDependencies": { "@babel/core": ">=7.0.0-beta.0 <8", - "@jest/transform": "^29.0.0", - "@jest/types": "^29.0.0", - "babel-jest": "^29.0.0", - "jest": "^29.0.0", + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", "typescript": ">=4.3 <6" }, "peerDependenciesMeta": { @@ -4200,6 +4200,9 @@ }, "esbuild": { "optional": true + }, + "jest-util": { + "optional": true } } }, diff --git a/Tokenization/backend/wrapper/package.json b/Tokenization/backend/wrapper/package.json index 02ce46ed5..060701af5 100644 --- a/Tokenization/backend/wrapper/package.json +++ b/Tokenization/backend/wrapper/package.json @@ -14,7 +14,7 @@ "devDependencies": { "@types/jest": "^29.5.14", "jest": "^29.7.0", - "ts-jest": "^29.3.4", + "ts-jest": "^29.4.0", "tsc-alias": "^1.8.16" }, "dependencies": { diff --git a/Tokenization/backend/wrapper/test/client/ConnectionManager/ConnectionManager.test.ts b/Tokenization/backend/wrapper/test/client/ConnectionManager/ConnectionManager.test.ts index 7f55df35e..de0a626a6 100644 --- a/Tokenization/backend/wrapper/test/client/ConnectionManager/ConnectionManager.test.ts +++ b/Tokenization/backend/wrapper/test/client/ConnectionManager/ConnectionManager.test.ts @@ -66,7 +66,7 @@ describe("ConnectionManager", () => { test("disconnect() should end stream and reset reconnectAttempts", () => { conn.connectToCentralSystem(); - conn.disconnect(); + conn.disconnectFromCentralSystem(); expect(mockStream.end).toHaveBeenCalled(); }); diff --git a/Tokenization/backend/wrapper/tsconfig.json b/Tokenization/backend/wrapper/tsconfig.json index d2e69b3a7..17a9fe200 100644 --- a/Tokenization/backend/wrapper/tsconfig.json +++ b/Tokenization/backend/wrapper/tsconfig.json @@ -1,14 +1,14 @@ { "compilerOptions": { - "target": "ES2022", + "target": "ES2020", "module": "ESNext", "moduleResolution": "node", - "outDir": "./dist", "rootDir": "./", + "outDir": "./dist", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, - "verbatimModuleSyntax": true, - "strict": true + "strict": true, + "skipLibCheck": true }, "include": ["client", "central", "utils", "types"] } diff --git a/Tokenization/backend/wrapper/tsconfig.test.json b/Tokenization/backend/wrapper/tsconfig.test.json new file mode 100644 index 000000000..32ee3a99a --- /dev/null +++ b/Tokenization/backend/wrapper/tsconfig.test.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "ESNext", + "target": "ES2022", + "moduleResolution": "node", + "verbatimModuleSyntax": true, + "allowImportingTsExtensions": false, + "esModuleInterop": true, + "types": ["jest"] + }, + "include": ["test", "client", "central", "utils", "types"] +} diff --git a/Tokenization/package-lock.json b/Tokenization/package-lock.json new file mode 100644 index 000000000..c7142dc66 --- /dev/null +++ b/Tokenization/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "Tokenization", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} From b6695a497e6fef472f2edf1c8605538375f59363 Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Mon, 28 Jul 2025 20:09:14 +0200 Subject: [PATCH 35/81] fix: fix tscofing for js builds and typescript files/tests runtime --- Tokenization/backend/wrapper/jest.config.mjs | 11 ---- Tokenization/backend/wrapper/jest.config.ts | 13 ++++ Tokenization/backend/wrapper/package.json | 8 +-- .../{ => src}/central/CentralSystem.ts | 9 +-- .../{ => src}/client/Connection/Connection.ts | 2 +- .../ConnectionManager/CentralConnection.ts | 0 .../ConnectionManager/ConnectionManager.ts | 2 +- .../EventManagement/EventDispatcher.ts | 5 +- .../wrapper/{ => src}/client/gRPCWrapper.ts | 3 - .../{ => src}/models/connection.model.ts | 0 .../wrapper/{ => src}/models/events.model.ts | 0 .../wrapper/{ => src}/models/message.model.ts | 0 .../wrapper/{ => src}/proto/wrapper.proto | 0 .../test/central/CentralSystem.test.ts | 0 .../ConnectionManager.test.ts | 59 +++++++++++-------- .../test/utils/serialization.utils.test.ts | 0 .../{ => src}/utils/serialization.utils.ts | 0 .../wrapper/{ => src}/utils/types/webui.d.ts | 0 .../backend/wrapper/tsconfig.build.json | 12 ++++ Tokenization/backend/wrapper/tsconfig.json | 22 ++++--- .../backend/wrapper/tsconfig.test.json | 13 ---- 21 files changed, 85 insertions(+), 74 deletions(-) delete mode 100644 Tokenization/backend/wrapper/jest.config.mjs create mode 100644 Tokenization/backend/wrapper/jest.config.ts rename Tokenization/backend/wrapper/{ => src}/central/CentralSystem.ts (91%) rename Tokenization/backend/wrapper/{ => src}/client/Connection/Connection.ts (95%) rename Tokenization/backend/wrapper/{ => src}/client/ConnectionManager/CentralConnection.ts (100%) rename Tokenization/backend/wrapper/{ => src}/client/ConnectionManager/ConnectionManager.ts (97%) rename Tokenization/backend/wrapper/{ => src}/client/ConnectionManager/EventManagement/EventDispatcher.ts (85%) rename Tokenization/backend/wrapper/{ => src}/client/gRPCWrapper.ts (91%) rename Tokenization/backend/wrapper/{ => src}/models/connection.model.ts (100%) rename Tokenization/backend/wrapper/{ => src}/models/events.model.ts (100%) rename Tokenization/backend/wrapper/{ => src}/models/message.model.ts (100%) rename Tokenization/backend/wrapper/{ => src}/proto/wrapper.proto (100%) rename Tokenization/backend/wrapper/{ => src}/test/central/CentralSystem.test.ts (100%) rename Tokenization/backend/wrapper/{ => src}/test/client/ConnectionManager/ConnectionManager.test.ts (66%) rename Tokenization/backend/wrapper/{ => src}/test/utils/serialization.utils.test.ts (100%) rename Tokenization/backend/wrapper/{ => src}/utils/serialization.utils.ts (100%) rename Tokenization/backend/wrapper/{ => src}/utils/types/webui.d.ts (100%) create mode 100644 Tokenization/backend/wrapper/tsconfig.build.json delete mode 100644 Tokenization/backend/wrapper/tsconfig.test.json diff --git a/Tokenization/backend/wrapper/jest.config.mjs b/Tokenization/backend/wrapper/jest.config.mjs deleted file mode 100644 index d74308377..000000000 --- a/Tokenization/backend/wrapper/jest.config.mjs +++ /dev/null @@ -1,11 +0,0 @@ -export default { - preset: "ts-jest/presets/default-esm", - transform: { - "^.+\\.ts$": ["ts-jest", { useESM: true, tsconfig: "tsconfig.test.json" }], - }, - extensionsToTreatAsEsm: [".ts"], - testEnvironment: "node", - moduleNameMapper: { - "^(\\.{1,2}/.*)\\.js$": "$1", - }, -}; diff --git a/Tokenization/backend/wrapper/jest.config.ts b/Tokenization/backend/wrapper/jest.config.ts new file mode 100644 index 000000000..9187fb239 --- /dev/null +++ b/Tokenization/backend/wrapper/jest.config.ts @@ -0,0 +1,13 @@ +import type { Config } from "jest"; + +const config: Config = { + preset: "ts-jest", + testEnvironment: "node", + testMatch: ["**/test/**/*.test.ts"], + moduleFileExtensions: ["ts", "js", "json"], + moduleNameMapper: { + "^@/(.*)$": "/src/$1", + }, +}; + +export default config; diff --git a/Tokenization/backend/wrapper/package.json b/Tokenization/backend/wrapper/package.json index 060701af5..04922d113 100644 --- a/Tokenization/backend/wrapper/package.json +++ b/Tokenization/backend/wrapper/package.json @@ -1,14 +1,12 @@ { "name": "grpc-wrapper", "version": "1.0.0", - "type": "module", + "type": "commonjs", "scripts": { "test": "jest", - "build": "tsc && tsc-alias --resolve-full-paths && cp -r proto dist", + "build": "tsc -p tsconfig.build.json && cp -r src/proto dist", "start-client": "node dist/client/gRPCWrapper.js", - "start-central": "node dist/central/CentralSystem.js", - "dev-client": "ts-node-esm client/gRPCWrapper.ts", - "dev-central": "ts-node-esm central/CentralSystem.ts" + "start-central": "node dist/central/CentralSystem.js" }, "author": "ALICEO2", "devDependencies": { diff --git a/Tokenization/backend/wrapper/central/CentralSystem.ts b/Tokenization/backend/wrapper/src/central/CentralSystem.ts similarity index 91% rename from Tokenization/backend/wrapper/central/CentralSystem.ts rename to Tokenization/backend/wrapper/src/central/CentralSystem.ts index 2a73f08d5..62b61b6ef 100644 --- a/Tokenization/backend/wrapper/central/CentralSystem.ts +++ b/Tokenization/backend/wrapper/src/central/CentralSystem.ts @@ -2,11 +2,6 @@ import * as grpc from "@grpc/grpc-js"; import * as protoLoader from "@grpc/proto-loader"; import path from "path"; import { LogManager } from "@aliceo2/web-ui"; -import { fileURLToPath } from "url"; -import { - DuplexMessageEvent, - DuplexMessageModel, -} from "../models/message.model.ts"; /** * @description Central System gRPC wrapper that manages client connections and handles gRPC streams with them. @@ -61,7 +56,7 @@ export class CentralSystemWrapper { // Listen for data events from the client call.on("data", (payload: any) => { - console.log(`Received from ${clientAddress}:`, payload); + console.log(`Received from ${call.getPeer()}:`, payload); }); // Handle stream end event @@ -99,8 +94,6 @@ export class CentralSystemWrapper { } // Instantiate the CentralSystemWrapper on port 50051, but don't start automatically -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); const PROTO_PATH = path.join(__dirname, "../proto/wrapper.proto"); const centralSystem = new CentralSystemWrapper(PROTO_PATH, 50051); // Start listening explicitly diff --git a/Tokenization/backend/wrapper/client/Connection/Connection.ts b/Tokenization/backend/wrapper/src/client/Connection/Connection.ts similarity index 95% rename from Tokenization/backend/wrapper/client/Connection/Connection.ts rename to Tokenization/backend/wrapper/src/client/Connection/Connection.ts index 80bf12f8c..c1eebcaec 100644 --- a/Tokenization/backend/wrapper/client/Connection/Connection.ts +++ b/Tokenization/backend/wrapper/src/client/Connection/Connection.ts @@ -1,4 +1,4 @@ -import { ConnectionStatus } from "../../models/connection.model.ts"; +import { ConnectionStatus } from "../../models/connection.model"; /** * @license diff --git a/Tokenization/backend/wrapper/client/ConnectionManager/CentralConnection.ts b/Tokenization/backend/wrapper/src/client/ConnectionManager/CentralConnection.ts similarity index 100% rename from Tokenization/backend/wrapper/client/ConnectionManager/CentralConnection.ts rename to Tokenization/backend/wrapper/src/client/ConnectionManager/CentralConnection.ts diff --git a/Tokenization/backend/wrapper/client/ConnectionManager/ConnectionManager.ts b/Tokenization/backend/wrapper/src/client/ConnectionManager/ConnectionManager.ts similarity index 97% rename from Tokenization/backend/wrapper/client/ConnectionManager/ConnectionManager.ts rename to Tokenization/backend/wrapper/src/client/ConnectionManager/ConnectionManager.ts index 7d5bb415c..3089a549c 100644 --- a/Tokenization/backend/wrapper/client/ConnectionManager/ConnectionManager.ts +++ b/Tokenization/backend/wrapper/src/client/ConnectionManager/ConnectionManager.ts @@ -14,7 +14,7 @@ import * as grpc from "@grpc/grpc-js"; import * as protoLoader from "@grpc/proto-loader"; import { CentralConnection } from "./CentralConnection"; -import { EventDispatcher } from "../ConnectionManager/EventManagement/EventDispatcher"; +import { EventDispatcher } from "./EventManagement/EventDispatcher"; import { Connection } from "../Connection/Connection"; import { LogManager } from "@aliceo2/web-ui"; diff --git a/Tokenization/backend/wrapper/client/ConnectionManager/EventManagement/EventDispatcher.ts b/Tokenization/backend/wrapper/src/client/ConnectionManager/EventManagement/EventDispatcher.ts similarity index 85% rename from Tokenization/backend/wrapper/client/ConnectionManager/EventManagement/EventDispatcher.ts rename to Tokenization/backend/wrapper/src/client/ConnectionManager/EventManagement/EventDispatcher.ts index 1f9d17be0..1b17efb7e 100644 --- a/Tokenization/backend/wrapper/client/ConnectionManager/EventManagement/EventDispatcher.ts +++ b/Tokenization/backend/wrapper/src/client/ConnectionManager/EventManagement/EventDispatcher.ts @@ -27,9 +27,12 @@ export class EventDispatcher implements MessageHandler { */ handle(event: any): void { switch (event.event) { - case "EMPTY_EVENT": + case DuplexMessageEvent.MESSAGE_EVENT_EMPTY: // handle empty event break; + case DuplexMessageEvent.MESSAGE_EVENT_REVOKE_TOKEN: + this.logger.infoMessage("Token revoked:", event.revokeToken); + // handle token revocation default: this.logger.infoMessage("Unknown event type:", event.event); } diff --git a/Tokenization/backend/wrapper/client/gRPCWrapper.ts b/Tokenization/backend/wrapper/src/client/gRPCWrapper.ts similarity index 91% rename from Tokenization/backend/wrapper/client/gRPCWrapper.ts rename to Tokenization/backend/wrapper/src/client/gRPCWrapper.ts index 66cfb6c48..bf514c088 100644 --- a/Tokenization/backend/wrapper/client/gRPCWrapper.ts +++ b/Tokenization/backend/wrapper/src/client/gRPCWrapper.ts @@ -1,6 +1,5 @@ import path from "path"; import { ConnectionManager } from "./ConnectionManager/ConnectionManager"; -import { fileURLToPath } from "url"; /** * @description Wrapper class for managing secure gRPC wrapper. @@ -37,8 +36,6 @@ export class gRPCWrapper { } } -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); const PROTO_PATH = path.join(__dirname, "../proto/wrapper.proto"); const grpc = new gRPCWrapper(PROTO_PATH, "localhost:50051"); grpc.connectToCentralSystem(); diff --git a/Tokenization/backend/wrapper/models/connection.model.ts b/Tokenization/backend/wrapper/src/models/connection.model.ts similarity index 100% rename from Tokenization/backend/wrapper/models/connection.model.ts rename to Tokenization/backend/wrapper/src/models/connection.model.ts diff --git a/Tokenization/backend/wrapper/models/events.model.ts b/Tokenization/backend/wrapper/src/models/events.model.ts similarity index 100% rename from Tokenization/backend/wrapper/models/events.model.ts rename to Tokenization/backend/wrapper/src/models/events.model.ts diff --git a/Tokenization/backend/wrapper/models/message.model.ts b/Tokenization/backend/wrapper/src/models/message.model.ts similarity index 100% rename from Tokenization/backend/wrapper/models/message.model.ts rename to Tokenization/backend/wrapper/src/models/message.model.ts diff --git a/Tokenization/backend/wrapper/proto/wrapper.proto b/Tokenization/backend/wrapper/src/proto/wrapper.proto similarity index 100% rename from Tokenization/backend/wrapper/proto/wrapper.proto rename to Tokenization/backend/wrapper/src/proto/wrapper.proto diff --git a/Tokenization/backend/wrapper/test/central/CentralSystem.test.ts b/Tokenization/backend/wrapper/src/test/central/CentralSystem.test.ts similarity index 100% rename from Tokenization/backend/wrapper/test/central/CentralSystem.test.ts rename to Tokenization/backend/wrapper/src/test/central/CentralSystem.test.ts diff --git a/Tokenization/backend/wrapper/test/client/ConnectionManager/ConnectionManager.test.ts b/Tokenization/backend/wrapper/src/test/client/ConnectionManager/ConnectionManager.test.ts similarity index 66% rename from Tokenization/backend/wrapper/test/client/ConnectionManager/ConnectionManager.test.ts rename to Tokenization/backend/wrapper/src/test/client/ConnectionManager/ConnectionManager.test.ts index de0a626a6..ea1d2498f 100644 --- a/Tokenization/backend/wrapper/test/client/ConnectionManager/ConnectionManager.test.ts +++ b/Tokenization/backend/wrapper/src/test/client/ConnectionManager/ConnectionManager.test.ts @@ -1,16 +1,31 @@ import * as grpc from "@grpc/grpc-js"; import { ConnectionManager } from "../../../client/ConnectionManager/ConnectionManager"; -// Mock of client and stream +// Mock duplex stream const mockStream = { on: jest.fn(), end: jest.fn(), }; +// Mock gRPC client const mockClient = { ClientStream: jest.fn(() => mockStream), }; +// Mock CentralSystem constructor +const CentralSystemMock = jest.fn(() => mockClient); + +// Mock EventDispatcher +jest.mock( + "../../../client/ConnectionManager/EventManagement/EventDispatcher", + () => ({ + EventDispatcher: jest.fn(() => ({ + handle: jest.fn(), + })), + }) +); + +// Mock logger jest.mock("@aliceo2/web-ui", () => ({ LogManager: { getLogger: () => ({ @@ -19,6 +34,7 @@ jest.mock("@aliceo2/web-ui", () => ({ }, })); +// Mock gRPC proto loader and client jest.mock("@grpc/proto-loader", () => ({ loadSync: jest.fn(() => { return {}; @@ -35,7 +51,7 @@ jest.mock("@grpc/grpc-js", () => { loadPackageDefinition: jest.fn(() => ({ webui: { tokenization: { - CentralSystem: jest.fn(() => mockClient), + CentralSystem: CentralSystemMock, }, }, })), @@ -53,9 +69,13 @@ describe("ConnectionManager", () => { test("should initialize client with correct address", () => { expect(conn).toBeDefined(); expect(grpc.loadPackageDefinition).toHaveBeenCalled(); + expect(CentralSystemMock).toHaveBeenCalledWith( + "localhost:12345", + undefined + ); }); - test("connectToCentralSystem() should create stream and log message", () => { + test("connectToCentralSystem() should set up stream listeners", () => { conn.connectToCentralSystem(); expect(mockClient.ClientStream).toHaveBeenCalled(); @@ -64,45 +84,38 @@ describe("ConnectionManager", () => { expect(mockStream.on).toHaveBeenCalledWith("error", expect.any(Function)); }); - test("disconnect() should end stream and reset reconnectAttempts", () => { + test("disconnectFromCentralSystem() should end stream", () => { conn.connectToCentralSystem(); conn.disconnectFromCentralSystem(); expect(mockStream.end).toHaveBeenCalled(); }); - test("scheduleReconnect() should call connect after delay", () => { - jest.useFakeTimers(); - const spy = jest.spyOn(conn as any, "connect"); - - (conn as any).scheduleReconnect(); - - jest.advanceTimersByTime(1000 * 2); // pierwszy delay = 2^1 * 1000 - expect(spy).toHaveBeenCalled(); - jest.useRealTimers(); - }); - test("should reconnect on stream 'end'", () => { + jest.useFakeTimers(); conn.connectToCentralSystem(); const onEnd = mockStream.on.mock.calls.find( ([event]) => event === "end" - )[1]; + )?.[1]; - const reconnectSpy = jest.spyOn(conn as any, "scheduleReconnect"); - onEnd(); + onEnd?.(); // simulate 'end' + jest.advanceTimersByTime(2000); - expect(reconnectSpy).toHaveBeenCalled(); + expect(mockClient.ClientStream).toHaveBeenCalledTimes(2); // initial + reconnect + jest.useRealTimers(); }); test("should reconnect on stream 'error'", () => { + jest.useFakeTimers(); conn.connectToCentralSystem(); const onError = mockStream.on.mock.calls.find( ([event]) => event === "error" - )[1]; + )?.[1]; - const reconnectSpy = jest.spyOn(conn as any, "scheduleReconnect"); - onError(new Error("Stream failed")); + onError?.(new Error("Simulated error")); + jest.advanceTimersByTime(2000); - expect(reconnectSpy).toHaveBeenCalled(); + expect(mockClient.ClientStream).toHaveBeenCalledTimes(2); + jest.useRealTimers(); }); }); diff --git a/Tokenization/backend/wrapper/test/utils/serialization.utils.test.ts b/Tokenization/backend/wrapper/src/test/utils/serialization.utils.test.ts similarity index 100% rename from Tokenization/backend/wrapper/test/utils/serialization.utils.test.ts rename to Tokenization/backend/wrapper/src/test/utils/serialization.utils.test.ts diff --git a/Tokenization/backend/wrapper/utils/serialization.utils.ts b/Tokenization/backend/wrapper/src/utils/serialization.utils.ts similarity index 100% rename from Tokenization/backend/wrapper/utils/serialization.utils.ts rename to Tokenization/backend/wrapper/src/utils/serialization.utils.ts diff --git a/Tokenization/backend/wrapper/utils/types/webui.d.ts b/Tokenization/backend/wrapper/src/utils/types/webui.d.ts similarity index 100% rename from Tokenization/backend/wrapper/utils/types/webui.d.ts rename to Tokenization/backend/wrapper/src/utils/types/webui.d.ts diff --git a/Tokenization/backend/wrapper/tsconfig.build.json b/Tokenization/backend/wrapper/tsconfig.build.json new file mode 100644 index 000000000..681ec5d54 --- /dev/null +++ b/Tokenization/backend/wrapper/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": false, + "outDir": "dist", + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*.ts"], + "exclude": ["tests", "node_modules"] +} diff --git a/Tokenization/backend/wrapper/tsconfig.json b/Tokenization/backend/wrapper/tsconfig.json index 17a9fe200..eda421baa 100644 --- a/Tokenization/backend/wrapper/tsconfig.json +++ b/Tokenization/backend/wrapper/tsconfig.json @@ -1,14 +1,20 @@ { "compilerOptions": { - "target": "ES2020", - "module": "ESNext", - "moduleResolution": "node", - "rootDir": "./", - "outDir": "./dist", + "target": "es2020", + "module": "CommonJS", + "moduleResolution": "Node", "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, "strict": true, - "skipLibCheck": true + "skipLibCheck": true, + "resolveJsonModule": true, + "baseUrl": ".", + "paths": { + "*": ["src/*"] + }, + "noEmit": true, + "allowImportingTsExtensions": false, + "forceConsistentCasingInFileNames": true }, - "include": ["client", "central", "utils", "types"] + "include": ["src/**/*.ts", "tests/**/*.ts"], + "exclude": ["dist", "node_modules"] } diff --git a/Tokenization/backend/wrapper/tsconfig.test.json b/Tokenization/backend/wrapper/tsconfig.test.json deleted file mode 100644 index 32ee3a99a..000000000 --- a/Tokenization/backend/wrapper/tsconfig.test.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "module": "ESNext", - "target": "ES2022", - "moduleResolution": "node", - "verbatimModuleSyntax": true, - "allowImportingTsExtensions": false, - "esModuleInterop": true, - "types": ["jest"] - }, - "include": ["test", "client", "central", "utils", "types"] -} From e79215c506d0095b4aa3dde08aa4b404553489b7 Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Mon, 4 Aug 2025 22:12:34 +0200 Subject: [PATCH 36/81] feat: refactor code for command design pattern --- .../wrapper/src/central/CentralSystem.ts | 13 +++++ .../client/Commands/revokeToken.command.ts | 19 ++++++++ .../client/Commands/revokeToken.handler.ts | 30 ++++++++++++ .../src/client/Connection/Connection.ts | 4 +- .../ConnectionManager/CentralConnection.ts | 9 ++-- .../ConnectionManager/ConnectionManager.ts | 39 +++++++++++++-- .../CentralCommandDispatcher.ts | 48 +++++++++++++++++++ .../EventManagement/EventDispatcher.ts | 40 ---------------- .../backend/wrapper/src/client/gRPCWrapper.ts | 21 ++++++++ .../{events.model.ts => commands.model.ts} | 12 ++++- .../wrapper/src/models/message.model.ts | 14 +++--- .../ConnectionManager.test.ts | 33 +++++++++++-- 12 files changed, 219 insertions(+), 63 deletions(-) create mode 100644 Tokenization/backend/wrapper/src/client/Commands/revokeToken.command.ts create mode 100644 Tokenization/backend/wrapper/src/client/Commands/revokeToken.handler.ts create mode 100644 Tokenization/backend/wrapper/src/client/ConnectionManager/EventManagement/CentralCommandDispatcher.ts delete mode 100644 Tokenization/backend/wrapper/src/client/ConnectionManager/EventManagement/EventDispatcher.ts rename Tokenization/backend/wrapper/src/models/{events.model.ts => commands.model.ts} (76%) diff --git a/Tokenization/backend/wrapper/src/central/CentralSystem.ts b/Tokenization/backend/wrapper/src/central/CentralSystem.ts index 62b61b6ef..035837940 100644 --- a/Tokenization/backend/wrapper/src/central/CentralSystem.ts +++ b/Tokenization/backend/wrapper/src/central/CentralSystem.ts @@ -1,3 +1,16 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ import * as grpc from "@grpc/grpc-js"; import * as protoLoader from "@grpc/proto-loader"; import path from "path"; diff --git a/Tokenization/backend/wrapper/src/client/Commands/revokeToken.command.ts b/Tokenization/backend/wrapper/src/client/Commands/revokeToken.command.ts new file mode 100644 index 000000000..dcdec0e34 --- /dev/null +++ b/Tokenization/backend/wrapper/src/client/Commands/revokeToken.command.ts @@ -0,0 +1,19 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ +import { Command } from "models/commands.model"; + +export class RevokeTokenCommand implements Command { + readonly type = DuplexMessageEvent.MESSAGE_EVENT_REVOKE_TOKEN; + constructor(public payload: DuplexMessageModel) {} +} diff --git a/Tokenization/backend/wrapper/src/client/Commands/revokeToken.handler.ts b/Tokenization/backend/wrapper/src/client/Commands/revokeToken.handler.ts new file mode 100644 index 000000000..8278c382d --- /dev/null +++ b/Tokenization/backend/wrapper/src/client/Commands/revokeToken.handler.ts @@ -0,0 +1,30 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ +import { CommandHandler } from "models/commands.model"; +import { RevokeTokenCommand } from "./revokeToken.command"; +import { ConnectionManager } from "client/ConnectionManager/ConnectionManager"; + +export class RevokeTokenHandler implements CommandHandler { + constructor(private manager: ConnectionManager) {} + + async handle(command: RevokeTokenCommand): Promise { + const { targetAddress } = command.payload.data.revokeToken || {}; + if (!targetAddress) { + throw new Error("Target address is required to revoke token."); + } + + const conn = this.manager.getConnectionByAddress(targetAddress); + conn?.handleRevokeToken(); + } +} diff --git a/Tokenization/backend/wrapper/src/client/Connection/Connection.ts b/Tokenization/backend/wrapper/src/client/Connection/Connection.ts index c1eebcaec..2cd5b572f 100644 --- a/Tokenization/backend/wrapper/src/client/Connection/Connection.ts +++ b/Tokenization/backend/wrapper/src/client/Connection/Connection.ts @@ -1,5 +1,3 @@ -import { ConnectionStatus } from "../../models/connection.model"; - /** * @license * Copyright 2019-2020 CERN and copyright holders of ALICE O2. @@ -13,6 +11,8 @@ import { ConnectionStatus } from "../../models/connection.model"; * granted to it by virtue of its status as an Intergovernmental Organization * or submit itself to any jurisdiction. */ +import { ConnectionStatus } from "../../models/connection.model"; + /** * @description This class represents a connection to a target client and manages sending messages to it. */ diff --git a/Tokenization/backend/wrapper/src/client/ConnectionManager/CentralConnection.ts b/Tokenization/backend/wrapper/src/client/ConnectionManager/CentralConnection.ts index fee5a82b5..70ed788d3 100644 --- a/Tokenization/backend/wrapper/src/client/ConnectionManager/CentralConnection.ts +++ b/Tokenization/backend/wrapper/src/client/ConnectionManager/CentralConnection.ts @@ -13,7 +13,7 @@ */ import * as grpc from "@grpc/grpc-js"; import { LogManager } from "@aliceo2/web-ui"; -import type { MessageHandler } from "../../models/events.model"; +import { CentralCommandDispatcher } from "./EventManagement/CentralCommandDispatcher"; /** * @description This class manages the duplex stream with the CentralSystem gRPC service. @@ -23,7 +23,10 @@ export class CentralConnection { private logger = LogManager.getLogger("CentralConnection"); private stream?: grpc.ClientDuplexStream; - constructor(private client: any, private handler: MessageHandler) {} + constructor( + private client: any, + private dispatcher: CentralCommandDispatcher + ) {} /** * @description Initializes the duplex stream and sets up event handlers. @@ -34,7 +37,7 @@ export class CentralConnection { this.stream = this.client.ClientStream(); this.stream!.on("data", (payload) => { - this.handler.handle(payload); + this.dispatcher.dispatch(payload); }); this.stream!.on("end", () => { diff --git a/Tokenization/backend/wrapper/src/client/ConnectionManager/ConnectionManager.ts b/Tokenization/backend/wrapper/src/client/ConnectionManager/ConnectionManager.ts index 3089a549c..57d751d8c 100644 --- a/Tokenization/backend/wrapper/src/client/ConnectionManager/ConnectionManager.ts +++ b/Tokenization/backend/wrapper/src/client/ConnectionManager/ConnectionManager.ts @@ -14,9 +14,11 @@ import * as grpc from "@grpc/grpc-js"; import * as protoLoader from "@grpc/proto-loader"; import { CentralConnection } from "./CentralConnection"; -import { EventDispatcher } from "./EventManagement/EventDispatcher"; +import { CentralCommandDispatcher } from "./EventManagement/CentralCommandDispatcher"; import { Connection } from "../Connection/Connection"; import { LogManager } from "@aliceo2/web-ui"; +import { Command, CommandHandler } from "models/commands.model"; +import { DuplexMessageEvent } from "models/message.model"; /** * @description Manages all the connection between clients and central system. @@ -36,6 +38,7 @@ import { LogManager } from "@aliceo2/web-ui"; */ export class ConnectionManager { private logger = LogManager.getLogger("ConnectionManager"); + private centralDispatcher: CentralCommandDispatcher; private centralConnection: CentralConnection; private sendingConnections = new Map(); private receivingConnections = new Map(); @@ -65,24 +68,50 @@ export class ConnectionManager { grpc.credentials.createInsecure() ); - const dispatcher = new EventDispatcher(); - this.centralConnection = new CentralConnection(client, dispatcher); + this.centralDispatcher = new CentralCommandDispatcher(); + + this.centralConnection = new CentralConnection( + client, + this.centralDispatcher + ); this.sendingConnections.set("a", new Connection("1", "a")); this.sendingConnections.set("b", new Connection("2", "b")); } + registerCommandHandlers( + commandHandlers: { + event: DuplexMessageEvent; + handler: CommandHandler; + }[] + ): void { + commandHandlers.forEach(({ event, handler }) => { + this.centralDispatcher.register(event, handler); + }); + } + /** * @description Starts the connection to the central system. */ - connectToCentralSystem() { + connectToCentralSystem(): void { this.centralConnection.start(); } /** * @description Disconnects from the central system. */ - disconnectFromCentralSystem() { + disconnectFromCentralSystem(): void { this.centralConnection.disconnect(); } + + /** + * @description Gets the connection instance by address. + * @returns{Connection} connection instance. + */ + getConnectionByAddress(address: string): Connection | undefined { + return ( + this.sendingConnections.get(address) || + this.receivingConnections.get(address) + ); + } } diff --git a/Tokenization/backend/wrapper/src/client/ConnectionManager/EventManagement/CentralCommandDispatcher.ts b/Tokenization/backend/wrapper/src/client/ConnectionManager/EventManagement/CentralCommandDispatcher.ts new file mode 100644 index 000000000..f6eb7ccf6 --- /dev/null +++ b/Tokenization/backend/wrapper/src/client/ConnectionManager/EventManagement/CentralCommandDispatcher.ts @@ -0,0 +1,48 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +import { LogManager } from "@aliceo2/web-ui"; +import { Command, CommandHandler } from "models/commands.model"; +import { DuplexMessageEvent } from "models/message.model"; + +export class CentralCommandDispatcher { + private handlers = new Map(); + private logger = LogManager.getLogger("CentralCommandDispatcher"); + + register( + type: DuplexMessageEvent, + handler: CommandHandler + ): void { + this.handlers.set(type, handler); + } + + async dispatch(command: Command): Promise { + const handler = this.handlers.get(command.type); + if (!handler) { + this.logger.warnMessage( + `No handler registered for command type: ${command.type}` + ); + return; + } + + try { + await handler.handle(command); + } catch (error) { + this.logger.errorMessage( + `Error handling command ${command.type}:`, + error + ); + } + } +} diff --git a/Tokenization/backend/wrapper/src/client/ConnectionManager/EventManagement/EventDispatcher.ts b/Tokenization/backend/wrapper/src/client/ConnectionManager/EventManagement/EventDispatcher.ts deleted file mode 100644 index 1b17efb7e..000000000 --- a/Tokenization/backend/wrapper/src/client/ConnectionManager/EventManagement/EventDispatcher.ts +++ /dev/null @@ -1,40 +0,0 @@ -/** - * @license - * Copyright 2019-2020 CERN and copyright holders of ALICE O2. - * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. - * All rights not expressly granted are reserved. - * - * This software is distributed under the terms of the GNU General Public - * License v3 (GPL Version 3), copied verbatim in the file "COPYING". - * - * In applying this license CERN does not waive the privileges and immunities - * granted to it by virtue of its status as an Intergovernmental Organization - * or submit itself to any jurisdiction. - */ -import type { MessageHandler } from "../../../models/events.model"; -import { LogManager } from "@aliceo2/web-ui"; - -/** - * @description Dispatches gRPC stream events received from CentralSystem. - */ -export class EventDispatcher implements MessageHandler { - private logger = LogManager.getLogger("ConnectionManager"); - - /** - * @description Handles incoming events from the gRPC stream. - * - * @param event - The event object received from the stream. - */ - handle(event: any): void { - switch (event.event) { - case DuplexMessageEvent.MESSAGE_EVENT_EMPTY: - // handle empty event - break; - case DuplexMessageEvent.MESSAGE_EVENT_REVOKE_TOKEN: - this.logger.infoMessage("Token revoked:", event.revokeToken); - // handle token revocation - default: - this.logger.infoMessage("Unknown event type:", event.event); - } - } -} diff --git a/Tokenization/backend/wrapper/src/client/gRPCWrapper.ts b/Tokenization/backend/wrapper/src/client/gRPCWrapper.ts index bf514c088..0452daa22 100644 --- a/Tokenization/backend/wrapper/src/client/gRPCWrapper.ts +++ b/Tokenization/backend/wrapper/src/client/gRPCWrapper.ts @@ -1,5 +1,20 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ import path from "path"; import { ConnectionManager } from "./ConnectionManager/ConnectionManager"; +import { RevokeTokenHandler } from "./Commands/revokeToken.handler"; +import { DuplexMessageEvent } from "models/message.model"; /** * @description Wrapper class for managing secure gRPC wrapper. @@ -26,6 +41,12 @@ export class gRPCWrapper { */ constructor(protoPath: string, centralAddress: string = "localhost:50051") { this.ConnectionManager = new ConnectionManager(protoPath, centralAddress); + this.ConnectionManager.registerCommandHandlers([ + { + event: DuplexMessageEvent.MESSAGE_EVENT_REVOKE_TOKEN, + handler: new RevokeTokenHandler(this.ConnectionManager), + }, + ]); } /** diff --git a/Tokenization/backend/wrapper/src/models/events.model.ts b/Tokenization/backend/wrapper/src/models/commands.model.ts similarity index 76% rename from Tokenization/backend/wrapper/src/models/events.model.ts rename to Tokenization/backend/wrapper/src/models/commands.model.ts index ff20b71ff..0a595b005 100644 --- a/Tokenization/backend/wrapper/src/models/events.model.ts +++ b/Tokenization/backend/wrapper/src/models/commands.model.ts @@ -12,12 +12,20 @@ * or submit itself to any jurisdiction. */ +import { DuplexMessageEvent } from "./message.model"; + /** * Interface representing a handler for processing events. * * @remarks * The `handle` method receives an event object and performs the necessary processing. */ -export interface MessageHandler { - handle(event: any): void; + +export interface Command { + type: DuplexMessageEvent; + payload: any; +} + +export interface CommandHandler { + handle(command: T): Promise; } diff --git a/Tokenization/backend/wrapper/src/models/message.model.ts b/Tokenization/backend/wrapper/src/models/message.model.ts index 3dc754218..0bfab7d8f 100644 --- a/Tokenization/backend/wrapper/src/models/message.model.ts +++ b/Tokenization/backend/wrapper/src/models/message.model.ts @@ -22,7 +22,7 @@ * @property MESSAGE_EVENT_NEW_TOKEN: Event for replacing with newly generated token. * @property MESSAGE_EVENT_REVOKE_TOKEN: Event for revoking an existing token. */ -enum DuplexMessageEvent { +export enum DuplexMessageEvent { MESSAGE_EVENT_EMPTY = 0, MESSAGE_EVENT_NEW_TOKEN = 1, MESSAGE_EVENT_REVOKE_TOKEN = 2, @@ -37,8 +37,8 @@ enum DuplexMessageEvent { * @property {string} token - The token to be replaced or revoked. * @property {string} targetAddress - The address of connection binded to this token. */ -interface TokenMessage { - token: string; +export interface TokenMessage { + token?: string; targetAddress: string; } @@ -48,8 +48,10 @@ interface TokenMessage { * @property {TokenMessage} data - The data associated with the event, it may be undefined for some events. * @example {event: DuplexMessageEvent.MESSAGE_EVENT_NEW_TOKEN, data: {token: '', targetAddress: ''}} */ -interface DuplexMessageModel { +export interface DuplexMessageModel { event: DuplexMessageEvent; - newToken?: TokenMessage; - revokeToken?: TokenMessage; + data: { + newToken?: TokenMessage; + revokeToken?: TokenMessage; + }; } diff --git a/Tokenization/backend/wrapper/src/test/client/ConnectionManager/ConnectionManager.test.ts b/Tokenization/backend/wrapper/src/test/client/ConnectionManager/ConnectionManager.test.ts index ea1d2498f..a2b34cde4 100644 --- a/Tokenization/backend/wrapper/src/test/client/ConnectionManager/ConnectionManager.test.ts +++ b/Tokenization/backend/wrapper/src/test/client/ConnectionManager/ConnectionManager.test.ts @@ -1,5 +1,6 @@ import * as grpc from "@grpc/grpc-js"; import { ConnectionManager } from "../../../client/ConnectionManager/ConnectionManager"; +import { DuplexMessageEvent } from "../../../models/message.model"; // Mock duplex stream const mockStream = { @@ -15,12 +16,13 @@ const mockClient = { // Mock CentralSystem constructor const CentralSystemMock = jest.fn(() => mockClient); -// Mock EventDispatcher +// Mock dispatcher +const mockDispatch = jest.fn(); jest.mock( - "../../../client/ConnectionManager/EventManagement/EventDispatcher", + "../../../client/ConnectionManager/EventManagement/CentralCommandDispatcher", () => ({ - EventDispatcher: jest.fn(() => ({ - handle: jest.fn(), + CentralCommandDispatcher: jest.fn(() => ({ + dispatch: mockDispatch, })), }) ); @@ -101,7 +103,7 @@ describe("ConnectionManager", () => { onEnd?.(); // simulate 'end' jest.advanceTimersByTime(2000); - expect(mockClient.ClientStream).toHaveBeenCalledTimes(2); // initial + reconnect + expect(mockClient.ClientStream).toHaveBeenCalledTimes(2); jest.useRealTimers(); }); @@ -118,4 +120,25 @@ describe("ConnectionManager", () => { expect(mockClient.ClientStream).toHaveBeenCalledTimes(2); jest.useRealTimers(); }); + + test("should dispatch event when 'data' is received", () => { + conn.connectToCentralSystem(); + const onData = mockStream.on.mock.calls.find( + ([event]) => event === "data" + )?.[1]; + + const mockMessage = { + event: DuplexMessageEvent.MESSAGE_EVENT_REVOKE_TOKEN, + data: { + revokeToken: { + token: "abc123", + targetAddress: "peer-123", + }, + }, + }; + + onData?.(mockMessage); + + expect(mockDispatch).toHaveBeenCalledWith(mockMessage); + }); }); From 90d713d4d31af35a581f6ab038a7c5a20dea0eb9 Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Mon, 4 Aug 2025 22:54:48 +0200 Subject: [PATCH 37/81] feat: unit tests for token revokation --- .../client/Commands/revokeToken.command.ts | 6 +- .../client/Commands/revokeToken.handler.ts | 6 +- .../wrapper/src/models/message.model.ts | 6 +- .../test/client/Commands/revokeToken.test.ts | 106 ++++++++++++++++++ .../{ConnectionManager.test.ts => index.ts} | 0 5 files changed, 116 insertions(+), 8 deletions(-) create mode 100644 Tokenization/backend/wrapper/src/test/client/Commands/revokeToken.test.ts rename Tokenization/backend/wrapper/src/test/client/ConnectionManager/{ConnectionManager.test.ts => index.ts} (100%) diff --git a/Tokenization/backend/wrapper/src/client/Commands/revokeToken.command.ts b/Tokenization/backend/wrapper/src/client/Commands/revokeToken.command.ts index dcdec0e34..88bea615b 100644 --- a/Tokenization/backend/wrapper/src/client/Commands/revokeToken.command.ts +++ b/Tokenization/backend/wrapper/src/client/Commands/revokeToken.command.ts @@ -11,7 +11,11 @@ * granted to it by virtue of its status as an Intergovernmental Organization * or submit itself to any jurisdiction. */ -import { Command } from "models/commands.model"; +import { Command } from "../../models/commands.model"; +import { + DuplexMessageEvent, + DuplexMessageModel, +} from "../../models/message.model"; export class RevokeTokenCommand implements Command { readonly type = DuplexMessageEvent.MESSAGE_EVENT_REVOKE_TOKEN; diff --git a/Tokenization/backend/wrapper/src/client/Commands/revokeToken.handler.ts b/Tokenization/backend/wrapper/src/client/Commands/revokeToken.handler.ts index 8278c382d..299c71c85 100644 --- a/Tokenization/backend/wrapper/src/client/Commands/revokeToken.handler.ts +++ b/Tokenization/backend/wrapper/src/client/Commands/revokeToken.handler.ts @@ -11,15 +11,15 @@ * granted to it by virtue of its status as an Intergovernmental Organization * or submit itself to any jurisdiction. */ -import { CommandHandler } from "models/commands.model"; +import { CommandHandler } from "../../models/commands.model"; import { RevokeTokenCommand } from "./revokeToken.command"; -import { ConnectionManager } from "client/ConnectionManager/ConnectionManager"; +import { ConnectionManager } from "../ConnectionManager/ConnectionManager"; export class RevokeTokenHandler implements CommandHandler { constructor(private manager: ConnectionManager) {} async handle(command: RevokeTokenCommand): Promise { - const { targetAddress } = command.payload.data.revokeToken || {}; + const { targetAddress } = command.payload.revokeToken || {}; if (!targetAddress) { throw new Error("Target address is required to revoke token."); } diff --git a/Tokenization/backend/wrapper/src/models/message.model.ts b/Tokenization/backend/wrapper/src/models/message.model.ts index 0bfab7d8f..07abc0b18 100644 --- a/Tokenization/backend/wrapper/src/models/message.model.ts +++ b/Tokenization/backend/wrapper/src/models/message.model.ts @@ -50,8 +50,6 @@ export interface TokenMessage { */ export interface DuplexMessageModel { event: DuplexMessageEvent; - data: { - newToken?: TokenMessage; - revokeToken?: TokenMessage; - }; + newToken?: TokenMessage; + revokeToken?: TokenMessage; } diff --git a/Tokenization/backend/wrapper/src/test/client/Commands/revokeToken.test.ts b/Tokenization/backend/wrapper/src/test/client/Commands/revokeToken.test.ts new file mode 100644 index 000000000..972565210 --- /dev/null +++ b/Tokenization/backend/wrapper/src/test/client/Commands/revokeToken.test.ts @@ -0,0 +1,106 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +import { RevokeTokenCommand } from "../../../client/Commands/revokeToken.command"; +import { RevokeTokenHandler } from "../../../client/Commands/revokeToken.handler"; +import { Connection } from "../../../client/Connection/Connection"; +import { ConnectionManager } from "../../../client/ConnectionManager/ConnectionManager"; +import { DuplexMessageEvent } from "../../../models/message.model"; +import { ConnectionStatus } from "../../../models/connection.model"; + +describe("RevokeToken", () => { + function createEventMessage(targetAddress: string) { + return { + event: DuplexMessageEvent.MESSAGE_EVENT_REVOKE_TOKEN, + revokeToken: { + token: "test-token", + targetAddress, + }, + }; + } + + let manager: ConnectionManager; + + beforeEach(() => { + manager = { + sendingConnections: new Map(), + receivingConnections: new Map(), + getConnectionByAddress: jest.fn(function (this: any, address: string) { + return ( + this.sendingConnections.get(address) || + this.receivingConnections.get(address) + ); + }), + } as unknown as ConnectionManager; + }); + + it("should revoke token when connection found in sendingConnections", async () => { + const targetAddress = "peer-123"; + const conn = new Connection("valid-token", targetAddress); + (manager as any).sendingConnections!.set(targetAddress, conn); + + const handler = new RevokeTokenHandler(manager); + const command = new RevokeTokenCommand(createEventMessage(targetAddress)); + + await handler.handle(command); + + expect(conn.getToken()).toBe(""); + expect(conn.getStatus()).toBe(ConnectionStatus.UNAUTHORIZED); + }); + + it("should revoke token when connection found in receivingConnections", async () => { + const targetAddress = "peer-456"; + const conn = new Connection("valid-token", targetAddress); + (manager as any).receivingConnections.set(targetAddress, conn); + + const handler = new RevokeTokenHandler(manager); + const command = new RevokeTokenCommand(createEventMessage(targetAddress)); + + await handler.handle(command); + + expect(conn.getToken()).toBe(""); + expect(conn.getStatus()).toBe(ConnectionStatus.UNAUTHORIZED); + }); + + it("should do nothing when connection not found", async () => { + const targetAddress = "non-existent"; + const handler = new RevokeTokenHandler(manager); + const command = new RevokeTokenCommand(createEventMessage(targetAddress)); + + await expect(handler.handle(command)).resolves.toBeUndefined(); + expect(manager.getConnectionByAddress).toHaveBeenCalledWith(targetAddress); + }); + + it("should throw error when targetAddress is missing", async () => { + const invalidMessage = { + event: DuplexMessageEvent.MESSAGE_EVENT_REVOKE_TOKEN, + revokeToken: { token: "test-token" }, + }; + + const handler = new RevokeTokenHandler(manager); + const command = new RevokeTokenCommand(invalidMessage as any); + + await expect(handler.handle(command)).rejects.toThrow( + "Target address is required to revoke token." + ); + }); + + it("should create command with correct type and payload", () => { + const eventMessage = createEventMessage("peer-001"); + const command = new RevokeTokenCommand(eventMessage); + + expect(command.type).toBe(DuplexMessageEvent.MESSAGE_EVENT_REVOKE_TOKEN); + expect(command.payload).toEqual(eventMessage); + }); +}); diff --git a/Tokenization/backend/wrapper/src/test/client/ConnectionManager/ConnectionManager.test.ts b/Tokenization/backend/wrapper/src/test/client/ConnectionManager/index.ts similarity index 100% rename from Tokenization/backend/wrapper/src/test/client/ConnectionManager/ConnectionManager.test.ts rename to Tokenization/backend/wrapper/src/test/client/ConnectionManager/index.ts From e0794386344df923b699554fa8f39a534e12e23f Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Tue, 5 Aug 2025 21:50:14 +0200 Subject: [PATCH 38/81] feat: implement token revokation logic and testing --- Tokenization/backend/wrapper/package.json | 4 +- .../wrapper/src/central/CentralSystem.ts | 105 ++++++++++++++++-- .../client/Commands/revokeToken.command.ts | 9 +- .../client/Commands/revokeToken.handler.ts | 2 +- .../src/client/Connection/Connection.ts | 8 ++ .../ConnectionManager/CentralConnection.ts | 4 +- .../ConnectionManager/ConnectionManager.ts | 12 +- .../CentralCommandDispatcher.ts | 14 ++- .../backend/wrapper/src/client/gRPCWrapper.ts | 38 ++++++- .../wrapper/src/models/commands.model.ts | 2 +- .../wrapper/src/models/message.model.ts | 11 +- .../backend/wrapper/src/proto/wrapper.proto | 3 +- .../test/client/Commands/revokeToken.test.ts | 21 ++-- 13 files changed, 189 insertions(+), 44 deletions(-) diff --git a/Tokenization/backend/wrapper/package.json b/Tokenization/backend/wrapper/package.json index 04922d113..7f472886a 100644 --- a/Tokenization/backend/wrapper/package.json +++ b/Tokenization/backend/wrapper/package.json @@ -5,8 +5,8 @@ "scripts": { "test": "jest", "build": "tsc -p tsconfig.build.json && cp -r src/proto dist", - "start-client": "node dist/client/gRPCWrapper.js", - "start-central": "node dist/central/CentralSystem.js" + "client": "node dist/client/gRPCWrapper.js", + "central": "node dist/central/CentralSystem.js" }, "author": "ALICEO2", "devDependencies": { diff --git a/Tokenization/backend/wrapper/src/central/CentralSystem.ts b/Tokenization/backend/wrapper/src/central/CentralSystem.ts index 035837940..0d122c390 100644 --- a/Tokenization/backend/wrapper/src/central/CentralSystem.ts +++ b/Tokenization/backend/wrapper/src/central/CentralSystem.ts @@ -15,6 +15,10 @@ import * as grpc from "@grpc/grpc-js"; import * as protoLoader from "@grpc/proto-loader"; import path from "path"; import { LogManager } from "@aliceo2/web-ui"; +import { + DuplexMessageEvent, + DuplexMessageModel, +} from "../models/message.model"; /** * @description Central System gRPC wrapper that manages client connections and handles gRPC streams with them. @@ -26,6 +30,10 @@ export class CentralSystemWrapper { // class properties private server: grpc.Server; + // clients management + private clients = new Map>(); + private clientIps = new Map(); // Peer -> IP map + /** * Initializes the Wrapper for CentralSystem. * @param port The port number to bind the gRPC server to. @@ -58,33 +66,103 @@ export class CentralSystemWrapper { }); } + /** + * @description Extracts IP address from peer string + * @param peer string e.g. ipv4:127.0.0.1:12345 + * @returns Extracted IP address + */ + private extractIpFromPeer(peer: string): string { + // Context + // IPv4 format: "ipv4:127.0.0.1:12345" + // IPv6 format: "ipv6:[::1]:12345" + + const ipv4Match = peer.match(/^ipv4:(.+?):\d+$/); + if (ipv4Match) return ipv4Match[1]; + + const ipv6Match = peer.match(/^ipv6:\[(.+?)\]:\d+$/); + if (ipv6Match) return ipv6Match[1]; + + // fallback to original peer if pattern doesn't match any + return peer; + } + /** * @description Handles the duplex stream from the client. * @param call The duplex stream call object. */ private clientStreamHandler(call: grpc.ServerDuplexStream): void { + const peer = call.getPeer(); + const clientIp = this.extractIpFromPeer(peer); + this.logger.infoMessage( - `Client ${call.getPeer()} connected to CentralSystem stream stream` + `Client ${clientIp} (${peer}) connected to CentralSystem stream` ); + // Add client to maps + this.clients.set(clientIp, call); + this.clientIps.set(peer, clientIp); + // Listen for data events from the client call.on("data", (payload: any) => { - console.log(`Received from ${call.getPeer()}:`, payload); + this.logger.infoMessage(`Received from ${clientIp}:`, payload); }); // Handle stream end event call.on("end", () => { - this.logger.infoMessage(`Client ${call.getPeer()} ended stream.`); + this.logger.infoMessage(`Client ${clientIp} ended stream.`); + this.cleanupClient(peer); call.end(); }); // Handle stream error event - call.on("error", (err) => - this.logger.infoMessage( - `Stream error from client ${call.getPeer()}:`, - err - ) - ); + call.on("error", (err) => { + this.logger.infoMessage(`Stream error from client ${clientIp}:`, err); + this.cleanupClient(peer); + }); + } + + /** + * @description Cleans up client resources + * @param peer Original peer string + */ + private cleanupClient(peer: string): void { + const clientIp = this.clientIps.get(peer); + if (clientIp) { + this.clients.delete(clientIp); + this.clientIps.delete(peer); + this.logger.infoMessage(`Cleaned up resources of ${clientIp}`); + } + } + + /** + * @description Sends data to a specific client by IP address + * @param ip Client IP address + * @param data Data to send + * @returns Whether the data was successfully sent + */ + public sendEvent(ip: string, data: DuplexMessageModel): boolean { + const client = this.clients.get(ip); + if (!client) { + this.logger.warnMessage(`Client ${ip} not found for sending event`); + return false; + } + + try { + client.write(data); + this.logger.infoMessage(`Sent event to ${ip}:`, data); + return true; + } catch (err) { + this.logger.errorMessage(`Error sending to ${ip}:`, err); + return false; + } + } + + /** + * @description Gets all connected client IPs + * @returns Array of connected client IPs + */ + public getConnectedClients(): string[] { + return Array.from(this.clients.keys()); } /** @@ -111,3 +189,12 @@ const PROTO_PATH = path.join(__dirname, "../proto/wrapper.proto"); const centralSystem = new CentralSystemWrapper(PROTO_PATH, 50051); // Start listening explicitly centralSystem.listen(); + +setTimeout(() => { + centralSystem.sendEvent(centralSystem.getConnectedClients()[0], { + event: DuplexMessageEvent.MESSAGE_EVENT_REVOKE_TOKEN, + payload: { + targetAddress: "a", + }, + }); +}, 5000); diff --git a/Tokenization/backend/wrapper/src/client/Commands/revokeToken.command.ts b/Tokenization/backend/wrapper/src/client/Commands/revokeToken.command.ts index 88bea615b..baeff3792 100644 --- a/Tokenization/backend/wrapper/src/client/Commands/revokeToken.command.ts +++ b/Tokenization/backend/wrapper/src/client/Commands/revokeToken.command.ts @@ -12,12 +12,9 @@ * or submit itself to any jurisdiction. */ import { Command } from "../../models/commands.model"; -import { - DuplexMessageEvent, - DuplexMessageModel, -} from "../../models/message.model"; +import { DuplexMessageEvent, TokenMessage } from "../../models/message.model"; export class RevokeTokenCommand implements Command { - readonly type = DuplexMessageEvent.MESSAGE_EVENT_REVOKE_TOKEN; - constructor(public payload: DuplexMessageModel) {} + readonly event = DuplexMessageEvent.MESSAGE_EVENT_REVOKE_TOKEN; + constructor(public payload: TokenMessage) {} } diff --git a/Tokenization/backend/wrapper/src/client/Commands/revokeToken.handler.ts b/Tokenization/backend/wrapper/src/client/Commands/revokeToken.handler.ts index 299c71c85..08a8b9f89 100644 --- a/Tokenization/backend/wrapper/src/client/Commands/revokeToken.handler.ts +++ b/Tokenization/backend/wrapper/src/client/Commands/revokeToken.handler.ts @@ -19,7 +19,7 @@ export class RevokeTokenHandler implements CommandHandler { constructor(private manager: ConnectionManager) {} async handle(command: RevokeTokenCommand): Promise { - const { targetAddress } = command.payload.revokeToken || {}; + const { targetAddress } = command.payload || {}; if (!targetAddress) { throw new Error("Target address is required to revoke token."); } diff --git a/Tokenization/backend/wrapper/src/client/Connection/Connection.ts b/Tokenization/backend/wrapper/src/client/Connection/Connection.ts index 2cd5b572f..79ebfdcec 100644 --- a/Tokenization/backend/wrapper/src/client/Connection/Connection.ts +++ b/Tokenization/backend/wrapper/src/client/Connection/Connection.ts @@ -56,4 +56,12 @@ export class Connection { public getStatus(): string { return this.status; } + + /** + * @description Returns target address for this Connection object + * @returns Target address + */ + public getTargetAddress(): string { + return this.targetAddress; + } } diff --git a/Tokenization/backend/wrapper/src/client/ConnectionManager/CentralConnection.ts b/Tokenization/backend/wrapper/src/client/ConnectionManager/CentralConnection.ts index 70ed788d3..30cc02b96 100644 --- a/Tokenization/backend/wrapper/src/client/ConnectionManager/CentralConnection.ts +++ b/Tokenization/backend/wrapper/src/client/ConnectionManager/CentralConnection.ts @@ -14,6 +14,7 @@ import * as grpc from "@grpc/grpc-js"; import { LogManager } from "@aliceo2/web-ui"; import { CentralCommandDispatcher } from "./EventManagement/CentralCommandDispatcher"; +import { DuplexMessageModel } from "../../models/message.model"; /** * @description This class manages the duplex stream with the CentralSystem gRPC service. @@ -36,7 +37,8 @@ export class CentralConnection { this.stream = this.client.ClientStream(); - this.stream!.on("data", (payload) => { + this.stream!.on("data", (payload: DuplexMessageModel) => { + console.log("Received payload:", JSON.stringify(payload)); this.dispatcher.dispatch(payload); }); diff --git a/Tokenization/backend/wrapper/src/client/ConnectionManager/ConnectionManager.ts b/Tokenization/backend/wrapper/src/client/ConnectionManager/ConnectionManager.ts index 57d751d8c..06712e2c2 100644 --- a/Tokenization/backend/wrapper/src/client/ConnectionManager/ConnectionManager.ts +++ b/Tokenization/backend/wrapper/src/client/ConnectionManager/ConnectionManager.ts @@ -18,7 +18,7 @@ import { CentralCommandDispatcher } from "./EventManagement/CentralCommandDispat import { Connection } from "../Connection/Connection"; import { LogManager } from "@aliceo2/web-ui"; import { Command, CommandHandler } from "models/commands.model"; -import { DuplexMessageEvent } from "models/message.model"; +import { DuplexMessageEvent } from "../../models/message.model"; /** * @description Manages all the connection between clients and central system. @@ -114,4 +114,14 @@ export class ConnectionManager { this.receivingConnections.get(address) ); } + + public getAllConnections(): { + sending: Connection[]; + receiving: Connection[]; + } { + return { + sending: [...this.sendingConnections.values()], + receiving: [...this.receivingConnections.values()], + }; + } } diff --git a/Tokenization/backend/wrapper/src/client/ConnectionManager/EventManagement/CentralCommandDispatcher.ts b/Tokenization/backend/wrapper/src/client/ConnectionManager/EventManagement/CentralCommandDispatcher.ts index f6eb7ccf6..451f275b8 100644 --- a/Tokenization/backend/wrapper/src/client/ConnectionManager/EventManagement/CentralCommandDispatcher.ts +++ b/Tokenization/backend/wrapper/src/client/ConnectionManager/EventManagement/CentralCommandDispatcher.ts @@ -14,24 +14,26 @@ import { LogManager } from "@aliceo2/web-ui"; import { Command, CommandHandler } from "models/commands.model"; -import { DuplexMessageEvent } from "models/message.model"; +import { DuplexMessageEvent } from "../../../models/message.model"; export class CentralCommandDispatcher { private handlers = new Map(); private logger = LogManager.getLogger("CentralCommandDispatcher"); register( - type: DuplexMessageEvent, + event: DuplexMessageEvent, handler: CommandHandler ): void { - this.handlers.set(type, handler); + console.log(`Registering handler for command type: ${event}`); + this.handlers.set(event, handler); } async dispatch(command: Command): Promise { - const handler = this.handlers.get(command.type); + const handler = this.handlers.get(command.event); + this.logger.debugMessage(`Dispatching command: ${command.event}`); if (!handler) { this.logger.warnMessage( - `No handler registered for command type: ${command.type}` + `No handler registered for command type: ${command.event}` ); return; } @@ -40,7 +42,7 @@ export class CentralCommandDispatcher { await handler.handle(command); } catch (error) { this.logger.errorMessage( - `Error handling command ${command.type}:`, + `Error handling command ${command.event}:`, error ); } diff --git a/Tokenization/backend/wrapper/src/client/gRPCWrapper.ts b/Tokenization/backend/wrapper/src/client/gRPCWrapper.ts index 0452daa22..6e1dd3148 100644 --- a/Tokenization/backend/wrapper/src/client/gRPCWrapper.ts +++ b/Tokenization/backend/wrapper/src/client/gRPCWrapper.ts @@ -14,7 +14,8 @@ import path from "path"; import { ConnectionManager } from "./ConnectionManager/ConnectionManager"; import { RevokeTokenHandler } from "./Commands/revokeToken.handler"; -import { DuplexMessageEvent } from "models/message.model"; +import { DuplexMessageEvent } from "../models/message.model"; +import { Connection } from "./Connection/Connection"; /** * @description Wrapper class for managing secure gRPC wrapper. @@ -52,11 +53,44 @@ export class gRPCWrapper { /** * @description Starts the Connection Manager stream connection with Central System */ - public connectToCentralSystem(): void { + public connectToCentralSystem() { this.ConnectionManager.connectToCentralSystem(); } + + /** + * @description Returns all saved connections. + * + * @returns An object containing the sending and receiving connections. + */ + public getAllConnections(): { + sending: Connection[]; + receiving: Connection[]; + } { + return this.ConnectionManager.getAllConnections(); + } + + public getSummary(): string { + const conn = this.ConnectionManager.getAllConnections(); + return ( + `Wrapper Summary: ` + + `\nSending Connections: ${conn.sending.length}` + + `\nReceiving Connections: ${conn.receiving.length}` + + conn.sending + .map((c) => `\n- ${c.getTargetAddress()} (${c.getStatus()})`) + .join("") + + conn.receiving + .map((c) => `\n- ${c.getTargetAddress()} (${c.getStatus()})`) + .join("") + ); + } } const PROTO_PATH = path.join(__dirname, "../proto/wrapper.proto"); const grpc = new gRPCWrapper(PROTO_PATH, "localhost:50051"); grpc.connectToCentralSystem(); +console.log(grpc.getSummary()); + +setTimeout(() => { + console.log("New status after 10 seconds and token revokation:"); + console.log(grpc.getSummary()); +}, 10000); diff --git a/Tokenization/backend/wrapper/src/models/commands.model.ts b/Tokenization/backend/wrapper/src/models/commands.model.ts index 0a595b005..874a301d4 100644 --- a/Tokenization/backend/wrapper/src/models/commands.model.ts +++ b/Tokenization/backend/wrapper/src/models/commands.model.ts @@ -22,7 +22,7 @@ import { DuplexMessageEvent } from "./message.model"; */ export interface Command { - type: DuplexMessageEvent; + event: DuplexMessageEvent; payload: any; } diff --git a/Tokenization/backend/wrapper/src/models/message.model.ts b/Tokenization/backend/wrapper/src/models/message.model.ts index 07abc0b18..d61598439 100644 --- a/Tokenization/backend/wrapper/src/models/message.model.ts +++ b/Tokenization/backend/wrapper/src/models/message.model.ts @@ -23,9 +23,9 @@ * @property MESSAGE_EVENT_REVOKE_TOKEN: Event for revoking an existing token. */ export enum DuplexMessageEvent { - MESSAGE_EVENT_EMPTY = 0, - MESSAGE_EVENT_NEW_TOKEN = 1, - MESSAGE_EVENT_REVOKE_TOKEN = 2, + MESSAGE_EVENT_EMPTY = "MESSAGE_EVENT_EMPTY", + MESSAGE_EVENT_NEW_TOKEN = "MESSAGE_EVENT_NEW_TOKEN", + MESSAGE_EVENT_REVOKE_TOKEN = "MESSAGE_EVENT_REVOKE_TOKEN", } // ====================================== @@ -46,10 +46,9 @@ export interface TokenMessage { * @description Model for duplex stream messages between client and central system. * @property {DuplexMessageEvent} event - The event type of the message. * @property {TokenMessage} data - The data associated with the event, it may be undefined for some events. - * @example {event: DuplexMessageEvent.MESSAGE_EVENT_NEW_TOKEN, data: {token: '', targetAddress: ''}} + * @example {event: DuplexMessageEvent.MESSAGE_EVENT_NEW_TOKEN, {token: '', targetAddress: ''}} */ export interface DuplexMessageModel { event: DuplexMessageEvent; - newToken?: TokenMessage; - revokeToken?: TokenMessage; + payload: TokenMessage; } diff --git a/Tokenization/backend/wrapper/src/proto/wrapper.proto b/Tokenization/backend/wrapper/src/proto/wrapper.proto index 9cb03f258..6b2de2832 100644 --- a/Tokenization/backend/wrapper/src/proto/wrapper.proto +++ b/Tokenization/backend/wrapper/src/proto/wrapper.proto @@ -44,8 +44,7 @@ message Payload { // Data related to specific event type oneof data { EmptyMessage emptyMessage = 2; - Token newToken = 3; - Token revokeToken = 4; + Token payload = 3; } } diff --git a/Tokenization/backend/wrapper/src/test/client/Commands/revokeToken.test.ts b/Tokenization/backend/wrapper/src/test/client/Commands/revokeToken.test.ts index 972565210..07bd939a6 100644 --- a/Tokenization/backend/wrapper/src/test/client/Commands/revokeToken.test.ts +++ b/Tokenization/backend/wrapper/src/test/client/Commands/revokeToken.test.ts @@ -18,16 +18,17 @@ import { Connection } from "../../../client/Connection/Connection"; import { ConnectionManager } from "../../../client/ConnectionManager/ConnectionManager"; import { DuplexMessageEvent } from "../../../models/message.model"; import { ConnectionStatus } from "../../../models/connection.model"; +import { Command } from "models/commands.model"; describe("RevokeToken", () => { function createEventMessage(targetAddress: string) { return { event: DuplexMessageEvent.MESSAGE_EVENT_REVOKE_TOKEN, - revokeToken: { + payload: { token: "test-token", targetAddress, }, - }; + } as Command; } let manager: ConnectionManager; @@ -51,7 +52,9 @@ describe("RevokeToken", () => { (manager as any).sendingConnections!.set(targetAddress, conn); const handler = new RevokeTokenHandler(manager); - const command = new RevokeTokenCommand(createEventMessage(targetAddress)); + const command = new RevokeTokenCommand( + createEventMessage(targetAddress).payload + ); await handler.handle(command); @@ -65,7 +68,9 @@ describe("RevokeToken", () => { (manager as any).receivingConnections.set(targetAddress, conn); const handler = new RevokeTokenHandler(manager); - const command = new RevokeTokenCommand(createEventMessage(targetAddress)); + const command = new RevokeTokenCommand( + createEventMessage(targetAddress).payload + ); await handler.handle(command); @@ -76,7 +81,9 @@ describe("RevokeToken", () => { it("should do nothing when connection not found", async () => { const targetAddress = "non-existent"; const handler = new RevokeTokenHandler(manager); - const command = new RevokeTokenCommand(createEventMessage(targetAddress)); + const command = new RevokeTokenCommand( + createEventMessage(targetAddress).payload + ); await expect(handler.handle(command)).resolves.toBeUndefined(); expect(manager.getConnectionByAddress).toHaveBeenCalledWith(targetAddress); @@ -98,9 +105,9 @@ describe("RevokeToken", () => { it("should create command with correct type and payload", () => { const eventMessage = createEventMessage("peer-001"); - const command = new RevokeTokenCommand(eventMessage); + const command = new RevokeTokenCommand(eventMessage.payload); - expect(command.type).toBe(DuplexMessageEvent.MESSAGE_EVENT_REVOKE_TOKEN); + expect(command.event).toBe(DuplexMessageEvent.MESSAGE_EVENT_REVOKE_TOKEN); expect(command.payload).toEqual(eventMessage); }); }); From 5527944cdc9e8db7e7f0312ec70a9cab1921c9fd Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Tue, 5 Aug 2025 22:25:14 +0200 Subject: [PATCH 39/81] feat: implement connection direction info inside of payload --- .../wrapper/src/central/CentralSystem.ts | 2 ++ .../client/Commands/revokeToken.handler.ts | 6 +++- .../src/client/Connection/Connection.ts | 7 +++- .../ConnectionManager/ConnectionManager.ts | 33 ++++++++++++++----- .../backend/wrapper/src/client/gRPCWrapper.ts | 10 ++++-- .../wrapper/src/models/message.model.ts | 21 +++++++++++- .../backend/wrapper/src/proto/wrapper.proto | 13 ++++++++ .../test/client/Commands/revokeToken.test.ts | 17 ++++++++-- 8 files changed, 93 insertions(+), 16 deletions(-) diff --git a/Tokenization/backend/wrapper/src/central/CentralSystem.ts b/Tokenization/backend/wrapper/src/central/CentralSystem.ts index 0d122c390..c5a827059 100644 --- a/Tokenization/backend/wrapper/src/central/CentralSystem.ts +++ b/Tokenization/backend/wrapper/src/central/CentralSystem.ts @@ -16,6 +16,7 @@ import * as protoLoader from "@grpc/proto-loader"; import path from "path"; import { LogManager } from "@aliceo2/web-ui"; import { + ConnectionDirection, DuplexMessageEvent, DuplexMessageModel, } from "../models/message.model"; @@ -194,6 +195,7 @@ setTimeout(() => { centralSystem.sendEvent(centralSystem.getConnectedClients()[0], { event: DuplexMessageEvent.MESSAGE_EVENT_REVOKE_TOKEN, payload: { + connectionDirection: ConnectionDirection.SENDING, targetAddress: "a", }, }); diff --git a/Tokenization/backend/wrapper/src/client/Commands/revokeToken.handler.ts b/Tokenization/backend/wrapper/src/client/Commands/revokeToken.handler.ts index 08a8b9f89..1cf9d00d8 100644 --- a/Tokenization/backend/wrapper/src/client/Commands/revokeToken.handler.ts +++ b/Tokenization/backend/wrapper/src/client/Commands/revokeToken.handler.ts @@ -24,7 +24,11 @@ export class RevokeTokenHandler implements CommandHandler { throw new Error("Target address is required to revoke token."); } - const conn = this.manager.getConnectionByAddress(targetAddress); + const conn = this.manager.getConnectionByAddress( + targetAddress, + command.payload.connectionDirection + ); + conn?.handleRevokeToken(); } } diff --git a/Tokenization/backend/wrapper/src/client/Connection/Connection.ts b/Tokenization/backend/wrapper/src/client/Connection/Connection.ts index 79ebfdcec..2347fa961 100644 --- a/Tokenization/backend/wrapper/src/client/Connection/Connection.ts +++ b/Tokenization/backend/wrapper/src/client/Connection/Connection.ts @@ -11,6 +11,7 @@ * granted to it by virtue of its status as an Intergovernmental Organization * or submit itself to any jurisdiction. */ +import { ConnectionDirection } from "../../models/message.model"; import { ConnectionStatus } from "../../models/connection.model"; /** @@ -21,7 +22,11 @@ export class Connection { private targetAddress: string; private status: ConnectionStatus; - constructor(token: string, targetAddress: string) { + constructor( + token: string, + targetAddress: string, + public direction: ConnectionDirection + ) { this.token = token; this.targetAddress = targetAddress; diff --git a/Tokenization/backend/wrapper/src/client/ConnectionManager/ConnectionManager.ts b/Tokenization/backend/wrapper/src/client/ConnectionManager/ConnectionManager.ts index 06712e2c2..4f0c11cc0 100644 --- a/Tokenization/backend/wrapper/src/client/ConnectionManager/ConnectionManager.ts +++ b/Tokenization/backend/wrapper/src/client/ConnectionManager/ConnectionManager.ts @@ -18,7 +18,10 @@ import { CentralCommandDispatcher } from "./EventManagement/CentralCommandDispat import { Connection } from "../Connection/Connection"; import { LogManager } from "@aliceo2/web-ui"; import { Command, CommandHandler } from "models/commands.model"; -import { DuplexMessageEvent } from "../../models/message.model"; +import { + ConnectionDirection, + DuplexMessageEvent, +} from "../../models/message.model"; /** * @description Manages all the connection between clients and central system. @@ -75,8 +78,14 @@ export class ConnectionManager { this.centralDispatcher ); - this.sendingConnections.set("a", new Connection("1", "a")); - this.sendingConnections.set("b", new Connection("2", "b")); + this.sendingConnections.set( + "a", + new Connection("1", "a", ConnectionDirection.SENDING) + ); + this.sendingConnections.set( + "b", + new Connection("2", "b", ConnectionDirection.SENDING) + ); } registerCommandHandlers( @@ -108,11 +117,19 @@ export class ConnectionManager { * @description Gets the connection instance by address. * @returns{Connection} connection instance. */ - getConnectionByAddress(address: string): Connection | undefined { - return ( - this.sendingConnections.get(address) || - this.receivingConnections.get(address) - ); + getConnectionByAddress( + address: string, + direction: ConnectionDirection + ): Connection | undefined { + switch (direction) { + case ConnectionDirection.SENDING: + return this.sendingConnections.get(address); + case ConnectionDirection.RECEIVING: + return this.receivingConnections.get(address); + default: + this.logger.errorMessage(`Invalid connection direction: ${direction}`); + return undefined; + } } public getAllConnections(): { diff --git a/Tokenization/backend/wrapper/src/client/gRPCWrapper.ts b/Tokenization/backend/wrapper/src/client/gRPCWrapper.ts index 6e1dd3148..9673c6ad2 100644 --- a/Tokenization/backend/wrapper/src/client/gRPCWrapper.ts +++ b/Tokenization/backend/wrapper/src/client/gRPCWrapper.ts @@ -76,10 +76,16 @@ export class gRPCWrapper { `\nSending Connections: ${conn.sending.length}` + `\nReceiving Connections: ${conn.receiving.length}` + conn.sending - .map((c) => `\n- ${c.getTargetAddress()} (${c.getStatus()})`) + .map( + (c) => + `\n- ${c.getTargetAddress()} - ${c.direction}\n\t(${c.getStatus()})` + ) .join("") + conn.receiving - .map((c) => `\n- ${c.getTargetAddress()} (${c.getStatus()})`) + .map( + (c) => + `\n- ${c.getTargetAddress()} - ${c.direction}\n\t(${c.getStatus()})` + ) .join("") ); } diff --git a/Tokenization/backend/wrapper/src/models/message.model.ts b/Tokenization/backend/wrapper/src/models/message.model.ts index d61598439..fb64a4002 100644 --- a/Tokenization/backend/wrapper/src/models/message.model.ts +++ b/Tokenization/backend/wrapper/src/models/message.model.ts @@ -28,6 +28,18 @@ export enum DuplexMessageEvent { MESSAGE_EVENT_REVOKE_TOKEN = "MESSAGE_EVENT_REVOKE_TOKEN", } +/** + * @enum Represents the direction of a connection in the system. + * @property SENDING: Indicates a connection where messages are sent to another client. + * @property RECEIVING: Indicates a connection where messages are received from another client. + * @property DUPLEX: Indicates a connection that can both send and receive messages. + */ +export enum ConnectionDirection { + SENDING = "SENDING", + RECEIVING = "RECEIVING", + DUPLEX = "DUPLEX", +} + // ====================================== // INTERFACES // ====================================== @@ -39,14 +51,21 @@ export enum DuplexMessageEvent { */ export interface TokenMessage { token?: string; + connectionDirection: ConnectionDirection; targetAddress: string; } /** * @description Model for duplex stream messages between client and central system. * @property {DuplexMessageEvent} event - The event type of the message. + * @property {ConnectionDirection} connectionDirection - The direction of the connection, optional for some events. * @property {TokenMessage} data - The data associated with the event, it may be undefined for some events. - * @example {event: DuplexMessageEvent.MESSAGE_EVENT_NEW_TOKEN, {token: '', targetAddress: ''}} + * @example + * { + * event: DuplexMessageEvent.MESSAGE_EVENT_NEW_TOKEN, + * connectionDirection: ConnectionDirection.SENDING, + * payload: {token: 'abc', targetAddress: 'localhost:50051'} + * } */ export interface DuplexMessageModel { event: DuplexMessageEvent; diff --git a/Tokenization/backend/wrapper/src/proto/wrapper.proto b/Tokenization/backend/wrapper/src/proto/wrapper.proto index 6b2de2832..ad2cac80a 100644 --- a/Tokenization/backend/wrapper/src/proto/wrapper.proto +++ b/Tokenization/backend/wrapper/src/proto/wrapper.proto @@ -35,12 +35,14 @@ message EmptyMessage {} message Token { string token = 1; string targetAddress = 2; + ConnectionDirection connectionDirection = 3; } // Stream message that can contain one of specific messages message Payload { // Message event type MessageEvent event = 1; + // Data related to specific event type oneof data { EmptyMessage emptyMessage = 2; @@ -62,3 +64,14 @@ enum MessageEvent { // Revoke token message type, contains a token to be revoked MESSAGE_EVENT_REVOKE_TOKEN = 2; } + +enum ConnectionDirection { + // Direction from client to server + SENDING = 1; + + // Direction from server to client + RECEIVING = 2; + + // Duplex connection, both sending and receiving + DUPLEX = 3; +} \ No newline at end of file diff --git a/Tokenization/backend/wrapper/src/test/client/Commands/revokeToken.test.ts b/Tokenization/backend/wrapper/src/test/client/Commands/revokeToken.test.ts index 07bd939a6..554507eee 100644 --- a/Tokenization/backend/wrapper/src/test/client/Commands/revokeToken.test.ts +++ b/Tokenization/backend/wrapper/src/test/client/Commands/revokeToken.test.ts @@ -16,7 +16,10 @@ import { RevokeTokenCommand } from "../../../client/Commands/revokeToken.command import { RevokeTokenHandler } from "../../../client/Commands/revokeToken.handler"; import { Connection } from "../../../client/Connection/Connection"; import { ConnectionManager } from "../../../client/ConnectionManager/ConnectionManager"; -import { DuplexMessageEvent } from "../../../models/message.model"; +import { + ConnectionDirection, + DuplexMessageEvent, +} from "../../../models/message.model"; import { ConnectionStatus } from "../../../models/connection.model"; import { Command } from "models/commands.model"; @@ -48,7 +51,11 @@ describe("RevokeToken", () => { it("should revoke token when connection found in sendingConnections", async () => { const targetAddress = "peer-123"; - const conn = new Connection("valid-token", targetAddress); + const conn = new Connection( + "valid-token", + targetAddress, + ConnectionDirection.SENDING + ); (manager as any).sendingConnections!.set(targetAddress, conn); const handler = new RevokeTokenHandler(manager); @@ -64,7 +71,11 @@ describe("RevokeToken", () => { it("should revoke token when connection found in receivingConnections", async () => { const targetAddress = "peer-456"; - const conn = new Connection("valid-token", targetAddress); + const conn = new Connection( + "valid-token", + targetAddress, + ConnectionDirection.RECEIVING + ); (manager as any).receivingConnections.set(targetAddress, conn); const handler = new RevokeTokenHandler(manager); From 090da6ba85d11e81dee9dde401572644a6e35c45 Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Thu, 7 Aug 2025 10:37:01 +0200 Subject: [PATCH 40/81] fix: fix tests --- .../wrapper/src/test/central/CentralSystem.test.ts | 3 ++- .../wrapper/src/test/client/Commands/revokeToken.test.ts | 9 ++++++--- Tokenization/backend/wrapper/tsconfig.build.json | 2 +- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/Tokenization/backend/wrapper/src/test/central/CentralSystem.test.ts b/Tokenization/backend/wrapper/src/test/central/CentralSystem.test.ts index 1599e0ccd..093412940 100644 --- a/Tokenization/backend/wrapper/src/test/central/CentralSystem.test.ts +++ b/Tokenization/backend/wrapper/src/test/central/CentralSystem.test.ts @@ -104,8 +104,9 @@ describe("CentralSystemWrapper", () => { expect(mockCall.end).toHaveBeenCalled(); expect(logger.infoMessage).toHaveBeenCalledWith( - "Client client123 connected to CentralSystem stream stream" + expect.stringContaining("Client client123") ); + expect(logger.infoMessage).toHaveBeenCalledWith( "Client client123 ended stream." ); diff --git a/Tokenization/backend/wrapper/src/test/client/Commands/revokeToken.test.ts b/Tokenization/backend/wrapper/src/test/client/Commands/revokeToken.test.ts index 554507eee..2f783fe16 100644 --- a/Tokenization/backend/wrapper/src/test/client/Commands/revokeToken.test.ts +++ b/Tokenization/backend/wrapper/src/test/client/Commands/revokeToken.test.ts @@ -28,8 +28,8 @@ describe("RevokeToken", () => { return { event: DuplexMessageEvent.MESSAGE_EVENT_REVOKE_TOKEN, payload: { + targetAddress: targetAddress, token: "test-token", - targetAddress, }, } as Command; } @@ -97,7 +97,10 @@ describe("RevokeToken", () => { ); await expect(handler.handle(command)).resolves.toBeUndefined(); - expect(manager.getConnectionByAddress).toHaveBeenCalledWith(targetAddress); + expect(manager.getConnectionByAddress).toHaveBeenCalledWith( + targetAddress, + undefined + ); }); it("should throw error when targetAddress is missing", async () => { @@ -119,6 +122,6 @@ describe("RevokeToken", () => { const command = new RevokeTokenCommand(eventMessage.payload); expect(command.event).toBe(DuplexMessageEvent.MESSAGE_EVENT_REVOKE_TOKEN); - expect(command.payload).toEqual(eventMessage); + expect(command).toEqual(eventMessage); }); }); diff --git a/Tokenization/backend/wrapper/tsconfig.build.json b/Tokenization/backend/wrapper/tsconfig.build.json index 681ec5d54..9478b5399 100644 --- a/Tokenization/backend/wrapper/tsconfig.build.json +++ b/Tokenization/backend/wrapper/tsconfig.build.json @@ -8,5 +8,5 @@ "sourceMap": true }, "include": ["src/**/*.ts"], - "exclude": ["tests", "node_modules"] + "exclude": ["src/**/test", "node_modules"] } From fd0e1dd35aaecc32069c6e26652392656343d4da Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Thu, 7 Aug 2025 12:33:30 +0200 Subject: [PATCH 41/81] feat: implement command for new token from central system --- .../wrapper/src/central/CentralSystem.ts | 16 +++++++ .../Commands/newToken/newToken.command.ts | 23 ++++++++++ .../Commands/newToken/newToken.handler.ts | 43 +++++++++++++++++++ .../{ => revokeToken}/revokeToken.command.ts | 7 ++- .../{ => revokeToken}/revokeToken.handler.ts | 4 +- .../ConnectionManager/ConnectionManager.ts | 26 +++++++++++ .../backend/wrapper/src/client/gRPCWrapper.ts | 17 ++++++-- .../test/client/Commands/revokeToken.test.ts | 4 +- 8 files changed, 130 insertions(+), 10 deletions(-) create mode 100644 Tokenization/backend/wrapper/src/client/Commands/newToken/newToken.command.ts create mode 100644 Tokenization/backend/wrapper/src/client/Commands/newToken/newToken.handler.ts rename Tokenization/backend/wrapper/src/client/Commands/{ => revokeToken}/revokeToken.command.ts (83%) rename Tokenization/backend/wrapper/src/client/Commands/{ => revokeToken}/revokeToken.handler.ts (88%) diff --git a/Tokenization/backend/wrapper/src/central/CentralSystem.ts b/Tokenization/backend/wrapper/src/central/CentralSystem.ts index c5a827059..c01e7f17d 100644 --- a/Tokenization/backend/wrapper/src/central/CentralSystem.ts +++ b/Tokenization/backend/wrapper/src/central/CentralSystem.ts @@ -199,4 +199,20 @@ setTimeout(() => { targetAddress: "a", }, }); + centralSystem.sendEvent(centralSystem.getConnectedClients()[0], { + event: DuplexMessageEvent.MESSAGE_EVENT_NEW_TOKEN, + payload: { + connectionDirection: ConnectionDirection.SENDING, + targetAddress: "a", + token: "newToken", + }, + }); + centralSystem.sendEvent(centralSystem.getConnectedClients()[0], { + event: DuplexMessageEvent.MESSAGE_EVENT_NEW_TOKEN, + payload: { + connectionDirection: ConnectionDirection.SENDING, + targetAddress: "c", + token: "tokenForNewAddress", + }, + }); }, 5000); diff --git a/Tokenization/backend/wrapper/src/client/Commands/newToken/newToken.command.ts b/Tokenization/backend/wrapper/src/client/Commands/newToken/newToken.command.ts new file mode 100644 index 000000000..98c6a5dc4 --- /dev/null +++ b/Tokenization/backend/wrapper/src/client/Commands/newToken/newToken.command.ts @@ -0,0 +1,23 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ +import { Command } from "../../../models/commands.model"; +import { + DuplexMessageEvent, + TokenMessage, +} from "../../../models/message.model"; + +export class NewTokenCommand implements Command { + readonly event = DuplexMessageEvent.MESSAGE_EVENT_NEW_TOKEN; + constructor(public payload: TokenMessage) {} +} diff --git a/Tokenization/backend/wrapper/src/client/Commands/newToken/newToken.handler.ts b/Tokenization/backend/wrapper/src/client/Commands/newToken/newToken.handler.ts new file mode 100644 index 000000000..b8471c407 --- /dev/null +++ b/Tokenization/backend/wrapper/src/client/Commands/newToken/newToken.handler.ts @@ -0,0 +1,43 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ +import { CommandHandler } from "../../../models/commands.model"; +import { NewTokenCommand } from "./newToken.command"; +import { ConnectionManager } from "../../ConnectionManager/ConnectionManager"; +import { ConnectionDirection } from "../../../models/message.model"; + +export class NewTokenHandler implements CommandHandler { + constructor(private manager: ConnectionManager) {} + + async handle(command: NewTokenCommand): Promise { + const { targetAddress, connectionDirection, token } = command.payload || {}; + if (!targetAddress || !token || !connectionDirection) { + throw new Error( + "Insufficient arguments. Expected: targetAddress, connectionDirection, token." + ); + } + + const directions = + connectionDirection === ConnectionDirection.DUPLEX + ? [ConnectionDirection.SENDING, ConnectionDirection.RECEIVING] + : [connectionDirection]; + + for (const dir of directions) { + let conn = this.manager.getConnectionByAddress(targetAddress, dir); + if (!conn) { + conn = this.manager.createNewConnection(targetAddress, dir, token); + } + conn.handleNewToken(token); + } + } +} diff --git a/Tokenization/backend/wrapper/src/client/Commands/revokeToken.command.ts b/Tokenization/backend/wrapper/src/client/Commands/revokeToken/revokeToken.command.ts similarity index 83% rename from Tokenization/backend/wrapper/src/client/Commands/revokeToken.command.ts rename to Tokenization/backend/wrapper/src/client/Commands/revokeToken/revokeToken.command.ts index baeff3792..f92c99022 100644 --- a/Tokenization/backend/wrapper/src/client/Commands/revokeToken.command.ts +++ b/Tokenization/backend/wrapper/src/client/Commands/revokeToken/revokeToken.command.ts @@ -11,8 +11,11 @@ * granted to it by virtue of its status as an Intergovernmental Organization * or submit itself to any jurisdiction. */ -import { Command } from "../../models/commands.model"; -import { DuplexMessageEvent, TokenMessage } from "../../models/message.model"; +import { Command } from "../../../models/commands.model"; +import { + DuplexMessageEvent, + TokenMessage, +} from "../../../models/message.model"; export class RevokeTokenCommand implements Command { readonly event = DuplexMessageEvent.MESSAGE_EVENT_REVOKE_TOKEN; diff --git a/Tokenization/backend/wrapper/src/client/Commands/revokeToken.handler.ts b/Tokenization/backend/wrapper/src/client/Commands/revokeToken/revokeToken.handler.ts similarity index 88% rename from Tokenization/backend/wrapper/src/client/Commands/revokeToken.handler.ts rename to Tokenization/backend/wrapper/src/client/Commands/revokeToken/revokeToken.handler.ts index 1cf9d00d8..2ca77a54f 100644 --- a/Tokenization/backend/wrapper/src/client/Commands/revokeToken.handler.ts +++ b/Tokenization/backend/wrapper/src/client/Commands/revokeToken/revokeToken.handler.ts @@ -11,9 +11,9 @@ * granted to it by virtue of its status as an Intergovernmental Organization * or submit itself to any jurisdiction. */ -import { CommandHandler } from "../../models/commands.model"; +import { CommandHandler } from "../../../models/commands.model"; import { RevokeTokenCommand } from "./revokeToken.command"; -import { ConnectionManager } from "../ConnectionManager/ConnectionManager"; +import { ConnectionManager } from "../../ConnectionManager/ConnectionManager"; export class RevokeTokenHandler implements CommandHandler { constructor(private manager: ConnectionManager) {} diff --git a/Tokenization/backend/wrapper/src/client/ConnectionManager/ConnectionManager.ts b/Tokenization/backend/wrapper/src/client/ConnectionManager/ConnectionManager.ts index 4f0c11cc0..c3d5bb45f 100644 --- a/Tokenization/backend/wrapper/src/client/ConnectionManager/ConnectionManager.ts +++ b/Tokenization/backend/wrapper/src/client/ConnectionManager/ConnectionManager.ts @@ -113,6 +113,28 @@ export class ConnectionManager { this.centralConnection.disconnect(); } + /** + * Creates new connection + * @param address Target (external) address of the connection + * @param direction Direction of connection + * @param token Optional token for connection + */ + createNewConnection( + address: string, + direction: ConnectionDirection, + token?: string + ) { + const conn = new Connection(token || "", address, direction); + + if (direction === ConnectionDirection.RECEIVING) { + this.receivingConnections.set(address, conn); + } else { + this.sendingConnections.set(address, conn); + } + + return conn; + } + /** * @description Gets the connection instance by address. * @returns{Connection} connection instance. @@ -132,6 +154,10 @@ export class ConnectionManager { } } + /** + * Returns object with all connections + * @returns Object of all connections + */ public getAllConnections(): { sending: Connection[]; receiving: Connection[]; diff --git a/Tokenization/backend/wrapper/src/client/gRPCWrapper.ts b/Tokenization/backend/wrapper/src/client/gRPCWrapper.ts index 9673c6ad2..94655ef91 100644 --- a/Tokenization/backend/wrapper/src/client/gRPCWrapper.ts +++ b/Tokenization/backend/wrapper/src/client/gRPCWrapper.ts @@ -13,9 +13,10 @@ */ import path from "path"; import { ConnectionManager } from "./ConnectionManager/ConnectionManager"; -import { RevokeTokenHandler } from "./Commands/revokeToken.handler"; +import { RevokeTokenHandler } from "./Commands/revokeToken/revokeToken.handler"; import { DuplexMessageEvent } from "../models/message.model"; import { Connection } from "./Connection/Connection"; +import { NewTokenHandler } from "./Commands/newToken/newToken.handler"; /** * @description Wrapper class for managing secure gRPC wrapper. @@ -47,6 +48,10 @@ export class gRPCWrapper { event: DuplexMessageEvent.MESSAGE_EVENT_REVOKE_TOKEN, handler: new RevokeTokenHandler(this.ConnectionManager), }, + { + event: DuplexMessageEvent.MESSAGE_EVENT_NEW_TOKEN, + handler: new NewTokenHandler(this.ConnectionManager), + }, ]); } @@ -78,13 +83,17 @@ export class gRPCWrapper { conn.sending .map( (c) => - `\n- ${c.getTargetAddress()} - ${c.direction}\n\t(${c.getStatus()})` + `\n- ${c.getTargetAddress()} \nDirection - ${ + c.direction + }\n\tStatus: (${c.getStatus()})\n\tToken: (${c.getToken()})` ) .join("") + conn.receiving .map( (c) => - `\n- ${c.getTargetAddress()} - ${c.direction}\n\t(${c.getStatus()})` + `\n- ${c.getTargetAddress()} \nDirection - ${ + c.direction + }\n\tStatus: (${c.getStatus()})\n\tToken: (${c.getToken()})` ) .join("") ); @@ -97,6 +106,6 @@ grpc.connectToCentralSystem(); console.log(grpc.getSummary()); setTimeout(() => { - console.log("New status after 10 seconds and token revokation:"); + console.log("New status after 10 seconds, token revokation and new token:"); console.log(grpc.getSummary()); }, 10000); diff --git a/Tokenization/backend/wrapper/src/test/client/Commands/revokeToken.test.ts b/Tokenization/backend/wrapper/src/test/client/Commands/revokeToken.test.ts index 2f783fe16..2602c5173 100644 --- a/Tokenization/backend/wrapper/src/test/client/Commands/revokeToken.test.ts +++ b/Tokenization/backend/wrapper/src/test/client/Commands/revokeToken.test.ts @@ -12,8 +12,8 @@ * or submit itself to any jurisdiction. */ -import { RevokeTokenCommand } from "../../../client/Commands/revokeToken.command"; -import { RevokeTokenHandler } from "../../../client/Commands/revokeToken.handler"; +import { RevokeTokenCommand } from "../../../client/Commands/revokeToken/revokeToken.command"; +import { RevokeTokenHandler } from "../../../client/Commands/revokeToken/revokeToken.handler"; import { Connection } from "../../../client/Connection/Connection"; import { ConnectionManager } from "../../../client/ConnectionManager/ConnectionManager"; import { From 5965abeb45836f40491b55458e4cfb276650e777 Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Thu, 7 Aug 2025 14:54:24 +0200 Subject: [PATCH 42/81] feat: add missing comments, banners and tests --- Tokenization/backend/wrapper/package.json | 3 +- .../backend/wrapper/scripts/banner.js | 68 ++++++++ .../wrapper/src/central/CentralSystem.ts | 1 + .../Commands/newToken/newToken.command.ts | 4 + .../Commands/newToken/newToken.handler.ts | 14 ++ .../revokeToken/revokeToken.command.ts | 4 + .../revokeToken/revokeToken.handler.ts | 17 ++ .../src/client/Connection/Connection.ts | 11 ++ .../ConnectionManager/CentralConnection.ts | 3 +- .../ConnectionManager/ConnectionManager.ts | 8 +- .../CentralCommandDispatcher.ts | 17 ++ .../backend/wrapper/src/client/gRPCWrapper.ts | 6 +- .../wrapper/src/models/commands.model.ts | 12 +- .../wrapper/src/models/connection.model.ts | 13 ++ .../src/test/central/CentralSystem.test.ts | 14 ++ .../src/test/client/Commands/newToken.test.ts | 154 ++++++++++++++++++ .../test/client/ConnectionManager/index.ts | 14 ++ .../wrapper/src/utils/types/webui.d.ts | 14 ++ 18 files changed, 368 insertions(+), 9 deletions(-) create mode 100644 Tokenization/backend/wrapper/scripts/banner.js create mode 100644 Tokenization/backend/wrapper/src/test/client/Commands/newToken.test.ts diff --git a/Tokenization/backend/wrapper/package.json b/Tokenization/backend/wrapper/package.json index 7f472886a..0c420b84c 100644 --- a/Tokenization/backend/wrapper/package.json +++ b/Tokenization/backend/wrapper/package.json @@ -6,7 +6,8 @@ "test": "jest", "build": "tsc -p tsconfig.build.json && cp -r src/proto dist", "client": "node dist/client/gRPCWrapper.js", - "central": "node dist/central/CentralSystem.js" + "central": "node dist/central/CentralSystem.js", + "process-banners": "node scripts/banner.js" }, "author": "ALICEO2", "devDependencies": { diff --git a/Tokenization/backend/wrapper/scripts/banner.js b/Tokenization/backend/wrapper/scripts/banner.js new file mode 100644 index 000000000..bbba4a30f --- /dev/null +++ b/Tokenization/backend/wrapper/scripts/banner.js @@ -0,0 +1,68 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +const fs = require("fs"); +const path = require("path"); + +const banner = `/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. +*/ +`; + +const processFile = (filePath) => { + try { + const content = fs.readFileSync(filePath, "utf8"); + + if (content.trim().startsWith(banner.trim())) { + return; + } + + const newContent = banner + "\n" + content; + fs.writeFileSync(filePath, newContent, "utf8"); + console.log(`Added banner to: ${filePath}`); + } catch (err) { + console.error(`Error with file ${filePath}:`, err); + } +}; + +const excludedDirs = ["node_modules", "dist"]; +const walkDir = (dir) => { + const files = fs.readdirSync(dir, { withFileTypes: true }); + + for (const file of files) { + const fullPath = path.join(dir, file.name); + + if (file.isDirectory() && !excludedDirs.includes(fullPath)) { + walkDir(fullPath); + } else if (file.isFile()) { + if (/\.(js|ts|jsx|tsx|mjs|cjs)$/.test(file.name)) { + processFile(fullPath); + } + } + } +}; + +const startDir = "./src/"; +walkDir(startDir); +console.log("Banners processed."); diff --git a/Tokenization/backend/wrapper/src/central/CentralSystem.ts b/Tokenization/backend/wrapper/src/central/CentralSystem.ts index c01e7f17d..58d938eed 100644 --- a/Tokenization/backend/wrapper/src/central/CentralSystem.ts +++ b/Tokenization/backend/wrapper/src/central/CentralSystem.ts @@ -11,6 +11,7 @@ * granted to it by virtue of its status as an Intergovernmental Organization * or submit itself to any jurisdiction. */ + import * as grpc from "@grpc/grpc-js"; import * as protoLoader from "@grpc/proto-loader"; import path from "path"; diff --git a/Tokenization/backend/wrapper/src/client/Commands/newToken/newToken.command.ts b/Tokenization/backend/wrapper/src/client/Commands/newToken/newToken.command.ts index 98c6a5dc4..df8889d0d 100644 --- a/Tokenization/backend/wrapper/src/client/Commands/newToken/newToken.command.ts +++ b/Tokenization/backend/wrapper/src/client/Commands/newToken/newToken.command.ts @@ -11,12 +11,16 @@ * granted to it by virtue of its status as an Intergovernmental Organization * or submit itself to any jurisdiction. */ + import { Command } from "../../../models/commands.model"; import { DuplexMessageEvent, TokenMessage, } from "../../../models/message.model"; +/** + * @description Command used to trigger new token for a specific connection. Handles structure logic. + */ export class NewTokenCommand implements Command { readonly event = DuplexMessageEvent.MESSAGE_EVENT_NEW_TOKEN; constructor(public payload: TokenMessage) {} diff --git a/Tokenization/backend/wrapper/src/client/Commands/newToken/newToken.handler.ts b/Tokenization/backend/wrapper/src/client/Commands/newToken/newToken.handler.ts index b8471c407..cac108af3 100644 --- a/Tokenization/backend/wrapper/src/client/Commands/newToken/newToken.handler.ts +++ b/Tokenization/backend/wrapper/src/client/Commands/newToken/newToken.handler.ts @@ -11,14 +11,28 @@ * granted to it by virtue of its status as an Intergovernmental Organization * or submit itself to any jurisdiction. */ + import { CommandHandler } from "../../../models/commands.model"; import { NewTokenCommand } from "./newToken.command"; import { ConnectionManager } from "../../ConnectionManager/ConnectionManager"; import { ConnectionDirection } from "../../../models/message.model"; +/** + * @description Handles the NewTokenCommand by updating or creating a connection with a new authentication token. + */ export class NewTokenHandler implements CommandHandler { + /** + * @param manager - Instance of ConnectionManager used to access and manage connections. + */ constructor(private manager: ConnectionManager) {} + /** + * @description Processes the NewTokenCommand by assigning a new token to the specified connection. + * If the connection does not exist, it is created. + * + * @param command - The new token event command. + * @throws Will throw an error if any of the required payload fields are missing. + */ async handle(command: NewTokenCommand): Promise { const { targetAddress, connectionDirection, token } = command.payload || {}; if (!targetAddress || !token || !connectionDirection) { diff --git a/Tokenization/backend/wrapper/src/client/Commands/revokeToken/revokeToken.command.ts b/Tokenization/backend/wrapper/src/client/Commands/revokeToken/revokeToken.command.ts index f92c99022..656bd6496 100644 --- a/Tokenization/backend/wrapper/src/client/Commands/revokeToken/revokeToken.command.ts +++ b/Tokenization/backend/wrapper/src/client/Commands/revokeToken/revokeToken.command.ts @@ -11,12 +11,16 @@ * granted to it by virtue of its status as an Intergovernmental Organization * or submit itself to any jurisdiction. */ + import { Command } from "../../../models/commands.model"; import { DuplexMessageEvent, TokenMessage, } from "../../../models/message.model"; +/** + * @description Command used to trigger token revocation for a specific connection. Handles structure logic. + */ export class RevokeTokenCommand implements Command { readonly event = DuplexMessageEvent.MESSAGE_EVENT_REVOKE_TOKEN; constructor(public payload: TokenMessage) {} diff --git a/Tokenization/backend/wrapper/src/client/Commands/revokeToken/revokeToken.handler.ts b/Tokenization/backend/wrapper/src/client/Commands/revokeToken/revokeToken.handler.ts index 2ca77a54f..af923fc36 100644 --- a/Tokenization/backend/wrapper/src/client/Commands/revokeToken/revokeToken.handler.ts +++ b/Tokenization/backend/wrapper/src/client/Commands/revokeToken/revokeToken.handler.ts @@ -11,13 +11,30 @@ * granted to it by virtue of its status as an Intergovernmental Organization * or submit itself to any jurisdiction. */ + import { CommandHandler } from "../../../models/commands.model"; import { RevokeTokenCommand } from "./revokeToken.command"; import { ConnectionManager } from "../../ConnectionManager/ConnectionManager"; +/** + * RevokeTokenHandler is responsible for handling the RevokeTokenCommand. + * It retrieves the target connection using the provided address and direction, + * and calls `handleRevokeToken()` on that connection if it exists. + */ export class RevokeTokenHandler implements CommandHandler { + /** + * Creates a new instance of RevokeTokenHandler. + * + * @param manager - The ConnectionManager used to retrieve active connections. + */ constructor(private manager: ConnectionManager) {} + /** + * Handles the RevokeTokenCommand by revoking the token on the target connection. + * + * @param command - The RevokeTokenCommand containing the target address and direction. + * @throws Will throw an error if the target address is missing in the command payload. + */ async handle(command: RevokeTokenCommand): Promise { const { targetAddress } = command.payload || {}; if (!targetAddress) { diff --git a/Tokenization/backend/wrapper/src/client/Connection/Connection.ts b/Tokenization/backend/wrapper/src/client/Connection/Connection.ts index 2347fa961..c0e4aa71f 100644 --- a/Tokenization/backend/wrapper/src/client/Connection/Connection.ts +++ b/Tokenization/backend/wrapper/src/client/Connection/Connection.ts @@ -11,6 +11,7 @@ * granted to it by virtue of its status as an Intergovernmental Organization * or submit itself to any jurisdiction. */ + import { ConnectionDirection } from "../../models/message.model"; import { ConnectionStatus } from "../../models/connection.model"; @@ -22,6 +23,13 @@ export class Connection { private targetAddress: string; private status: ConnectionStatus; + /** + * @description Creates a new Connection instance with the given token, target address, and connection direction. + * + * @param token - The authentication token for the connection. + * @param targetAddress - The unique address of the target client. + * @param direction - The direction of the connection (e.g., sending or receiving). + */ constructor( token: string, targetAddress: string, @@ -41,6 +49,9 @@ export class Connection { this.token = token; } + /** + * @description Revoke current token and set status of unauthorized connection + */ public handleRevokeToken(): void { this.token = ""; this.status = ConnectionStatus.UNAUTHORIZED; diff --git a/Tokenization/backend/wrapper/src/client/ConnectionManager/CentralConnection.ts b/Tokenization/backend/wrapper/src/client/ConnectionManager/CentralConnection.ts index 30cc02b96..f41a586fd 100644 --- a/Tokenization/backend/wrapper/src/client/ConnectionManager/CentralConnection.ts +++ b/Tokenization/backend/wrapper/src/client/ConnectionManager/CentralConnection.ts @@ -11,6 +11,7 @@ * granted to it by virtue of its status as an Intergovernmental Organization * or submit itself to any jurisdiction. */ + import * as grpc from "@grpc/grpc-js"; import { LogManager } from "@aliceo2/web-ui"; import { CentralCommandDispatcher } from "./EventManagement/CentralCommandDispatcher"; @@ -38,7 +39,7 @@ export class CentralConnection { this.stream = this.client.ClientStream(); this.stream!.on("data", (payload: DuplexMessageModel) => { - console.log("Received payload:", JSON.stringify(payload)); + this.logger.debugMessage(`Received payload: ${JSON.stringify(payload)}`); this.dispatcher.dispatch(payload); }); diff --git a/Tokenization/backend/wrapper/src/client/ConnectionManager/ConnectionManager.ts b/Tokenization/backend/wrapper/src/client/ConnectionManager/ConnectionManager.ts index c3d5bb45f..34e73cd74 100644 --- a/Tokenization/backend/wrapper/src/client/ConnectionManager/ConnectionManager.ts +++ b/Tokenization/backend/wrapper/src/client/ConnectionManager/ConnectionManager.ts @@ -11,6 +11,7 @@ * granted to it by virtue of its status as an Intergovernmental Organization * or submit itself to any jurisdiction. */ + import * as grpc from "@grpc/grpc-js"; import * as protoLoader from "@grpc/proto-loader"; import { CentralConnection } from "./CentralConnection"; @@ -36,6 +37,7 @@ import { * * @remarks * - `centralConnection`: Handles the duplex stream with the central gRPC server. + * - `centralDispatcher`: Dispatcher for central system events * - `sendingConnections`: Map of active outbound connections. * - `receivingConnections`: Map of active inbound connections. */ @@ -71,8 +73,8 @@ export class ConnectionManager { grpc.credentials.createInsecure() ); + // event dispatcher for central system events this.centralDispatcher = new CentralCommandDispatcher(); - this.centralConnection = new CentralConnection( client, this.centralDispatcher @@ -88,6 +90,10 @@ export class ConnectionManager { ); } + /** + * Registers new Command Handler for specific central event + * @param commandHandlers Array of event names and handler instances + */ registerCommandHandlers( commandHandlers: { event: DuplexMessageEvent; diff --git a/Tokenization/backend/wrapper/src/client/ConnectionManager/EventManagement/CentralCommandDispatcher.ts b/Tokenization/backend/wrapper/src/client/ConnectionManager/EventManagement/CentralCommandDispatcher.ts index 451f275b8..013f98132 100644 --- a/Tokenization/backend/wrapper/src/client/ConnectionManager/EventManagement/CentralCommandDispatcher.ts +++ b/Tokenization/backend/wrapper/src/client/ConnectionManager/EventManagement/CentralCommandDispatcher.ts @@ -16,10 +16,21 @@ import { LogManager } from "@aliceo2/web-ui"; import { Command, CommandHandler } from "models/commands.model"; import { DuplexMessageEvent } from "../../../models/message.model"; +/** + * CentralCommandDispatcher is responsible for registering and dispatching command handlers + * based on the command's event type. It acts as the central hub for routing incoming + * command messages coming from central system to the appropriate handler functions. + */ export class CentralCommandDispatcher { private handlers = new Map(); private logger = LogManager.getLogger("CentralCommandDispatcher"); + /** + * Registers a command handler for a specific command event type. + * + * @param event - The event type of the command to be handled. + * @param handler - The handler that should process commands of the given event type. + */ register( event: DuplexMessageEvent, handler: CommandHandler @@ -28,6 +39,12 @@ export class CentralCommandDispatcher { this.handlers.set(event, handler); } + /** + * Dispatches a command to the appropriate registered handler based on its event type. + * Logs warnings if no handler is found, and catches/logs errors during handler execution. + * + * @param command - The command object containing an event and its associated payload. + */ async dispatch(command: Command): Promise { const handler = this.handlers.get(command.event); this.logger.debugMessage(`Dispatching command: ${command.event}`); diff --git a/Tokenization/backend/wrapper/src/client/gRPCWrapper.ts b/Tokenization/backend/wrapper/src/client/gRPCWrapper.ts index 94655ef91..48000d61f 100644 --- a/Tokenization/backend/wrapper/src/client/gRPCWrapper.ts +++ b/Tokenization/backend/wrapper/src/client/gRPCWrapper.ts @@ -11,6 +11,7 @@ * granted to it by virtue of its status as an Intergovernmental Organization * or submit itself to any jurisdiction. */ + import path from "path"; import { ConnectionManager } from "./ConnectionManager/ConnectionManager"; import { RevokeTokenHandler } from "./Commands/revokeToken/revokeToken.handler"; @@ -28,7 +29,7 @@ import { NewTokenHandler } from "./Commands/newToken/newToken.handler"; * * @example * ```typescript - * const grpcWrapper = new gRPCWrapper(); + * const grpcWrapper = new gRPCWrapper(PROTO_PATH, CENTRAL_SYSTEM_ADDRESS); * // Use grpcWrapper to interact with gRPC services * ``` */ @@ -74,6 +75,9 @@ export class gRPCWrapper { return this.ConnectionManager.getAllConnections(); } + /** + * @returns Returns string with summary of all connection + */ public getSummary(): string { const conn = this.ConnectionManager.getAllConnections(); return ( diff --git a/Tokenization/backend/wrapper/src/models/commands.model.ts b/Tokenization/backend/wrapper/src/models/commands.model.ts index 874a301d4..1bba14108 100644 --- a/Tokenization/backend/wrapper/src/models/commands.model.ts +++ b/Tokenization/backend/wrapper/src/models/commands.model.ts @@ -15,17 +15,19 @@ import { DuplexMessageEvent } from "./message.model"; /** - * Interface representing a handler for processing events. - * - * @remarks - * The `handle` method receives an event object and performs the necessary processing. + * Interface representing a Command for specific event. */ - export interface Command { event: DuplexMessageEvent; payload: any; } +/** + * Interface representing a handler for processing events. + * + * @remarks + * The `handle` method receives an event object and performs the necessary processing. + */ export interface CommandHandler { handle(command: T): Promise; } diff --git a/Tokenization/backend/wrapper/src/models/connection.model.ts b/Tokenization/backend/wrapper/src/models/connection.model.ts index 474a2964c..b57432958 100644 --- a/Tokenization/backend/wrapper/src/models/connection.model.ts +++ b/Tokenization/backend/wrapper/src/models/connection.model.ts @@ -1,3 +1,16 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ export enum ConnectionStatus { // The connection is in the process of being established CONNECTING = "CONNECTING", diff --git a/Tokenization/backend/wrapper/src/test/central/CentralSystem.test.ts b/Tokenization/backend/wrapper/src/test/central/CentralSystem.test.ts index 093412940..3b85e4c6e 100644 --- a/Tokenization/backend/wrapper/src/test/central/CentralSystem.test.ts +++ b/Tokenization/backend/wrapper/src/test/central/CentralSystem.test.ts @@ -1,3 +1,17 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + const mockAddService = jest.fn(); const mockBindAsync = jest.fn(); const mockServerInstance = { diff --git a/Tokenization/backend/wrapper/src/test/client/Commands/newToken.test.ts b/Tokenization/backend/wrapper/src/test/client/Commands/newToken.test.ts new file mode 100644 index 000000000..91f109204 --- /dev/null +++ b/Tokenization/backend/wrapper/src/test/client/Commands/newToken.test.ts @@ -0,0 +1,154 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +import { NewTokenCommand } from "../../../client/Commands/newToken/newToken.command"; +import { NewTokenHandler } from "../../../client/Commands/newToken/newToken.handler"; +import { Connection } from "../../../client/Connection/Connection"; +import { ConnectionManager } from "../../../client/ConnectionManager/ConnectionManager"; +import { Command } from "models/commands.model"; +import { + ConnectionDirection, + DuplexMessageEvent, +} from "../../../models/message.model"; + +/** + * Helper to create a new token command with given address, direction, and token. + */ +function createEventMessage( + targetAddress: string, + connectionDirection: ConnectionDirection +): Command { + return { + event: DuplexMessageEvent.MESSAGE_EVENT_NEW_TOKEN, + payload: { + targetAddress, + connectionDirection, + token: "test-token", + }, + } as Command; +} + +describe("NewTokenHandler", () => { + let manager: ConnectionManager; + + beforeEach(() => { + manager = { + sendingConnections: new Map(), + receivingConnections: new Map(), + getConnectionByAddress: jest.fn(function ( + this: any, + address: string, + dir: ConnectionDirection + ) { + if (dir === ConnectionDirection.SENDING) { + return this.sendingConnections.get(address); + } else if (dir === ConnectionDirection.RECEIVING) { + return this.receivingConnections.get(address); + } + return undefined; + }), + createNewConnection: jest.fn(function ( + this: any, + address: string, + dir: ConnectionDirection, + token: string + ) { + const conn = new Connection(token, address, dir); + if (dir === ConnectionDirection.SENDING) { + this.sendingConnections.set(address, conn); + } else { + this.receivingConnections.set(address, conn); + } + return conn; + }), + } as unknown as ConnectionManager; + }); + + it("should update token on existing SENDING connection", async () => { + const targetAddress = "peer-123"; + const conn = new Connection( + "old-token", + targetAddress, + ConnectionDirection.SENDING + ); + (manager as any).sendingConnections.set(targetAddress, conn); + + const handler = new NewTokenHandler(manager); + const command = new NewTokenCommand( + createEventMessage(targetAddress, ConnectionDirection.SENDING).payload + ); + + await handler.handle(command); + + expect(conn.getToken()).toBe("test-token"); + }); + + it("should create new RECEIVING connection if not found", async () => { + const targetAddress = "peer-456"; + + const handler = new NewTokenHandler(manager); + const command = new NewTokenCommand( + createEventMessage(targetAddress, ConnectionDirection.RECEIVING).payload + ); + + await handler.handle(command); + + const conn = (manager as any).receivingConnections.get(targetAddress); + expect(conn).toBeDefined(); + expect(conn.getToken()).toBe("test-token"); + }); + + it("should handle DUPLEX direction by updating/creating both connections", async () => { + const targetAddress = "peer-789"; + + const handler = new NewTokenHandler(manager); + const command = new NewTokenCommand( + createEventMessage(targetAddress, ConnectionDirection.DUPLEX).payload + ); + + await handler.handle(command); + + const sendingConn = (manager as any).sendingConnections.get(targetAddress); + const receivingConn = (manager as any).receivingConnections.get( + targetAddress + ); + + expect(sendingConn).toBeDefined(); + expect(receivingConn).toBeDefined(); + expect(sendingConn.getToken()).toBe("test-token"); + expect(receivingConn.getToken()).toBe("test-token"); + }); + + it("should throw error when payload is missing required fields", async () => { + const invalidCommand = new NewTokenCommand({} as any); + + const handler = new NewTokenHandler(manager); + await expect(handler.handle(invalidCommand)).rejects.toThrow( + "Insufficient arguments. Expected: targetAddress, connectionDirection, token." + ); + }); + + it("should create command with correct event and payload", () => { + const payload = { + targetAddress: "peer-000", + connectionDirection: ConnectionDirection.SENDING, + token: "sample-token", + }; + + const command = new NewTokenCommand(payload); + + expect(command.event).toBe(DuplexMessageEvent.MESSAGE_EVENT_NEW_TOKEN); + expect(command.payload).toEqual(payload); + }); +}); diff --git a/Tokenization/backend/wrapper/src/test/client/ConnectionManager/index.ts b/Tokenization/backend/wrapper/src/test/client/ConnectionManager/index.ts index a2b34cde4..f60739154 100644 --- a/Tokenization/backend/wrapper/src/test/client/ConnectionManager/index.ts +++ b/Tokenization/backend/wrapper/src/test/client/ConnectionManager/index.ts @@ -1,3 +1,17 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + import * as grpc from "@grpc/grpc-js"; import { ConnectionManager } from "../../../client/ConnectionManager/ConnectionManager"; import { DuplexMessageEvent } from "../../../models/message.model"; diff --git a/Tokenization/backend/wrapper/src/utils/types/webui.d.ts b/Tokenization/backend/wrapper/src/utils/types/webui.d.ts index 1f2aeee51..1bf1cc574 100644 --- a/Tokenization/backend/wrapper/src/utils/types/webui.d.ts +++ b/Tokenization/backend/wrapper/src/utils/types/webui.d.ts @@ -1,3 +1,17 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. +*/ + declare module "@aliceo2/web-ui" { export const LogManager: { getLogger: (name: string) => { From 456276204c90512079a9232df291f94c63a12ebf Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Thu, 7 Aug 2025 14:59:28 +0200 Subject: [PATCH 43/81] fix: fixed banner processing. removed console logs --- Tokenization/backend/wrapper/scripts/banner.js | 11 ++++++++--- .../EventManagement/CentralCommandDispatcher.ts | 2 +- Tokenization/backend/wrapper/src/proto/wrapper.proto | 3 ++- .../backend/wrapper/src/utils/types/webui.d.ts | 2 +- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/Tokenization/backend/wrapper/scripts/banner.js b/Tokenization/backend/wrapper/scripts/banner.js index bbba4a30f..a8a9be9e2 100644 --- a/Tokenization/backend/wrapper/scripts/banner.js +++ b/Tokenization/backend/wrapper/scripts/banner.js @@ -27,14 +27,19 @@ const banner = `/** * In applying this license CERN does not waive the privileges and immunities * granted to it by virtue of its status as an Intergovernmental Organization * or submit itself to any jurisdiction. -*/ + */ `; const processFile = (filePath) => { try { const content = fs.readFileSync(filePath, "utf8"); - if (content.trim().startsWith(banner.trim())) { + if ( + content.includes(`@license`) && + content.includes( + `Copyright 2019-2020 CERN and copyright holders of ALICE O2.` + ) + ) { return; } @@ -56,7 +61,7 @@ const walkDir = (dir) => { if (file.isDirectory() && !excludedDirs.includes(fullPath)) { walkDir(fullPath); } else if (file.isFile()) { - if (/\.(js|ts|jsx|tsx|mjs|cjs)$/.test(file.name)) { + if (/\.(js|ts|jsx|tsx|mjs|cjs|proto)$/.test(file.name)) { processFile(fullPath); } } diff --git a/Tokenization/backend/wrapper/src/client/ConnectionManager/EventManagement/CentralCommandDispatcher.ts b/Tokenization/backend/wrapper/src/client/ConnectionManager/EventManagement/CentralCommandDispatcher.ts index 013f98132..3a433edbc 100644 --- a/Tokenization/backend/wrapper/src/client/ConnectionManager/EventManagement/CentralCommandDispatcher.ts +++ b/Tokenization/backend/wrapper/src/client/ConnectionManager/EventManagement/CentralCommandDispatcher.ts @@ -35,7 +35,7 @@ export class CentralCommandDispatcher { event: DuplexMessageEvent, handler: CommandHandler ): void { - console.log(`Registering handler for command type: ${event}`); + this.logger.infoMessage(`Registering handler for command type: ${event}`); this.handlers.set(event, handler); } diff --git a/Tokenization/backend/wrapper/src/proto/wrapper.proto b/Tokenization/backend/wrapper/src/proto/wrapper.proto index ad2cac80a..6688164da 100644 --- a/Tokenization/backend/wrapper/src/proto/wrapper.proto +++ b/Tokenization/backend/wrapper/src/proto/wrapper.proto @@ -10,7 +10,8 @@ * In applying this license CERN does not waive the privileges and immunities * granted to it by virtue of its status as an Intergovernmental Organization * or submit itself to any jurisdiction. -*/ + */ + syntax = "proto3"; package webui.tokenization; diff --git a/Tokenization/backend/wrapper/src/utils/types/webui.d.ts b/Tokenization/backend/wrapper/src/utils/types/webui.d.ts index 1bf1cc574..9432663e8 100644 --- a/Tokenization/backend/wrapper/src/utils/types/webui.d.ts +++ b/Tokenization/backend/wrapper/src/utils/types/webui.d.ts @@ -10,7 +10,7 @@ * In applying this license CERN does not waive the privileges and immunities * granted to it by virtue of its status as an Intergovernmental Organization * or submit itself to any jurisdiction. -*/ + */ declare module "@aliceo2/web-ui" { export const LogManager: { From 1fd05795fa99b7a4fa988f37ad330af5a41e563b Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Sat, 16 Aug 2025 13:32:11 +0200 Subject: [PATCH 44/81] fix: fix logging --- Tokenization/backend/wrapper/central/CentralSystem.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tokenization/backend/wrapper/central/CentralSystem.ts b/Tokenization/backend/wrapper/central/CentralSystem.ts index 350984751..f726203fb 100644 --- a/Tokenization/backend/wrapper/central/CentralSystem.ts +++ b/Tokenization/backend/wrapper/central/CentralSystem.ts @@ -67,7 +67,7 @@ export class CentralSystemWrapper { // Handle stream error event call.on("error", (err) => - this.logger.infoMessage( + this.logger.errorMessage( `Stream error from client ${call.getPeer()}:`, err ) @@ -84,7 +84,7 @@ export class CentralSystemWrapper { grpc.ServerCredentials.createInsecure(), (err, _port) => { if (err) { - this.logger.infoMessage("Server bind error:", err); + this.logger.errorMessage("Server bind error:", err); return; } this.logger.infoMessage(`CentralSytem started listening on ${addr}`); From 99530a8e5e48bd2fc1bc8478cbed1986e4fa79ee Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Sat, 16 Aug 2025 15:31:18 +0200 Subject: [PATCH 45/81] feat: change parameters to config objects --- .../backend/wrapper/central/CentralSystem.ts | 17 +++++++++-- .../backend/wrapper/client/gRPCWrapper.ts | 13 +++++++-- .../backend/wrapper/models/config.model.ts | 29 +++++++++++++++++++ 3 files changed, 53 insertions(+), 6 deletions(-) create mode 100644 Tokenization/backend/wrapper/models/config.model.ts diff --git a/Tokenization/backend/wrapper/central/CentralSystem.ts b/Tokenization/backend/wrapper/central/CentralSystem.ts index f726203fb..060944614 100644 --- a/Tokenization/backend/wrapper/central/CentralSystem.ts +++ b/Tokenization/backend/wrapper/central/CentralSystem.ts @@ -2,6 +2,7 @@ import * as grpc from "@grpc/grpc-js"; import * as protoLoader from "@grpc/proto-loader"; import path from "path"; import { LogManager } from "@aliceo2/web-ui"; +import { CentralSystemConfig } from "../models/config.model"; /** * @description Central System gRPC wrapper that manages client connections and handles gRPC streams with them. @@ -12,12 +13,18 @@ export class CentralSystemWrapper { // class properties private server: grpc.Server; + private protoPath: string; + private host: string; + private port: number; /** * Initializes the Wrapper for CentralSystem. * @param port The port number to bind the gRPC server to. */ - constructor(private protoPath: string, private port: number) { + constructor(config: CentralSystemConfig) { + this.protoPath = config.protoPath; + this.host = config.host || "0.0.0.0"; + this.port = config.port || 50051; this.server = new grpc.Server(); this.setupService(); } @@ -78,7 +85,7 @@ export class CentralSystemWrapper { * @desciprion Starts the gRPC server and binds it to the specified in class port. */ public listen() { - const addr = `localhost:${this.port}`; + const addr = `${this.host}:${this.port}`; this.server.bindAsync( addr, grpc.ServerCredentials.createInsecure(), @@ -95,6 +102,10 @@ export class CentralSystemWrapper { // Instantiate the CentralSystemWrapper on port 50051, but don't start automatically const PROTO_PATH = path.join(__dirname, "../proto/wrapper.proto"); -const centralSystem = new CentralSystemWrapper(PROTO_PATH, 50051); +const centralSystem = new CentralSystemWrapper({ + protoPath: PROTO_PATH, + host: "localhost", + port: 50051, +}); // Start listening explicitly centralSystem.listen(); diff --git a/Tokenization/backend/wrapper/client/gRPCWrapper.ts b/Tokenization/backend/wrapper/client/gRPCWrapper.ts index d77b8fa33..a70a4765d 100644 --- a/Tokenization/backend/wrapper/client/gRPCWrapper.ts +++ b/Tokenization/backend/wrapper/client/gRPCWrapper.ts @@ -1,5 +1,6 @@ import path from "path"; import { ConnectionManager } from "./ConnectionManager/ConnectionManager.ts"; +import { gRPCWrapperConfig } from "../models/config.model.ts"; /** * @description Wrapper class for managing secure gRPC wrapper. @@ -24,8 +25,11 @@ export class gRPCWrapper { * @param protoPath - The file path to the gRPC proto definition. * @param centralAddress - The address of the central gRPC server (default: "localhost:50051"). */ - constructor(protoPath: string, centralAddress: string = "localhost:50051") { - this.ConnectionManager = new ConnectionManager(protoPath, centralAddress); + constructor(config: gRPCWrapperConfig) { + this.ConnectionManager = new ConnectionManager( + config.protoPath, + config.centralAddress || "localhost" + ); } /** @@ -37,5 +41,8 @@ export class gRPCWrapper { } const PROTO_PATH = path.join(__dirname, "../proto/wrapper.proto"); -const grpc = new gRPCWrapper(PROTO_PATH, "localhost:50051"); +const grpc = new gRPCWrapper({ + protoPath: PROTO_PATH, + centralAddress: "localhost:50051", +}); grpc.connectToCentralSystem(); diff --git a/Tokenization/backend/wrapper/models/config.model.ts b/Tokenization/backend/wrapper/models/config.model.ts new file mode 100644 index 000000000..8583bd987 --- /dev/null +++ b/Tokenization/backend/wrapper/models/config.model.ts @@ -0,0 +1,29 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +export interface CentralSystemConfig { + /** Path to the proto file defining the services. */ + protoPath: string; + /** Host/IP to bind the gRPC server on. Defaults to "0.0.0.0" which is docker-friendly. */ + host?: string; + /** Port to bind. Defaults to 50051. */ + port?: number; +} + +export interface gRPCWrapperConfig { + /** Path to the proto file defining the services. */ + protoPath: string; + /** Address of the CentralSystem server. Defaults to "localhost". */ + centralAddress: string; +} From 5efb5ca064726fe9d9f950603036e35c858362b9 Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Sat, 16 Aug 2025 15:32:44 +0200 Subject: [PATCH 46/81] fix: fix deafult values in config --- Tokenization/backend/wrapper/client/gRPCWrapper.ts | 2 +- Tokenization/backend/wrapper/models/config.model.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Tokenization/backend/wrapper/client/gRPCWrapper.ts b/Tokenization/backend/wrapper/client/gRPCWrapper.ts index a70a4765d..b3aed93d7 100644 --- a/Tokenization/backend/wrapper/client/gRPCWrapper.ts +++ b/Tokenization/backend/wrapper/client/gRPCWrapper.ts @@ -28,7 +28,7 @@ export class gRPCWrapper { constructor(config: gRPCWrapperConfig) { this.ConnectionManager = new ConnectionManager( config.protoPath, - config.centralAddress || "localhost" + config.centralAddress ); } diff --git a/Tokenization/backend/wrapper/models/config.model.ts b/Tokenization/backend/wrapper/models/config.model.ts index 8583bd987..5f4c82a0f 100644 --- a/Tokenization/backend/wrapper/models/config.model.ts +++ b/Tokenization/backend/wrapper/models/config.model.ts @@ -24,6 +24,6 @@ export interface CentralSystemConfig { export interface gRPCWrapperConfig { /** Path to the proto file defining the services. */ protoPath: string; - /** Address of the CentralSystem server. Defaults to "localhost". */ + /** Address of the CentralSystem server. */ centralAddress: string; } From ec36841f5410b334548690d0fbd66823edf1e7be Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Sat, 16 Aug 2025 16:17:14 +0200 Subject: [PATCH 47/81] feat: add github actions for wrapper tests --- .github/workflows/grpc-wrapper.yml | 40 ++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 .github/workflows/grpc-wrapper.yml diff --git a/.github/workflows/grpc-wrapper.yml b/.github/workflows/grpc-wrapper.yml new file mode 100644 index 000000000..27efe6dfa --- /dev/null +++ b/.github/workflows/grpc-wrapper.yml @@ -0,0 +1,40 @@ +name: Grpc Wrapper + +on: + push: + branches: ["**"] + paths: + - "Tokenization/backend/wrapper/test/**" + - ".github/workflows/grpc-wrapper.yml" + pull_request: + branches: ["**"] + paths: + - "Tokenization/backend/wrapper/test/**" + - ".github/workflows/grpc-wrapper.yml" + +concurrency: + group: wrapper-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + runs-on: ubuntu-latest + + defaults: + run: + working-directory: Tokenization/backend/wrapper + + steps: + - uses: actions/checkout@v4 + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: "22.x" + cache: npm + cache-dependency-path: Tokenization/backend/wrapper/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Run Jest + run: npm run test From 836ff51e7d116a0685e563bf776b23aebfebf7a4 Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Sat, 16 Aug 2025 16:21:12 +0200 Subject: [PATCH 48/81] fix: paths --- .github/workflows/grpc-wrapper.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/grpc-wrapper.yml b/.github/workflows/grpc-wrapper.yml index 27efe6dfa..217034103 100644 --- a/.github/workflows/grpc-wrapper.yml +++ b/.github/workflows/grpc-wrapper.yml @@ -22,7 +22,7 @@ jobs: defaults: run: - working-directory: Tokenization/backend/wrapper + working-directory: "Tokenization/backend/wrapper" steps: - uses: actions/checkout@v4 @@ -31,7 +31,7 @@ jobs: with: node-version: "22.x" cache: npm - cache-dependency-path: Tokenization/backend/wrapper/package-lock.json + cache-dependency-path: "Tokenization/backend/wrapper/package-lock.json" - name: Install dependencies run: npm ci From c61ac97f8ccfea6ee8ff79db2a27127e60842d03 Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Sat, 16 Aug 2025 16:25:57 +0200 Subject: [PATCH 49/81] fix: pathing --- .github/workflows/grpc-wrapper.yml | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/.github/workflows/grpc-wrapper.yml b/.github/workflows/grpc-wrapper.yml index 217034103..8b27019e2 100644 --- a/.github/workflows/grpc-wrapper.yml +++ b/.github/workflows/grpc-wrapper.yml @@ -1,15 +1,10 @@ name: Grpc Wrapper on: - push: - branches: ["**"] - paths: - - "Tokenization/backend/wrapper/test/**" - - ".github/workflows/grpc-wrapper.yml" pull_request: branches: ["**"] paths: - - "Tokenization/backend/wrapper/test/**" + - "Tokenization/backend/wrapper/**" - ".github/workflows/grpc-wrapper.yml" concurrency: @@ -22,7 +17,7 @@ jobs: defaults: run: - working-directory: "Tokenization/backend/wrapper" + working-directory: Tokenization/backend/wrapper steps: - uses: actions/checkout@v4 @@ -30,8 +25,6 @@ jobs: uses: actions/setup-node@v4 with: node-version: "22.x" - cache: npm - cache-dependency-path: "Tokenization/backend/wrapper/package-lock.json" - name: Install dependencies run: npm ci From ec8ea1a4fa7216ee04646ecb89b92614a10bad5b Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Sat, 16 Aug 2025 16:38:50 +0200 Subject: [PATCH 50/81] fix: fix tests --- .../src/test/central/CentralSystem.test.ts | 14 +++++++++----- .../src/test/client/ConnectionManager/index.ts | 18 +++++++++++------- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/Tokenization/backend/wrapper/src/test/central/CentralSystem.test.ts b/Tokenization/backend/wrapper/src/test/central/CentralSystem.test.ts index 3b85e4c6e..82ad251ca 100644 --- a/Tokenization/backend/wrapper/src/test/central/CentralSystem.test.ts +++ b/Tokenization/backend/wrapper/src/test/central/CentralSystem.test.ts @@ -23,11 +23,15 @@ const logger = { infoMessage: jest.fn(), }; -jest.mock("@aliceo2/web-ui", () => ({ - LogManager: { - getLogger: () => logger, - }, -})); +jest.mock( + "@aliceo2/web-ui", + () => ({ + LogManager: { + getLogger: () => logger, + }, + }), + { virtual: true } +); jest.mock("@grpc/proto-loader", () => ({ loadSync: jest.fn(() => { diff --git a/Tokenization/backend/wrapper/src/test/client/ConnectionManager/index.ts b/Tokenization/backend/wrapper/src/test/client/ConnectionManager/index.ts index f60739154..1da1fb06f 100644 --- a/Tokenization/backend/wrapper/src/test/client/ConnectionManager/index.ts +++ b/Tokenization/backend/wrapper/src/test/client/ConnectionManager/index.ts @@ -42,13 +42,17 @@ jest.mock( ); // Mock logger -jest.mock("@aliceo2/web-ui", () => ({ - LogManager: { - getLogger: () => ({ - infoMessage: jest.fn(), - }), - }, -})); +jest.mock( + "@aliceo2/web-ui", + () => ({ + LogManager: { + getLogger: () => ({ + infoMessage: jest.fn(), + }), + }, + }), + { virtual: true } +); // Mock gRPC proto loader and client jest.mock("@grpc/proto-loader", () => ({ From 6ac9f712412e77caf0b264567c23b35535a80b91 Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Wed, 27 Aug 2025 14:41:37 +0200 Subject: [PATCH 51/81] feat: implement simple connection peer to peers --- Tokenization/backend/wrapper/.gitignore | 3 +- .../backend/wrapper/package-lock.json | 850 +++++++++++++++++- Tokenization/backend/wrapper/package.json | 4 +- .../wrapper/src/central/CentralSystem.ts | 39 +- .../Commands/newToken/newToken.handler.ts | 6 +- .../src/client/Connection/Connection.ts | 86 +- .../ConnectionManager/ConnectionManager.ts | 136 ++- .../backend/wrapper/src/client/gRPCWrapper.ts | 67 +- .../wrapper/src/models/connection.model.ts | 32 + .../backend/wrapper/src/proto/wrapper.proto | 32 + 10 files changed, 1187 insertions(+), 68 deletions(-) diff --git a/Tokenization/backend/wrapper/.gitignore b/Tokenization/backend/wrapper/.gitignore index 763301fc0..d90c70c7a 100644 --- a/Tokenization/backend/wrapper/.gitignore +++ b/Tokenization/backend/wrapper/.gitignore @@ -1,2 +1,3 @@ dist/ -node_modules/ \ No newline at end of file +node_modules/ +run_tests/ \ No newline at end of file diff --git a/Tokenization/backend/wrapper/package-lock.json b/Tokenization/backend/wrapper/package-lock.json index 1a24c0038..cd3e9595a 100644 --- a/Tokenization/backend/wrapper/package-lock.json +++ b/Tokenization/backend/wrapper/package-lock.json @@ -10,10 +10,12 @@ "dependencies": { "@grpc/grpc-js": "^1.13.4", "@grpc/proto-loader": "^0.7.15", + "express": "^5.1.0", "ts-node": "^10.9.2", "typescript": "^5.8.3" }, "devDependencies": { + "@types/express": "^5.0.3", "@types/jest": "^29.5.14", "jest": "^29.7.0", "ts-jest": "^29.4.0", @@ -1151,6 +1153,52 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.3.tgz", + "integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.7.tgz", + "integrity": "sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -1161,6 +1209,13 @@ "@types/node": "*" } }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -1199,6 +1254,13 @@ "pretty-format": "^29.0.0" } }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "24.0.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.0.tgz", @@ -1208,6 +1270,43 @@ "undici-types": "~7.8.0" } }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "0.17.5", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", + "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz", + "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -1232,6 +1331,19 @@ "dev": true, "license": "MIT" }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -1479,6 +1591,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -1566,6 +1698,44 @@ "dev": true, "license": "MIT" }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -1749,6 +1919,27 @@ "dev": true, "license": "MIT" }, + "node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -1756,6 +1947,24 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, "node_modules/create-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", @@ -1803,7 +2012,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -1842,6 +2050,15 @@ "node": ">=0.10.0" } }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -1884,6 +2101,26 @@ "node": ">=8" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, "node_modules/ejs": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", @@ -1926,6 +2163,15 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "license": "MIT" }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -1936,6 +2182,36 @@ "is-arrayish": "^0.2.1" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -1945,6 +2221,12 @@ "node": ">=6" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, "node_modules/esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", @@ -1959,6 +2241,15 @@ "node": ">=4" } }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -2009,6 +2300,48 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -2099,6 +2432,23 @@ "node": ">=8" } }, + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/find-up": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", @@ -2113,6 +2463,24 @@ "node": ">=8" } }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -2139,7 +2507,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -2164,6 +2531,30 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/get-package-type": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", @@ -2174,6 +2565,19 @@ "node": ">=8.0.0" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -2266,6 +2670,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -2283,11 +2699,22 @@ "node": ">=8" } }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -2303,6 +2730,31 @@ "dev": true, "license": "MIT" }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -2313,6 +2765,18 @@ "node": ">=10.17.0" } }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -2369,9 +2833,17 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, "license": "ISC" }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -2460,6 +2932,12 @@ "node": ">=0.12.0" } }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -3347,6 +3825,36 @@ "tmpl": "1.0.5" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -3378,6 +3886,27 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -3405,7 +3934,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/mylas": { @@ -3429,6 +3957,15 @@ "dev": true, "license": "MIT" }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -3466,11 +4003,34 @@ "node": ">=8" } }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -3566,6 +4126,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -3603,6 +4172,15 @@ "dev": true, "license": "MIT" }, + "node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -3735,6 +4313,19 @@ "node": ">=12.0.0" } }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/pure-rand": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", @@ -3752,6 +4343,21 @@ ], "license": "MIT" }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/queue-lit": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/queue-lit/-/queue-lit-1.5.2.tgz", @@ -3783,6 +4389,30 @@ ], "license": "MIT" }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -3887,6 +4517,22 @@ "node": ">=0.10.0" } }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -3911,6 +4557,32 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -3921,6 +4593,49 @@ "semver": "bin/semver.js" } }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -3944,6 +4659,78 @@ "node": ">=8" } }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -4019,6 +4806,15 @@ "node": ">=8" } }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -4153,6 +4949,15 @@ "node": ">=8.0" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, "node_modules/ts-jest": { "version": "29.4.0", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.0.tgz", @@ -4320,6 +5125,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/typescript": { "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", @@ -4339,6 +5158,15 @@ "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", "license": "MIT" }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/update-browserslist-db": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", @@ -4391,6 +5219,15 @@ "node": ">=10.12.0" } }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", @@ -4438,7 +5275,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, "license": "ISC" }, "node_modules/write-file-atomic": { diff --git a/Tokenization/backend/wrapper/package.json b/Tokenization/backend/wrapper/package.json index 0c420b84c..e099eeaab 100644 --- a/Tokenization/backend/wrapper/package.json +++ b/Tokenization/backend/wrapper/package.json @@ -5,12 +5,11 @@ "scripts": { "test": "jest", "build": "tsc -p tsconfig.build.json && cp -r src/proto dist", - "client": "node dist/client/gRPCWrapper.js", - "central": "node dist/central/CentralSystem.js", "process-banners": "node scripts/banner.js" }, "author": "ALICEO2", "devDependencies": { + "@types/express": "^5.0.3", "@types/jest": "^29.5.14", "jest": "^29.7.0", "ts-jest": "^29.4.0", @@ -19,6 +18,7 @@ "dependencies": { "@grpc/grpc-js": "^1.13.4", "@grpc/proto-loader": "^0.7.15", + "express": "^5.1.0", "ts-node": "^10.9.2", "typescript": "^5.8.3" } diff --git a/Tokenization/backend/wrapper/src/central/CentralSystem.ts b/Tokenization/backend/wrapper/src/central/CentralSystem.ts index 58d938eed..5c5da23d4 100644 --- a/Tokenization/backend/wrapper/src/central/CentralSystem.ts +++ b/Tokenization/backend/wrapper/src/central/CentralSystem.ts @@ -14,13 +14,8 @@ import * as grpc from "@grpc/grpc-js"; import * as protoLoader from "@grpc/proto-loader"; -import path from "path"; import { LogManager } from "@aliceo2/web-ui"; -import { - ConnectionDirection, - DuplexMessageEvent, - DuplexMessageModel, -} from "../models/message.model"; +import { DuplexMessageModel } from "../models/message.model"; /** * @description Central System gRPC wrapper that manages client connections and handles gRPC streams with them. @@ -185,35 +180,3 @@ export class CentralSystemWrapper { ); } } - -// Instantiate the CentralSystemWrapper on port 50051, but don't start automatically -const PROTO_PATH = path.join(__dirname, "../proto/wrapper.proto"); -const centralSystem = new CentralSystemWrapper(PROTO_PATH, 50051); -// Start listening explicitly -centralSystem.listen(); - -setTimeout(() => { - centralSystem.sendEvent(centralSystem.getConnectedClients()[0], { - event: DuplexMessageEvent.MESSAGE_EVENT_REVOKE_TOKEN, - payload: { - connectionDirection: ConnectionDirection.SENDING, - targetAddress: "a", - }, - }); - centralSystem.sendEvent(centralSystem.getConnectedClients()[0], { - event: DuplexMessageEvent.MESSAGE_EVENT_NEW_TOKEN, - payload: { - connectionDirection: ConnectionDirection.SENDING, - targetAddress: "a", - token: "newToken", - }, - }); - centralSystem.sendEvent(centralSystem.getConnectedClients()[0], { - event: DuplexMessageEvent.MESSAGE_EVENT_NEW_TOKEN, - payload: { - connectionDirection: ConnectionDirection.SENDING, - targetAddress: "c", - token: "tokenForNewAddress", - }, - }); -}, 5000); diff --git a/Tokenization/backend/wrapper/src/client/Commands/newToken/newToken.handler.ts b/Tokenization/backend/wrapper/src/client/Commands/newToken/newToken.handler.ts index cac108af3..feade4424 100644 --- a/Tokenization/backend/wrapper/src/client/Commands/newToken/newToken.handler.ts +++ b/Tokenization/backend/wrapper/src/client/Commands/newToken/newToken.handler.ts @@ -49,7 +49,11 @@ export class NewTokenHandler implements CommandHandler { for (const dir of directions) { let conn = this.manager.getConnectionByAddress(targetAddress, dir); if (!conn) { - conn = this.manager.createNewConnection(targetAddress, dir, token); + conn = await this.manager.createNewConnection( + targetAddress, + dir, + token + ); } conn.handleNewToken(token); } diff --git a/Tokenization/backend/wrapper/src/client/Connection/Connection.ts b/Tokenization/backend/wrapper/src/client/Connection/Connection.ts index c0e4aa71f..54f0e075f 100644 --- a/Tokenization/backend/wrapper/src/client/Connection/Connection.ts +++ b/Tokenization/backend/wrapper/src/client/Connection/Connection.ts @@ -13,15 +13,24 @@ */ import { ConnectionDirection } from "../../models/message.model"; -import { ConnectionStatus } from "../../models/connection.model"; +import { + ConnectionHeaders, + ConnectionStatus, + FetchOptions, + FetchResponse, +} from "../../models/connection.model"; +import * as grpc from "@grpc/grpc-js"; /** * @description This class represents a connection to a target client and manages sending messages to it. */ export class Connection { private token: string; - private targetAddress: string; private status: ConnectionStatus; + private peerClient?: any; // a client grpc connection instance + + public targetAddress: string; + public direction: ConnectionDirection; /** * @description Creates a new Connection instance with the given token, target address, and connection direction. @@ -33,10 +42,17 @@ export class Connection { constructor( token: string, targetAddress: string, - public direction: ConnectionDirection + direction: ConnectionDirection, + peerCtor: any ) { this.token = token; this.targetAddress = targetAddress; + this.direction = direction; + + this.peerClient = new peerCtor( + targetAddress, + grpc.credentials.createInsecure() + ); this.status = ConnectionStatus.CONNECTED; } @@ -73,6 +89,14 @@ export class Connection { return this.status; } + /** + * @description Updates the status of the connection. + * @param status New status + */ + public updateStatus(status: ConnectionStatus): void { + this.status = status; + } + /** * @description Returns target address for this Connection object * @returns Target address @@ -80,4 +104,60 @@ export class Connection { public getTargetAddress(): string { return this.targetAddress; } + + /** + * @description Attaches gRPC client to that connection + */ + public attachGrpcClient(client: any): void { + this.peerClient = client; + } + + /** + * @description "HTTP-like" fetch via gRPC protocol + * @returns Promise with peer's response + */ + public fetch(options: FetchOptions = {}): Promise { + if (!this.peerClient) { + return Promise.reject( + new Error(`Peer client not attached for ${this.getTargetAddress()}`) + ); + } + + // build a request object + const method = (options.method || "POST").toUpperCase(); + const path = options.path || "/"; + const headers: ConnectionHeaders = { ...(options.headers || {}) }; + + let bodyBuf: Buffer = Buffer.alloc(0); + const b = options.body; + if (b != null) { + if (Buffer.isBuffer(b)) bodyBuf = b; + else if (b instanceof Uint8Array) bodyBuf = Buffer.from(b); + else if (typeof b === "string") bodyBuf = Buffer.from(b, "utf8"); + else + return Promise.reject( + new Error("Body must be a string/Buffer/Uint8Array") + ); + } + + const req = { method, path, headers, body: bodyBuf }; + + // return promise with response + return new Promise((resolve, reject) => { + this.peerClient.Fetch(req, (err: any, resp: any) => { + if (err) return reject(err); + + const resBody = resp?.body ? Buffer.from(resp.body) : Buffer.alloc(0); + const fetchResponse: FetchResponse = { + status: Number(resp?.status ?? 200), + headers: resp?.headers || {}, + body: resBody, + text: async () => resBody.toString("utf8"), + json: async () => JSON.parse(resBody.toString("utf8")), + }; + + resolve(fetchResponse); + }); + }); + } } diff --git a/Tokenization/backend/wrapper/src/client/ConnectionManager/ConnectionManager.ts b/Tokenization/backend/wrapper/src/client/ConnectionManager/ConnectionManager.ts index 34e73cd74..807ae4088 100644 --- a/Tokenization/backend/wrapper/src/client/ConnectionManager/ConnectionManager.ts +++ b/Tokenization/backend/wrapper/src/client/ConnectionManager/ConnectionManager.ts @@ -23,6 +23,7 @@ import { ConnectionDirection, DuplexMessageEvent, } from "../../models/message.model"; +import { ConnectionStatus } from "../../models/connection.model"; /** * @description Manages all the connection between clients and central system. @@ -43,10 +44,16 @@ import { */ export class ConnectionManager { private logger = LogManager.getLogger("ConnectionManager"); + private wrapper: any; // gRPC wrapper file + private centralDispatcher: CentralCommandDispatcher; private centralConnection: CentralConnection; private sendingConnections = new Map(); + private receivingConnections = new Map(); + private peerCtor: any; // p2p gRPC constructor + private peerServer?: grpc.Server; + private baseAPIPath: string = "localhost:40041/api/"; /** * @description Initializes a new instance of the ConnectionManager class. @@ -66,9 +73,10 @@ export class ConnectionManager { }); const proto = grpc.loadPackageDefinition(packageDef) as any; - const wrapper = proto.webui.tokenization; + this.wrapper = proto.webui.tokenization; + this.peerCtor = this.wrapper.Peer2Peer; - const client = new wrapper.CentralSystem( + const centralClient = new this.wrapper.CentralSystem( centralAddress, grpc.credentials.createInsecure() ); @@ -76,17 +84,17 @@ export class ConnectionManager { // event dispatcher for central system events this.centralDispatcher = new CentralCommandDispatcher(); this.centralConnection = new CentralConnection( - client, + centralClient, this.centralDispatcher ); this.sendingConnections.set( "a", - new Connection("1", "a", ConnectionDirection.SENDING) + new Connection("1", "a", ConnectionDirection.SENDING, this.peerCtor) ); this.sendingConnections.set( "b", - new Connection("2", "b", ConnectionDirection.SENDING) + new Connection("2", "b", ConnectionDirection.SENDING, this.peerCtor) ); } @@ -125,18 +133,40 @@ export class ConnectionManager { * @param direction Direction of connection * @param token Optional token for connection */ - createNewConnection( + public async createNewConnection( address: string, direction: ConnectionDirection, token?: string ) { - const conn = new Connection(token || "", address, direction); + let conn: Connection | undefined; + + // Checks if connection already exists + conn = + direction === ConnectionDirection.RECEIVING + ? this.receivingConnections.get(address) + : this.sendingConnections.get(address); + + // Return existing connection if found + if (conn) { + if (token) { + conn.handleNewToken(token); + } + return conn; + } + + // Create new connection + conn = new Connection(token || "", address, direction, this.peerCtor); + conn.updateStatus(ConnectionStatus.CONNECTING); if (direction === ConnectionDirection.RECEIVING) { this.receivingConnections.set(address, conn); } else { this.sendingConnections.set(address, conn); } + conn.updateStatus(ConnectionStatus.CONNECTED); + this.logger.infoMessage( + `Connection with ${address} has been estabilished. Status: ${conn.getStatus()}` + ); return conn; } @@ -173,4 +203,96 @@ export class ConnectionManager { receiving: [...this.receivingConnections.values()], }; } + + /** Starts a listener server for p2p connections */ + public async listenForPeers( + port: number, + baseAPIPath?: string + ): Promise { + if (baseAPIPath) this.baseAPIPath = baseAPIPath; + + if (this.peerServer) { + this.peerServer.forceShutdown(); + this.peerServer = undefined; + } + + this.peerServer = new grpc.Server(); + this.peerServer.addService(this.wrapper.Peer2Peer.service, { + Fetch: async ( + call: grpc.ServerUnaryCall, + callback: grpc.sendUnaryData + ) => { + try { + const clientAddress = call.getPeer(); + this.logger.infoMessage(`Incoming request from ${clientAddress}`); + + let conn: Connection | undefined = + this.receivingConnections.get(clientAddress); + + if (!conn) { + conn = new Connection( + "", + clientAddress, + ConnectionDirection.RECEIVING, + this.peerCtor + ); + conn.updateStatus(ConnectionStatus.CONNECTED); + this.receivingConnections.set(clientAddress, conn); + this.logger.infoMessage( + `New incoming connection registered for: ${clientAddress}` + ); + } + + // create request to forward to local API endpoint + const method = String(call.request?.method || "POST").toUpperCase(); + const url = this.baseAPIPath + (call.request?.path || ""); + const headers: { [key: string]: string } = call.request?.headers; + const body = call.request?.body + ? Buffer.from(call.request.body).toString("utf-8") + : undefined; + + this.logger.infoMessage( + `Received payload from ${clientAddress}: \n${url}\n${JSON.stringify( + headers + )}\n${JSON.stringify(body)}\n` + ); + + const httpResp = await fetch(url, { + method, + headers: headers, + body, + }); + + const respHeaders: Record = {}; + httpResp.headers.forEach((v, k) => (respHeaders[k] = v)); + const resBody = Buffer.from(await httpResp.arrayBuffer()); + + callback(null, { + status: httpResp.status, + headers: respHeaders, + body: resBody, + }); + } catch (e: any) { + this.logger.errorMessage( + `Error forwarding request: ${e ?? "Uknown error"}` + ); + + callback({ + code: grpc.status.INTERNAL, + message: e?.message ?? "forward error", + } as any); + } + }, + }); + + await new Promise((resolve, reject) => { + this.peerServer!.bindAsync( + `localhost:${port}`, + grpc.ServerCredentials.createInsecure(), + (err) => (err ? reject(err) : resolve()) + ); + }); + + this.logger.infoMessage(`Peer server listening on localhost:${port}`); + } } diff --git a/Tokenization/backend/wrapper/src/client/gRPCWrapper.ts b/Tokenization/backend/wrapper/src/client/gRPCWrapper.ts index 48000d61f..05eca013e 100644 --- a/Tokenization/backend/wrapper/src/client/gRPCWrapper.ts +++ b/Tokenization/backend/wrapper/src/client/gRPCWrapper.ts @@ -15,7 +15,10 @@ import path from "path"; import { ConnectionManager } from "./ConnectionManager/ConnectionManager"; import { RevokeTokenHandler } from "./Commands/revokeToken/revokeToken.handler"; -import { DuplexMessageEvent } from "../models/message.model"; +import { + ConnectionDirection, + DuplexMessageEvent, +} from "../models/message.model"; import { Connection } from "./Connection/Connection"; import { NewTokenHandler } from "./Commands/newToken/newToken.handler"; @@ -63,6 +66,30 @@ export class gRPCWrapper { this.ConnectionManager.connectToCentralSystem(); } + /** + * @description Starts the Connection Manager stream connection with Central System + */ + public async connectToClient( + address: string, + token?: string + ): Promise { + return this.ConnectionManager.createNewConnection( + address, + ConnectionDirection.SENDING, + token || "" + ); + } + + /** + * @description Starts the Connection Manager stream connection with Central System + */ + public async listenForPeers( + port: number, + baseAPIPath?: string + ): Promise { + return this.ConnectionManager.listenForPeers(port, baseAPIPath); + } + /** * @description Returns all saved connections. * @@ -104,12 +131,34 @@ export class gRPCWrapper { } } -const PROTO_PATH = path.join(__dirname, "../proto/wrapper.proto"); -const grpc = new gRPCWrapper(PROTO_PATH, "localhost:50051"); -grpc.connectToCentralSystem(); -console.log(grpc.getSummary()); +// const PROTO_PATH = path.join(__dirname, "../proto/wrapper.proto"); +// const grpc = new gRPCWrapper(PROTO_PATH, "localhost:50051"); +// grpc.connectToCentralSystem(); +// console.log(grpc.getSummary()); + +// setTimeout(() => { +// console.log("New status after 10 seconds, token revokation and new token:"); +// console.log(grpc.getSummary()); +// }, 10000); + +// grpc.connectToCentralSystem(); + +// const conn1: Connection = grpc.connectToClient("localhost:40001"); -setTimeout(() => { - console.log("New status after 10 seconds, token revokation and new token:"); - console.log(grpc.getSummary()); -}, 10000); +// // wrapping request +// conn1 +// .fetch({ +// method: "POST", +// headers: { +// "Content-Type": "application/json", +// Authorization: "bearer someToken", +// }, +// body: JSON.stringify({ +// name: "Jan Kowalski", +// email: "jan.kowalski@example.com", +// age: 28, +// }), +// }) +// .then((response) => response.json()) +// .then((data) => console.log("Response:", data)) +// .catch((error) => console.error("Error:", error)); diff --git a/Tokenization/backend/wrapper/src/models/connection.model.ts b/Tokenization/backend/wrapper/src/models/connection.model.ts index b57432958..f18c24fa4 100644 --- a/Tokenization/backend/wrapper/src/models/connection.model.ts +++ b/Tokenization/backend/wrapper/src/models/connection.model.ts @@ -28,3 +28,35 @@ export enum ConnectionStatus { // The connection is refreshing its authentication token TOKEN_REFRESH = "TOKEN_REFRESH", } + +export type ConnectionHeaders = Record; + +export type FetchOptions = { + method?: string; + path?: string; + headers?: ConnectionHeaders; + body?: string | Buffer | Uint8Array | null; +}; + +export type FetchResponse = { + status: number; + headers: ConnectionHeaders; + body: Buffer; + text: () => Promise; + json: () => Promise; +}; + +export type HttpLikeRequest = { + method: string; + path: string; + headers: Headers; + body: Buffer; + correlation_id?: string; + sequence_number?: number; +}; + +export type HttpLikeResponse = { + status: number; + headers: Headers; + body: Buffer; +}; diff --git a/Tokenization/backend/wrapper/src/proto/wrapper.proto b/Tokenization/backend/wrapper/src/proto/wrapper.proto index 6688164da..abe296b59 100644 --- a/Tokenization/backend/wrapper/src/proto/wrapper.proto +++ b/Tokenization/backend/wrapper/src/proto/wrapper.proto @@ -25,6 +25,12 @@ service CentralSystem { rpc ClientStream(stream Payload) returns (stream Payload); } +// Peer2Peer service handling HTTP-like requests between wrapper clients +service Peer2Peer { + rpc Fetch (HttpLikeRequest) returns (HttpLikeResponse); +} + + // ====================================== // MESSAGES // ====================================== @@ -51,6 +57,32 @@ message Payload { } } +// http method enum +enum HttpMethod { + HTTP_METHOD_UNSPECIFIED = 0; + GET = 1; + POST = 2; + PUT = 3; + PATCH = 4; + DELETE = 5; + HEAD = 6; + OPTIONS = 7; +} + +message HttpLikeRequest { + HttpMethod method = 1; // GET/POST/... + string path = 2; // request path e.g. "/orders/add" + map headers = 3; // "content-type": "application/json" + bytes body = 4; // body (e.g. JSON) +} + +message HttpLikeResponse { + int32 status = 1; + map headers = 2; + bytes body = 3; +} + + // ====================================== // ENUMS // ====================================== From 5b759d80ada2058960a1cd0039e871eb59da1ca0 Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Thu, 28 Aug 2025 21:46:07 +0200 Subject: [PATCH 52/81] fix: refactor ConnectionManager and remove unnecessary code. --- Tokenization/backend/wrapper/.gitignore | 3 + ...ntralSystem.ts => CentralSystemWrapper.ts} | 15 +--- .../ConnectionManager/ConnectionManager.ts | 2 +- .../backend/wrapper/client/gRPCWrapper.ts | 7 -- ...m.test.ts => CentralSystemWrapper.test.ts} | 7 +- .../test/utils/serialization.utils.test.ts | 71 ------------------- .../wrapper/utils/serialization.utils.ts | 68 ------------------ 7 files changed, 11 insertions(+), 162 deletions(-) create mode 100644 Tokenization/backend/wrapper/.gitignore rename Tokenization/backend/wrapper/central/{CentralSystem.ts => CentralSystemWrapper.ts} (86%) rename Tokenization/backend/wrapper/test/central/{CentralSystem.test.ts => CentralSystemWrapper.test.ts} (96%) delete mode 100644 Tokenization/backend/wrapper/test/utils/serialization.utils.test.ts delete mode 100644 Tokenization/backend/wrapper/utils/serialization.utils.ts diff --git a/Tokenization/backend/wrapper/.gitignore b/Tokenization/backend/wrapper/.gitignore new file mode 100644 index 000000000..d90c70c7a --- /dev/null +++ b/Tokenization/backend/wrapper/.gitignore @@ -0,0 +1,3 @@ +dist/ +node_modules/ +run_tests/ \ No newline at end of file diff --git a/Tokenization/backend/wrapper/central/CentralSystem.ts b/Tokenization/backend/wrapper/central/CentralSystemWrapper.ts similarity index 86% rename from Tokenization/backend/wrapper/central/CentralSystem.ts rename to Tokenization/backend/wrapper/central/CentralSystemWrapper.ts index 060944614..353450633 100644 --- a/Tokenization/backend/wrapper/central/CentralSystem.ts +++ b/Tokenization/backend/wrapper/central/CentralSystemWrapper.ts @@ -75,8 +75,7 @@ export class CentralSystemWrapper { // Handle stream error event call.on("error", (err) => this.logger.errorMessage( - `Stream error from client ${call.getPeer()}:`, - err + `Stream error from client ${call.getPeer()}: ${err}` ) ); } @@ -91,7 +90,7 @@ export class CentralSystemWrapper { grpc.ServerCredentials.createInsecure(), (err, _port) => { if (err) { - this.logger.errorMessage("Server bind error:", err); + this.logger.errorMessage(`Server bind error: ${err}`); return; } this.logger.infoMessage(`CentralSytem started listening on ${addr}`); @@ -99,13 +98,3 @@ export class CentralSystemWrapper { ); } } - -// Instantiate the CentralSystemWrapper on port 50051, but don't start automatically -const PROTO_PATH = path.join(__dirname, "../proto/wrapper.proto"); -const centralSystem = new CentralSystemWrapper({ - protoPath: PROTO_PATH, - host: "localhost", - port: 50051, -}); -// Start listening explicitly -centralSystem.listen(); diff --git a/Tokenization/backend/wrapper/client/ConnectionManager/ConnectionManager.ts b/Tokenization/backend/wrapper/client/ConnectionManager/ConnectionManager.ts index ed6ddb48b..d37586c9c 100644 --- a/Tokenization/backend/wrapper/client/ConnectionManager/ConnectionManager.ts +++ b/Tokenization/backend/wrapper/client/ConnectionManager/ConnectionManager.ts @@ -122,8 +122,8 @@ export class ConnectionManager { if (this.stream) { this.stream.end(); this.stream = undefined; + this.logger.infoMessage(`Disconnected from CentralSystem service`); } this.reconnectAttempts = 0; - this.logger.infoMessage(`Disconnected from CentralSystem service`); } } diff --git a/Tokenization/backend/wrapper/client/gRPCWrapper.ts b/Tokenization/backend/wrapper/client/gRPCWrapper.ts index b3aed93d7..7a4b8e796 100644 --- a/Tokenization/backend/wrapper/client/gRPCWrapper.ts +++ b/Tokenization/backend/wrapper/client/gRPCWrapper.ts @@ -39,10 +39,3 @@ export class gRPCWrapper { this.ConnectionManager.connectToCentralSystem(); } } - -const PROTO_PATH = path.join(__dirname, "../proto/wrapper.proto"); -const grpc = new gRPCWrapper({ - protoPath: PROTO_PATH, - centralAddress: "localhost:50051", -}); -grpc.connectToCentralSystem(); diff --git a/Tokenization/backend/wrapper/test/central/CentralSystem.test.ts b/Tokenization/backend/wrapper/test/central/CentralSystemWrapper.test.ts similarity index 96% rename from Tokenization/backend/wrapper/test/central/CentralSystem.test.ts rename to Tokenization/backend/wrapper/test/central/CentralSystemWrapper.test.ts index 1599e0ccd..f40da83f6 100644 --- a/Tokenization/backend/wrapper/test/central/CentralSystem.test.ts +++ b/Tokenization/backend/wrapper/test/central/CentralSystemWrapper.test.ts @@ -41,7 +41,7 @@ jest.mock("@grpc/grpc-js", () => { }; }); -import { CentralSystemWrapper } from "../../central/CentralSystem"; +import { CentralSystemWrapper } from "../../central/CentralSystemWrapper"; import * as grpc from "@grpc/grpc-js"; describe("CentralSystemWrapper", () => { @@ -49,7 +49,10 @@ describe("CentralSystemWrapper", () => { beforeEach(() => { jest.clearAllMocks(); - wrapper = new CentralSystemWrapper("dummy.proto", 12345); + wrapper = new CentralSystemWrapper({ + protoPath: "dummy.proto", + port: 12345, + }); }); test("should set up gRPC service and add it to the server", () => { diff --git a/Tokenization/backend/wrapper/test/utils/serialization.utils.test.ts b/Tokenization/backend/wrapper/test/utils/serialization.utils.test.ts deleted file mode 100644 index 93d5c7d7c..000000000 --- a/Tokenization/backend/wrapper/test/utils/serialization.utils.test.ts +++ /dev/null @@ -1,71 +0,0 @@ -/** - * @license - * Copyright 2019-2020 CERN and copyright holders of ALICE O2. - * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. - * All rights not expressly granted are reserved. - * - * This software is distributed under the terms of the GNU General Public - * License v3 (GPL Version 3), copied verbatim in the file "COPYING". - * - * In applying this license CERN does not waive the privileges and immunities - * granted to it by virtue of its status as an Intergovernmental Organization - * or submit itself to any jurisdiction. - */ - -import { - deserializeRequest, - serializeRequest, -} from "../../utils/serialization.utils"; -import { describe, expect, test } from "@jest/globals"; - -describe("serializeRequest", () => { - test("serializes URL and options correctly", () => { - const url = "/api/test"; - const options = { - method: "POST", - headers: { "Content-Type": "application/json" }, - }; - - const buffer = serializeRequest(url, options); - const view = new Uint8Array(buffer); - - const typeLength = view[0]; - const typeBytes = view.slice(1, 1 + typeLength); - const jsonBytes = view.slice(1 + typeLength); - - const contentType = new TextDecoder().decode(typeBytes); - const json = JSON.parse(new TextDecoder().decode(jsonBytes)); - - expect(contentType).toBe("application/json"); - expect(json.url).toBe(url); - expect(json.options.method).toBe(options.method); - }); -}); - -describe("deserializeRequest", () => { - test("deserializes payload into correct request object", () => { - const url = "/api/test"; - const options = { method: "GET" }; - - const buffer = serializeRequest(url, options); - const result = deserializeRequest(buffer); - - expect(result.url).toBe(url); - expect(result.options.method).toBe("GET"); - }); - - test("throws error on unsupported content type", () => { - const encoder = new TextEncoder(); - const badType = encoder.encode("text/plain"); - const json = encoder.encode(JSON.stringify({ url: "/x" })); - - const buffer = new Uint8Array(1 + badType.length + json.length); - buffer[0] = badType.length; - buffer.set(badType, 1); - buffer.set(json, 1 + badType.length); - - expect(() => { - deserializeRequest(buffer.buffer); - }).toThrow("Unsupported content type: text/plain"); - }); -}); diff --git a/Tokenization/backend/wrapper/utils/serialization.utils.ts b/Tokenization/backend/wrapper/utils/serialization.utils.ts deleted file mode 100644 index 57ca8bcd2..000000000 --- a/Tokenization/backend/wrapper/utils/serialization.utils.ts +++ /dev/null @@ -1,68 +0,0 @@ -/** - * @license - * Copyright 2019-2020 CERN and copyright holders of ALICE O2. - * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. - * All rights not expressly granted are reserved. - * - * This software is distributed under the terms of the GNU General Public - * License v3 (GPL Version 3), copied verbatim in the file "COPYING". - * - * In applying this license CERN does not waive the privileges and immunities - * granted to it by virtue of its status as an Intergovernmental Organization - * or submit itself to any jurisdiction. - */ - -/** - * @description Serializes Json formatted request into binary payload with specific endpoint - * @param url - The endpoint URL to which the request is made - * @param options - Request options, such as headers or body - * @return {ArrayBuffer} - The serialized binary payload containing the URL and options - */ -export const serializeRequest = (url: string, options: any): ArrayBuffer => { - const encoder = new TextEncoder(); - const contentTypeBytes = encoder.encode("application/json"); - - // build JSON data - const jsonData = { - url: url, - options: options, - }; - - const jsonString = JSON.stringify(jsonData); - const jsonBytes = encoder.encode(jsonString); - - // Buffer following structure: - // 1 byte -> type length - // N bytes -> types - // rest -> JSON data - const buffer = new Uint8Array(1 + contentTypeBytes.length + jsonBytes.length); - - buffer[0] = contentTypeBytes.length; - buffer.set(contentTypeBytes, 1); - buffer.set(jsonBytes, 1 + contentTypeBytes.length); - - return buffer.buffer; -}; - -/** - * @description Deserializes binary payload to Json formated request - * @param payload - The binary payload to deserialize - * @return {any} - The deserialized request object containing the URL and options - */ -export const deserializeRequest = (payload: ArrayBuffer): any => { - const view = new Uint8Array(payload); - const decoder = new TextDecoder(); - - const contentTypeLength = view[0]; - const contentTypeBytes = view.slice(1, 1 + contentTypeLength); - const contentType = decoder.decode(contentTypeBytes); - - const dataBytes = view.slice(1 + contentTypeLength); - - // deserialization of JSON content - if (contentType === "application/json") { - return JSON.parse(decoder.decode(dataBytes)); - } else { - throw new Error(`Unsupported content type: ${contentType}`); - } -}; From d43912a823a9824b0f9d9ad2f93420e9e36a60dd Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Thu, 28 Aug 2025 21:54:24 +0200 Subject: [PATCH 53/81] fix: remove unused imports --- Tokenization/backend/wrapper/central/CentralSystemWrapper.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/Tokenization/backend/wrapper/central/CentralSystemWrapper.ts b/Tokenization/backend/wrapper/central/CentralSystemWrapper.ts index 353450633..2e99499f2 100644 --- a/Tokenization/backend/wrapper/central/CentralSystemWrapper.ts +++ b/Tokenization/backend/wrapper/central/CentralSystemWrapper.ts @@ -1,6 +1,5 @@ import * as grpc from "@grpc/grpc-js"; import * as protoLoader from "@grpc/proto-loader"; -import path from "path"; import { LogManager } from "@aliceo2/web-ui"; import { CentralSystemConfig } from "../models/config.model"; From 6ba86f253a33d44073ef5f20569c85f106520a06 Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Sun, 31 Aug 2025 12:14:16 +0200 Subject: [PATCH 54/81] fix: remove unnecessary utils --- .../test/utils/serialization.utils.test.ts | 71 ------------------- .../wrapper/src/utils/serialization.utils.ts | 68 ------------------ 2 files changed, 139 deletions(-) delete mode 100644 Tokenization/backend/wrapper/src/test/utils/serialization.utils.test.ts delete mode 100644 Tokenization/backend/wrapper/src/utils/serialization.utils.ts diff --git a/Tokenization/backend/wrapper/src/test/utils/serialization.utils.test.ts b/Tokenization/backend/wrapper/src/test/utils/serialization.utils.test.ts deleted file mode 100644 index 93d5c7d7c..000000000 --- a/Tokenization/backend/wrapper/src/test/utils/serialization.utils.test.ts +++ /dev/null @@ -1,71 +0,0 @@ -/** - * @license - * Copyright 2019-2020 CERN and copyright holders of ALICE O2. - * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. - * All rights not expressly granted are reserved. - * - * This software is distributed under the terms of the GNU General Public - * License v3 (GPL Version 3), copied verbatim in the file "COPYING". - * - * In applying this license CERN does not waive the privileges and immunities - * granted to it by virtue of its status as an Intergovernmental Organization - * or submit itself to any jurisdiction. - */ - -import { - deserializeRequest, - serializeRequest, -} from "../../utils/serialization.utils"; -import { describe, expect, test } from "@jest/globals"; - -describe("serializeRequest", () => { - test("serializes URL and options correctly", () => { - const url = "/api/test"; - const options = { - method: "POST", - headers: { "Content-Type": "application/json" }, - }; - - const buffer = serializeRequest(url, options); - const view = new Uint8Array(buffer); - - const typeLength = view[0]; - const typeBytes = view.slice(1, 1 + typeLength); - const jsonBytes = view.slice(1 + typeLength); - - const contentType = new TextDecoder().decode(typeBytes); - const json = JSON.parse(new TextDecoder().decode(jsonBytes)); - - expect(contentType).toBe("application/json"); - expect(json.url).toBe(url); - expect(json.options.method).toBe(options.method); - }); -}); - -describe("deserializeRequest", () => { - test("deserializes payload into correct request object", () => { - const url = "/api/test"; - const options = { method: "GET" }; - - const buffer = serializeRequest(url, options); - const result = deserializeRequest(buffer); - - expect(result.url).toBe(url); - expect(result.options.method).toBe("GET"); - }); - - test("throws error on unsupported content type", () => { - const encoder = new TextEncoder(); - const badType = encoder.encode("text/plain"); - const json = encoder.encode(JSON.stringify({ url: "/x" })); - - const buffer = new Uint8Array(1 + badType.length + json.length); - buffer[0] = badType.length; - buffer.set(badType, 1); - buffer.set(json, 1 + badType.length); - - expect(() => { - deserializeRequest(buffer.buffer); - }).toThrow("Unsupported content type: text/plain"); - }); -}); diff --git a/Tokenization/backend/wrapper/src/utils/serialization.utils.ts b/Tokenization/backend/wrapper/src/utils/serialization.utils.ts deleted file mode 100644 index 57ca8bcd2..000000000 --- a/Tokenization/backend/wrapper/src/utils/serialization.utils.ts +++ /dev/null @@ -1,68 +0,0 @@ -/** - * @license - * Copyright 2019-2020 CERN and copyright holders of ALICE O2. - * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. - * All rights not expressly granted are reserved. - * - * This software is distributed under the terms of the GNU General Public - * License v3 (GPL Version 3), copied verbatim in the file "COPYING". - * - * In applying this license CERN does not waive the privileges and immunities - * granted to it by virtue of its status as an Intergovernmental Organization - * or submit itself to any jurisdiction. - */ - -/** - * @description Serializes Json formatted request into binary payload with specific endpoint - * @param url - The endpoint URL to which the request is made - * @param options - Request options, such as headers or body - * @return {ArrayBuffer} - The serialized binary payload containing the URL and options - */ -export const serializeRequest = (url: string, options: any): ArrayBuffer => { - const encoder = new TextEncoder(); - const contentTypeBytes = encoder.encode("application/json"); - - // build JSON data - const jsonData = { - url: url, - options: options, - }; - - const jsonString = JSON.stringify(jsonData); - const jsonBytes = encoder.encode(jsonString); - - // Buffer following structure: - // 1 byte -> type length - // N bytes -> types - // rest -> JSON data - const buffer = new Uint8Array(1 + contentTypeBytes.length + jsonBytes.length); - - buffer[0] = contentTypeBytes.length; - buffer.set(contentTypeBytes, 1); - buffer.set(jsonBytes, 1 + contentTypeBytes.length); - - return buffer.buffer; -}; - -/** - * @description Deserializes binary payload to Json formated request - * @param payload - The binary payload to deserialize - * @return {any} - The deserialized request object containing the URL and options - */ -export const deserializeRequest = (payload: ArrayBuffer): any => { - const view = new Uint8Array(payload); - const decoder = new TextDecoder(); - - const contentTypeLength = view[0]; - const contentTypeBytes = view.slice(1, 1 + contentTypeLength); - const contentType = decoder.decode(contentTypeBytes); - - const dataBytes = view.slice(1 + contentTypeLength); - - // deserialization of JSON content - if (contentType === "application/json") { - return JSON.parse(decoder.decode(dataBytes)); - } else { - throw new Error(`Unsupported content type: ${contentType}`); - } -}; From 2569c5c8fa2dc16c0cde4c650e4bf05e98df7ae0 Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Sun, 31 Aug 2025 12:15:49 +0200 Subject: [PATCH 55/81] fix: remove unused variable --- Tokenization/backend/wrapper/src/client/gRPCWrapper.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/Tokenization/backend/wrapper/src/client/gRPCWrapper.ts b/Tokenization/backend/wrapper/src/client/gRPCWrapper.ts index 05eca013e..9b7835474 100644 --- a/Tokenization/backend/wrapper/src/client/gRPCWrapper.ts +++ b/Tokenization/backend/wrapper/src/client/gRPCWrapper.ts @@ -12,7 +12,6 @@ * or submit itself to any jurisdiction. */ -import path from "path"; import { ConnectionManager } from "./ConnectionManager/ConnectionManager"; import { RevokeTokenHandler } from "./Commands/revokeToken/revokeToken.handler"; import { From 21fb63e48d99902df30faf75622756f26e1f98fc Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Sun, 31 Aug 2025 12:24:08 +0200 Subject: [PATCH 56/81] fix: refactor unit tests --- .../src/test/client/Commands/newToken.test.ts | 28 ++++++++++++++++-- .../test/client/Commands/revokeToken.test.ts | 29 +++++++++++++++++-- 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/Tokenization/backend/wrapper/src/test/client/Commands/newToken.test.ts b/Tokenization/backend/wrapper/src/test/client/Commands/newToken.test.ts index 91f109204..829dff294 100644 --- a/Tokenization/backend/wrapper/src/test/client/Commands/newToken.test.ts +++ b/Tokenization/backend/wrapper/src/test/client/Commands/newToken.test.ts @@ -21,6 +21,9 @@ import { ConnectionDirection, DuplexMessageEvent, } from "../../../models/message.model"; +import * as grpc from "@grpc/grpc-js"; +import * as protoLoader from "@grpc/proto-loader"; +import path from "path"; /** * Helper to create a new token command with given address, direction, and token. @@ -42,6 +45,26 @@ function createEventMessage( describe("NewTokenHandler", () => { let manager: ConnectionManager; + const protoPath = path.join( + __dirname, + "..", + "..", + "..", + "proto", + "wrapper.proto" + ); + const packageDef = protoLoader.loadSync(protoPath, { + keepCase: true, + longs: String, + enums: String, + defaults: true, + oneofs: true, + }); + + const proto = grpc.loadPackageDefinition(packageDef) as any; + const wrapper = proto.webui.tokenization; + const peerCtor = wrapper.Peer2Peer; + beforeEach(() => { manager = { sendingConnections: new Map(), @@ -64,7 +87,7 @@ describe("NewTokenHandler", () => { dir: ConnectionDirection, token: string ) { - const conn = new Connection(token, address, dir); + const conn = new Connection(token, address, dir, peerCtor); if (dir === ConnectionDirection.SENDING) { this.sendingConnections.set(address, conn); } else { @@ -80,7 +103,8 @@ describe("NewTokenHandler", () => { const conn = new Connection( "old-token", targetAddress, - ConnectionDirection.SENDING + ConnectionDirection.SENDING, + peerCtor ); (manager as any).sendingConnections.set(targetAddress, conn); diff --git a/Tokenization/backend/wrapper/src/test/client/Commands/revokeToken.test.ts b/Tokenization/backend/wrapper/src/test/client/Commands/revokeToken.test.ts index 2602c5173..2eb821e90 100644 --- a/Tokenization/backend/wrapper/src/test/client/Commands/revokeToken.test.ts +++ b/Tokenization/backend/wrapper/src/test/client/Commands/revokeToken.test.ts @@ -22,8 +22,31 @@ import { } from "../../../models/message.model"; import { ConnectionStatus } from "../../../models/connection.model"; import { Command } from "models/commands.model"; +import * as grpc from "@grpc/grpc-js"; +import * as protoLoader from "@grpc/proto-loader"; +import path from "path"; describe("RevokeToken", () => { + const protoPath = path.join( + __dirname, + "..", + "..", + "..", + "proto", + "wrapper.proto" + ); + const packageDef = protoLoader.loadSync(protoPath, { + keepCase: true, + longs: String, + enums: String, + defaults: true, + oneofs: true, + }); + + const proto = grpc.loadPackageDefinition(packageDef) as any; + const wrapper = proto.webui.tokenization; + const peerCtor = wrapper.Peer2Peer; + function createEventMessage(targetAddress: string) { return { event: DuplexMessageEvent.MESSAGE_EVENT_REVOKE_TOKEN, @@ -54,7 +77,8 @@ describe("RevokeToken", () => { const conn = new Connection( "valid-token", targetAddress, - ConnectionDirection.SENDING + ConnectionDirection.SENDING, + peerCtor ); (manager as any).sendingConnections!.set(targetAddress, conn); @@ -74,7 +98,8 @@ describe("RevokeToken", () => { const conn = new Connection( "valid-token", targetAddress, - ConnectionDirection.RECEIVING + ConnectionDirection.RECEIVING, + peerCtor ); (manager as any).receivingConnections.set(targetAddress, conn); From 957a7befa3bdcdf50e5b60865d435d3af115c732 Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Sun, 31 Aug 2025 14:25:51 +0200 Subject: [PATCH 57/81] feat: update unit tests for p2p connections --- .../ConnectionManager.test.ts | 318 ++++++++++++++++++ .../test/client/ConnectionManager/index.ts | 162 --------- 2 files changed, 318 insertions(+), 162 deletions(-) create mode 100644 Tokenization/backend/wrapper/src/test/client/ConnectionManager/ConnectionManager.test.ts delete mode 100644 Tokenization/backend/wrapper/src/test/client/ConnectionManager/index.ts diff --git a/Tokenization/backend/wrapper/src/test/client/ConnectionManager/ConnectionManager.test.ts b/Tokenization/backend/wrapper/src/test/client/ConnectionManager/ConnectionManager.test.ts new file mode 100644 index 000000000..5397e99fd --- /dev/null +++ b/Tokenization/backend/wrapper/src/test/client/ConnectionManager/ConnectionManager.test.ts @@ -0,0 +1,318 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +import * as grpc from "@grpc/grpc-js"; +import { ConnectionManager } from "../../../client/ConnectionManager/ConnectionManager"; +import { + ConnectionDirection, + DuplexMessageEvent, +} from "../../../models/message.model"; + +// Mock duplex stream +const mockStream = { + on: jest.fn(), + end: jest.fn(), +}; + +// Mock gRPC client +const mockClient = { + ClientStream: jest.fn(() => mockStream), +}; + +// Mock CentralSystem constructor +const CentralSystemMock = jest.fn(() => mockClient); + +// Mock dispatcher +const mockDispatch = jest.fn(); +jest.mock( + "../../../client/ConnectionManager/EventManagement/CentralCommandDispatcher", + () => ({ + CentralCommandDispatcher: jest.fn(() => ({ + dispatch: mockDispatch, + register: jest.fn(), + })), + }) +); + +// Mock logger +jest.mock( + "@aliceo2/web-ui", + () => ({ + LogManager: { + getLogger: () => ({ + infoMessage: jest.fn(), + debugMessage: jest.fn(), + errorMessage: jest.fn(), + }), + }, + }), + { virtual: true } +); + +// Mock gRPC proto loader and client +jest.mock("@grpc/proto-loader", () => ({ + loadSync: jest.fn(() => { + return {}; + }), +})); + +let capturedServerImpl: any | null = null; + +jest.mock("@grpc/grpc-js", () => { + const original = jest.requireActual("@grpc/grpc-js"); + const Peer2PeerMock: any = jest.fn(() => ({ + Fetch: jest.fn(), + })); + // simulation of the service definition + Peer2PeerMock.service = { + Fetch: { + path: "/webui.tokenization.Peer2Peer/Fetch", + requestStream: false, + responseStream: false, + requestSerialize: (x: any) => x, + requestDeserialize: (x: any) => x, + responseSerialize: (x: any) => x, + responseDeserialize: (x: any) => x, + }, + }; + + // Mock server + const mockServer = { + addService: jest.fn((_svc: any, impl: any) => { + capturedServerImpl = impl; + }), + bindAsync: jest.fn((_addr: any, _creds: any, cb: any) => cb(null)), + forceShutdown: jest.fn(), + }; + + const mockServerCtor = jest.fn(() => mockServer); + + return { + ...original, + credentials: { + createInsecure: jest.fn(), + }, + ServerCredentials: { + createInsecure: jest.fn(() => ({})), + }, + status: { + ...original.status, + INTERNAL: 13, + }, + loadPackageDefinition: jest.fn(() => ({ + webui: { + tokenization: { + CentralSystem: CentralSystemMock, + Peer2Peer: Peer2PeerMock, + }, + }, + })), + Server: mockServerCtor, + }; +}); + +describe("ConnectionManager", () => { + let conn: ConnectionManager; + + beforeEach(() => { + jest.clearAllMocks(); + capturedServerImpl = null; + global.fetch = jest.fn(); + conn = new ConnectionManager("dummy.proto", "localhost:12345"); + }); + + afterAll(() => { + // @ts-ignore + delete global.fetch; + }); + + test("should initialize client with correct address", () => { + expect(conn).toBeDefined(); + expect(grpc.loadPackageDefinition).toHaveBeenCalled(); + expect(CentralSystemMock).toHaveBeenCalledWith( + "localhost:12345", + undefined + ); + }); + + test("connectToCentralSystem() should set up stream listeners", () => { + conn.connectToCentralSystem(); + + expect(mockClient.ClientStream).toHaveBeenCalled(); + expect(mockStream.on).toHaveBeenCalledWith("data", expect.any(Function)); + expect(mockStream.on).toHaveBeenCalledWith("end", expect.any(Function)); + expect(mockStream.on).toHaveBeenCalledWith("error", expect.any(Function)); + }); + + test("disconnectFromCentralSystem() should end stream", () => { + conn.connectToCentralSystem(); + conn.disconnectFromCentralSystem(); + + expect(mockStream.end).toHaveBeenCalled(); + }); + + test("should reconnect on stream 'end'", () => { + jest.useFakeTimers(); + conn.connectToCentralSystem(); + const onEnd = mockStream.on.mock.calls.find( + ([event]) => event === "end" + )?.[1]; + + onEnd?.(); // simulate 'end' + jest.advanceTimersByTime(2000); + + expect(mockClient.ClientStream).toHaveBeenCalledTimes(2); + jest.useRealTimers(); + }); + + test("should reconnect on stream 'error'", () => { + jest.useFakeTimers(); + conn.connectToCentralSystem(); + const onError = mockStream.on.mock.calls.find( + ([event]) => event === "error" + )?.[1]; + + onError?.(new Error("Simulated error")); + jest.advanceTimersByTime(2000); + + expect(mockClient.ClientStream).toHaveBeenCalledTimes(2); + jest.useRealTimers(); + }); + + test("should dispatch event when 'data' is received", () => { + conn.connectToCentralSystem(); + const onData = mockStream.on.mock.calls.find( + ([event]) => event === "data" + )?.[1]; + + const mockMessage = { + event: DuplexMessageEvent.MESSAGE_EVENT_REVOKE_TOKEN, + data: { + revokeToken: { + token: "abc123", + targetAddress: "peer-123", + }, + }, + }; + + onData?.(mockMessage); + + expect(mockDispatch).toHaveBeenCalledWith(mockMessage); + }); + + test("listenForPeers() should start server and register service", async () => { + await conn.listenForPeers(50055, "http://localhost:40041/api/"); + + const serverCtor = (grpc.Server as any).mock; + expect(serverCtor).toBeDefined(); + expect(serverCtor.calls.length).toBeGreaterThan(0); + + const serverInstance = serverCtor.results[0].value; + expect(serverInstance.addService).toHaveBeenCalled(); + expect(serverInstance.bindAsync).toHaveBeenCalledWith( + "localhost:50055", + expect.anything(), + expect.any(Function) + ); + + // implementacja metody Fetch została przechwycona + expect(capturedServerImpl).toBeTruthy(); + expect(typeof capturedServerImpl.Fetch).toBe("function"); + }); + + test("p2p Fetch should register incoming receiving connection and forward request", async () => { + await conn.listenForPeers(50056, "http://localhost:40041/api/"); + + // przygotuj dane wywołania + const call = { + getPeer: () => "client-42", + request: { + method: "POST", + path: "echo", + headers: { "content-type": "application/json" }, + body: Buffer.from(JSON.stringify({ ping: true })), + }, + } as any; + + const callback = jest.fn(); + + // @ts-ignore - mock global.fetch response + global.fetch.mockResolvedValue({ + status: 202, + headers: { + forEach: (fn: (v: string, k: string) => void) => { + fn("application/json", "content-type"); + fn("test", "x-extra"); + }, + }, + arrayBuffer: async () => Buffer.from(JSON.stringify({ ok: 1 })), + }); + + const before = conn.getAllConnections().receiving.length; + await capturedServerImpl.Fetch(call, callback); + + expect(global.fetch).toHaveBeenCalledWith( + "http://localhost:40041/api/echo", + { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ ping: true }), + } + ); + + // callback with response from forwarded fetch + expect(callback).toHaveBeenCalledWith(null, { + status: 202, + headers: { "content-type": "application/json", "x-extra": "test" }, + body: expect.any(Buffer), + }); + + // connection receiving should be registered + const after = conn.getAllConnections().receiving.length; + expect(after).toBeGreaterThan(before); + + const rec = conn.getConnectionByAddress( + "client-42", + ConnectionDirection.RECEIVING + ); + expect(rec).toBeDefined(); + }); + + test("p2p Fetch should return INTERNAL on forward error", async () => { + await conn.listenForPeers(50057, "http://localhost:40041/api/"); + + const call = { + getPeer: () => "client-error", + request: { + method: "GET", + path: "fail", + headers: {}, + }, + } as any; + + const callback = jest.fn(); + + // @ts-ignore + global.fetch.mockRejectedValue(new Error("err")); + + await capturedServerImpl.Fetch(call, callback); + + expect(callback).toHaveBeenCalledWith( + expect.objectContaining({ + code: grpc.status.INTERNAL, + message: "err", + }) + ); + }); +}); diff --git a/Tokenization/backend/wrapper/src/test/client/ConnectionManager/index.ts b/Tokenization/backend/wrapper/src/test/client/ConnectionManager/index.ts deleted file mode 100644 index 1da1fb06f..000000000 --- a/Tokenization/backend/wrapper/src/test/client/ConnectionManager/index.ts +++ /dev/null @@ -1,162 +0,0 @@ -/** - * @license - * Copyright 2019-2020 CERN and copyright holders of ALICE O2. - * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. - * All rights not expressly granted are reserved. - * - * This software is distributed under the terms of the GNU General Public - * License v3 (GPL Version 3), copied verbatim in the file "COPYING". - * - * In applying this license CERN does not waive the privileges and immunities - * granted to it by virtue of its status as an Intergovernmental Organization - * or submit itself to any jurisdiction. - */ - -import * as grpc from "@grpc/grpc-js"; -import { ConnectionManager } from "../../../client/ConnectionManager/ConnectionManager"; -import { DuplexMessageEvent } from "../../../models/message.model"; - -// Mock duplex stream -const mockStream = { - on: jest.fn(), - end: jest.fn(), -}; - -// Mock gRPC client -const mockClient = { - ClientStream: jest.fn(() => mockStream), -}; - -// Mock CentralSystem constructor -const CentralSystemMock = jest.fn(() => mockClient); - -// Mock dispatcher -const mockDispatch = jest.fn(); -jest.mock( - "../../../client/ConnectionManager/EventManagement/CentralCommandDispatcher", - () => ({ - CentralCommandDispatcher: jest.fn(() => ({ - dispatch: mockDispatch, - })), - }) -); - -// Mock logger -jest.mock( - "@aliceo2/web-ui", - () => ({ - LogManager: { - getLogger: () => ({ - infoMessage: jest.fn(), - }), - }, - }), - { virtual: true } -); - -// Mock gRPC proto loader and client -jest.mock("@grpc/proto-loader", () => ({ - loadSync: jest.fn(() => { - return {}; - }), -})); - -jest.mock("@grpc/grpc-js", () => { - const original = jest.requireActual("@grpc/grpc-js"); - return { - ...original, - credentials: { - createInsecure: jest.fn(), - }, - loadPackageDefinition: jest.fn(() => ({ - webui: { - tokenization: { - CentralSystem: CentralSystemMock, - }, - }, - })), - }; -}); - -describe("ConnectionManager", () => { - let conn: ConnectionManager; - - beforeEach(() => { - jest.clearAllMocks(); - conn = new ConnectionManager("dummy.proto", "localhost:12345"); - }); - - test("should initialize client with correct address", () => { - expect(conn).toBeDefined(); - expect(grpc.loadPackageDefinition).toHaveBeenCalled(); - expect(CentralSystemMock).toHaveBeenCalledWith( - "localhost:12345", - undefined - ); - }); - - test("connectToCentralSystem() should set up stream listeners", () => { - conn.connectToCentralSystem(); - - expect(mockClient.ClientStream).toHaveBeenCalled(); - expect(mockStream.on).toHaveBeenCalledWith("data", expect.any(Function)); - expect(mockStream.on).toHaveBeenCalledWith("end", expect.any(Function)); - expect(mockStream.on).toHaveBeenCalledWith("error", expect.any(Function)); - }); - - test("disconnectFromCentralSystem() should end stream", () => { - conn.connectToCentralSystem(); - conn.disconnectFromCentralSystem(); - - expect(mockStream.end).toHaveBeenCalled(); - }); - - test("should reconnect on stream 'end'", () => { - jest.useFakeTimers(); - conn.connectToCentralSystem(); - const onEnd = mockStream.on.mock.calls.find( - ([event]) => event === "end" - )?.[1]; - - onEnd?.(); // simulate 'end' - jest.advanceTimersByTime(2000); - - expect(mockClient.ClientStream).toHaveBeenCalledTimes(2); - jest.useRealTimers(); - }); - - test("should reconnect on stream 'error'", () => { - jest.useFakeTimers(); - conn.connectToCentralSystem(); - const onError = mockStream.on.mock.calls.find( - ([event]) => event === "error" - )?.[1]; - - onError?.(new Error("Simulated error")); - jest.advanceTimersByTime(2000); - - expect(mockClient.ClientStream).toHaveBeenCalledTimes(2); - jest.useRealTimers(); - }); - - test("should dispatch event when 'data' is received", () => { - conn.connectToCentralSystem(); - const onData = mockStream.on.mock.calls.find( - ([event]) => event === "data" - )?.[1]; - - const mockMessage = { - event: DuplexMessageEvent.MESSAGE_EVENT_REVOKE_TOKEN, - data: { - revokeToken: { - token: "abc123", - targetAddress: "peer-123", - }, - }, - }; - - onData?.(mockMessage); - - expect(mockDispatch).toHaveBeenCalledWith(mockMessage); - }); -}); From a7b81a25e004035aacfe9dd8a75f418842da1d2a Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki <42175519+OmegaCreations@users.noreply.github.com> Date: Tue, 2 Sep 2025 19:05:23 +0200 Subject: [PATCH 58/81] Potential fix for code scanning alert no. 236: Unused variable, import, function or class Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- Tokenization/backend/wrapper/client/gRPCWrapper.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/Tokenization/backend/wrapper/client/gRPCWrapper.ts b/Tokenization/backend/wrapper/client/gRPCWrapper.ts index 7a4b8e796..fb74b45dc 100644 --- a/Tokenization/backend/wrapper/client/gRPCWrapper.ts +++ b/Tokenization/backend/wrapper/client/gRPCWrapper.ts @@ -1,4 +1,3 @@ -import path from "path"; import { ConnectionManager } from "./ConnectionManager/ConnectionManager.ts"; import { gRPCWrapperConfig } from "../models/config.model.ts"; From b8edd56a4b7939394133bec6981dc418d6ff4e2c Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Tue, 2 Sep 2025 20:55:20 +0200 Subject: [PATCH 59/81] feat: implement reconnection scheduler --- .../ConnectionManager/CentralConnection.ts | 22 ++--- .../src/utils/ReconnectionScheduler.ts | 95 +++++++++++++++++++ .../wrapper/src/utils/types/webui.d.ts | 14 +-- 3 files changed, 113 insertions(+), 18 deletions(-) create mode 100644 Tokenization/backend/wrapper/src/utils/ReconnectionScheduler.ts diff --git a/Tokenization/backend/wrapper/src/client/ConnectionManager/CentralConnection.ts b/Tokenization/backend/wrapper/src/client/ConnectionManager/CentralConnection.ts index f41a586fd..32ad6ad37 100644 --- a/Tokenization/backend/wrapper/src/client/ConnectionManager/CentralConnection.ts +++ b/Tokenization/backend/wrapper/src/client/ConnectionManager/CentralConnection.ts @@ -16,6 +16,7 @@ import * as grpc from "@grpc/grpc-js"; import { LogManager } from "@aliceo2/web-ui"; import { CentralCommandDispatcher } from "./EventManagement/CentralCommandDispatcher"; import { DuplexMessageModel } from "../../models/message.model"; +import { ReconnectionScheduler } from "utils/reconnectionScheduler"; /** * @description This class manages the duplex stream with the CentralSystem gRPC service. @@ -24,6 +25,12 @@ import { DuplexMessageModel } from "../../models/message.model"; export class CentralConnection { private logger = LogManager.getLogger("CentralConnection"); private stream?: grpc.ClientDuplexStream; + private reconnectionScheduler: ReconnectionScheduler = + new ReconnectionScheduler( + () => this.connect(), + { initialDelay: 1000, maxDelay: 30000 }, + this.logger + ); constructor( private client: any, @@ -40,13 +47,14 @@ export class CentralConnection { this.stream!.on("data", (payload: DuplexMessageModel) => { this.logger.debugMessage(`Received payload: ${JSON.stringify(payload)}`); + this.reconnectionScheduler.reset(); this.dispatcher.dispatch(payload); }); this.stream!.on("end", () => { this.logger.infoMessage(`Stream ended, attempting to reconnect...`); this.stream = undefined; - this.scheduleReconnect(); + this.reconnectionScheduler.schedule(); }); this.stream!.on("error", (err: any) => { @@ -56,20 +64,10 @@ export class CentralConnection { " attempting to reconnect..." ); this.stream = undefined; - this.scheduleReconnect(); + this.reconnectionScheduler.schedule(); }); } - /** - * @description Schedules a reconnect with exponential backoff. - */ - private scheduleReconnect() { - setTimeout(() => { - this.logger.infoMessage(`Trying to reconnect...`); - this.connect(); - }, 2000); - } - /** * @description Starts the connection to the central system. */ diff --git a/Tokenization/backend/wrapper/src/utils/ReconnectionScheduler.ts b/Tokenization/backend/wrapper/src/utils/ReconnectionScheduler.ts new file mode 100644 index 000000000..c1a99f923 --- /dev/null +++ b/Tokenization/backend/wrapper/src/utils/ReconnectionScheduler.ts @@ -0,0 +1,95 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +export interface ReconnectionOptions { + initialDelay?: number; // Initial delay in ms + maxDelay?: number; // Maximum delay in ms +} + +/** + * @description Schedules reconnection attempts with exponential backoff. + */ +export class ReconnectionScheduler { + private reconnectCallback: any; + private initialDelay: number; + private maxDelay: number; + private currentDelay: number; + private attemptCount: number; + private timeoutId: any; + private logger: Logger; + private isResetting: boolean = false; + + /** + * @param reconnectCallback Function to call for reconnection attempt + * @param options Configuration options for reconnection scheduling + */ + constructor( + reconnectCallback: any, + options: ReconnectionOptions = {}, + logger: Logger + ) { + this.reconnectCallback = reconnectCallback; + this.initialDelay = options.initialDelay || 1000; + this.maxDelay = options.maxDelay || 30000; + + this.currentDelay = this.initialDelay; + this.attemptCount = 0; + this.timeoutId = null; + + this.logger = logger; + } + + /** + * @description Schedules the next reconnection attempt using exponential backoff. + */ + schedule() { + this.isResetting = false; + this.attemptCount++; + + // exponential backoff calculation + let delay = this.initialDelay * Math.pow(2, this.attemptCount); + + this.currentDelay = Math.min(this.maxDelay, delay); + + this.logger.infoMessage( + `Recconection attempt #${ + this.attemptCount + }: Sleep for ${this.currentDelay.toFixed(0)} ms.` + ); + + // plan the reconnection attempt + this.timeoutId = setTimeout(() => { + this.reconnectCallback(); + }, this.currentDelay); + } + + /** + * @description Resets the scheduler to its initial state. + */ + reset() { + if (this.isResetting) return; + this.isResetting = true; + + clearTimeout(this.timeoutId); + this.attemptCount = 0; + this.currentDelay = this.initialDelay; + } + + /** + * @description stops the reconnection attempts. + */ + stop() { + clearTimeout(this.timeoutId); + } +} diff --git a/Tokenization/backend/wrapper/src/utils/types/webui.d.ts b/Tokenization/backend/wrapper/src/utils/types/webui.d.ts index 9432663e8..41fb94bb8 100644 --- a/Tokenization/backend/wrapper/src/utils/types/webui.d.ts +++ b/Tokenization/backend/wrapper/src/utils/types/webui.d.ts @@ -14,11 +14,13 @@ declare module "@aliceo2/web-ui" { export const LogManager: { - getLogger: (name: string) => { - infoMessage: (...args: any[]) => void; - errorMessage: (...args: any[]) => void; - warnMessage: (...args: any[]) => void; - debugMessage: (...args: any[]) => void; - }; + getLogger: (name: string) => Logger; }; } + +declare interface Logger { + infoMessage: (...args: any[]) => void; + errorMessage: (...args: any[]) => void; + warnMessage: (...args: any[]) => void; + debugMessage: (...args: any[]) => void; +} From 863438b6cee11fa4dd24fb834d38835dcc0c4006 Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Wed, 3 Sep 2025 14:20:11 +0200 Subject: [PATCH 60/81] fix: fix scheduler --- .../ConnectionManager/CentralConnection.ts | 2 +- .../backend/wrapper/src/client/gRPCWrapper.ts | 32 ------------------- .../src/utils/ReconnectionScheduler.ts | 5 +++ 3 files changed, 6 insertions(+), 33 deletions(-) diff --git a/Tokenization/backend/wrapper/src/client/ConnectionManager/CentralConnection.ts b/Tokenization/backend/wrapper/src/client/ConnectionManager/CentralConnection.ts index 32ad6ad37..3aefaf9fb 100644 --- a/Tokenization/backend/wrapper/src/client/ConnectionManager/CentralConnection.ts +++ b/Tokenization/backend/wrapper/src/client/ConnectionManager/CentralConnection.ts @@ -16,7 +16,7 @@ import * as grpc from "@grpc/grpc-js"; import { LogManager } from "@aliceo2/web-ui"; import { CentralCommandDispatcher } from "./EventManagement/CentralCommandDispatcher"; import { DuplexMessageModel } from "../../models/message.model"; -import { ReconnectionScheduler } from "utils/reconnectionScheduler"; +import { ReconnectionScheduler } from "../../utils/reconnectionScheduler"; /** * @description This class manages the duplex stream with the CentralSystem gRPC service. diff --git a/Tokenization/backend/wrapper/src/client/gRPCWrapper.ts b/Tokenization/backend/wrapper/src/client/gRPCWrapper.ts index 9b7835474..797f39816 100644 --- a/Tokenization/backend/wrapper/src/client/gRPCWrapper.ts +++ b/Tokenization/backend/wrapper/src/client/gRPCWrapper.ts @@ -129,35 +129,3 @@ export class gRPCWrapper { ); } } - -// const PROTO_PATH = path.join(__dirname, "../proto/wrapper.proto"); -// const grpc = new gRPCWrapper(PROTO_PATH, "localhost:50051"); -// grpc.connectToCentralSystem(); -// console.log(grpc.getSummary()); - -// setTimeout(() => { -// console.log("New status after 10 seconds, token revokation and new token:"); -// console.log(grpc.getSummary()); -// }, 10000); - -// grpc.connectToCentralSystem(); - -// const conn1: Connection = grpc.connectToClient("localhost:40001"); - -// // wrapping request -// conn1 -// .fetch({ -// method: "POST", -// headers: { -// "Content-Type": "application/json", -// Authorization: "bearer someToken", -// }, -// body: JSON.stringify({ -// name: "Jan Kowalski", -// email: "jan.kowalski@example.com", -// age: 28, -// }), -// }) -// .then((response) => response.json()) -// .then((data) => console.log("Response:", data)) -// .catch((error) => console.error("Error:", error)); diff --git a/Tokenization/backend/wrapper/src/utils/ReconnectionScheduler.ts b/Tokenization/backend/wrapper/src/utils/ReconnectionScheduler.ts index c1a99f923..d4f0f2504 100644 --- a/Tokenization/backend/wrapper/src/utils/ReconnectionScheduler.ts +++ b/Tokenization/backend/wrapper/src/utils/ReconnectionScheduler.ts @@ -28,7 +28,9 @@ export class ReconnectionScheduler { private attemptCount: number; private timeoutId: any; private logger: Logger; + private isResetting: boolean = false; + private isScheduling: boolean = false; /** * @param reconnectCallback Function to call for reconnection attempt @@ -54,6 +56,8 @@ export class ReconnectionScheduler { * @description Schedules the next reconnection attempt using exponential backoff. */ schedule() { + if (this.isScheduling) return; + this.isScheduling = true; this.isResetting = false; this.attemptCount++; @@ -70,6 +74,7 @@ export class ReconnectionScheduler { // plan the reconnection attempt this.timeoutId = setTimeout(() => { + this.isScheduling = false; this.reconnectCallback(); }, this.currentDelay); } From 542e0f6dd1c2e777cb86bd127e844a4e9dbbd013 Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Wed, 3 Sep 2025 14:54:23 +0200 Subject: [PATCH 61/81] fix: fix imports --- .../utils/{ReconnectionScheduler.ts => reconnectionScheduler.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Tokenization/backend/wrapper/src/utils/{ReconnectionScheduler.ts => reconnectionScheduler.ts} (100%) diff --git a/Tokenization/backend/wrapper/src/utils/ReconnectionScheduler.ts b/Tokenization/backend/wrapper/src/utils/reconnectionScheduler.ts similarity index 100% rename from Tokenization/backend/wrapper/src/utils/ReconnectionScheduler.ts rename to Tokenization/backend/wrapper/src/utils/reconnectionScheduler.ts From b866463d500953c7241e7eddf0b0b35cef837220 Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Thu, 25 Sep 2025 22:42:34 +0200 Subject: [PATCH 62/81] feat: implement secure connection and fix unit tests --- ...ntralSystem.ts => CentralSystemWrapper.ts} | 34 +++++--- .../src/client/Connection/Connection.ts | 24 ++++-- .../ConnectionManager/ConnectionManager.ts | 50 +++++------- .../backend/wrapper/src/client/gRPCWrapper.ts | 48 +++++++----- .../wrapper/src/models/config.model.ts | 46 +++++++++++ .../src/test/central/CentralSystem.test.ts | 12 ++- .../test/central/CentralSystemWrapper.test.ts | 12 ++- .../src/test/client/Commands/newToken.test.ts | 13 +++- .../test/client/Commands/revokeToken.test.ts | 7 +- .../ConnectionManager.test.ts | 40 ++++++++-- .../backend/wrapper/src/test/testCerts/ca.crt | 12 +++ .../central.system.svc.local.crt | 14 ++++ .../central.system.svc.local.key | 5 ++ .../clientListener/client-b.svc.local.crt | 13 ++++ .../clientListener/client-b.svc.local.key | 5 ++ .../clientSender/client-a.svc.local.crt | 13 ++++ .../clientSender/client-a.svc.local.key | 5 ++ .../wrapper/src/test/testCerts/testCerts.ts | 78 +++++++++++++++++++ 18 files changed, 346 insertions(+), 85 deletions(-) rename Tokenization/backend/wrapper/src/central/{CentralSystem.ts => CentralSystemWrapper.ts} (87%) create mode 100644 Tokenization/backend/wrapper/src/models/config.model.ts create mode 100644 Tokenization/backend/wrapper/src/test/testCerts/ca.crt create mode 100644 Tokenization/backend/wrapper/src/test/testCerts/centralSystem/central.system.svc.local.crt create mode 100644 Tokenization/backend/wrapper/src/test/testCerts/centralSystem/central.system.svc.local.key create mode 100644 Tokenization/backend/wrapper/src/test/testCerts/clientListener/client-b.svc.local.crt create mode 100644 Tokenization/backend/wrapper/src/test/testCerts/clientListener/client-b.svc.local.key create mode 100644 Tokenization/backend/wrapper/src/test/testCerts/clientSender/client-a.svc.local.crt create mode 100644 Tokenization/backend/wrapper/src/test/testCerts/clientSender/client-a.svc.local.key create mode 100644 Tokenization/backend/wrapper/src/test/testCerts/testCerts.ts diff --git a/Tokenization/backend/wrapper/src/central/CentralSystem.ts b/Tokenization/backend/wrapper/src/central/CentralSystemWrapper.ts similarity index 87% rename from Tokenization/backend/wrapper/src/central/CentralSystem.ts rename to Tokenization/backend/wrapper/src/central/CentralSystemWrapper.ts index 4244317ac..a7a8b5eb4 100644 --- a/Tokenization/backend/wrapper/src/central/CentralSystem.ts +++ b/Tokenization/backend/wrapper/src/central/CentralSystemWrapper.ts @@ -17,6 +17,7 @@ import * as protoLoader from "@grpc/proto-loader"; import { LogManager } from "@aliceo2/web-ui"; import { DuplexMessageModel } from "../models/message.model"; import * as fs from "fs"; +import { CentralSystemConfig } from "models/config.model"; /** * @description Central System gRPC wrapper that manages client connections and handles gRPC streams with them. @@ -27,6 +28,11 @@ export class CentralSystemWrapper { // class properties private server: grpc.Server; + private protoPath: string; + private port: number; + + // certificates paths + private serverCerts: CentralSystemConfig["serverCerts"]; // clients management private clients = new Map>(); @@ -36,13 +42,21 @@ export class CentralSystemWrapper { * Initializes the Wrapper for CentralSystem. * @param port The port number to bind the gRPC server to. */ - constructor( - private protoPath: string, - private port: number, - private readonly caCertPath: string, - private readonly centralCertPath: string, - private readonly centralKeyPath: string - ) { + constructor(config: CentralSystemConfig) { + if ( + !config.protoPath || + !config.serverCerts || + !config.serverCerts.caCertPath || + !config.serverCerts.certPath || + !config.serverCerts.keyPath + ) { + throw new Error("Invalid CentralSystemConfig provided"); + } + + this.protoPath = config.protoPath; + this.serverCerts = config.serverCerts; + this.port = config.port || 50051; + this.server = new grpc.Server(); this.setupService(); } @@ -176,9 +190,9 @@ export class CentralSystemWrapper { const addr = `localhost:${this.port}`; // create mTLS secure gRPC server - const caCert = fs.readFileSync(this.caCertPath); - const centralKey = fs.readFileSync(this.centralKeyPath); - const centralCert = fs.readFileSync(this.centralCertPath); + const caCert = fs.readFileSync(this.serverCerts.caCertPath); + const centralKey = fs.readFileSync(this.serverCerts.keyPath); + const centralCert = fs.readFileSync(this.serverCerts.certPath); const sslCreds = grpc.ServerCredentials.createSsl( caCert, diff --git a/Tokenization/backend/wrapper/src/client/Connection/Connection.ts b/Tokenization/backend/wrapper/src/client/Connection/Connection.ts index b322f51dd..b4ca2e597 100644 --- a/Tokenization/backend/wrapper/src/client/Connection/Connection.ts +++ b/Tokenization/backend/wrapper/src/client/Connection/Connection.ts @@ -49,19 +49,31 @@ export class Connection { targetAddress: string, direction: ConnectionDirection, peerCtor: any, - private readonly caCert: NonSharedBuffer, - private readonly clientCert: NonSharedBuffer, - private readonly clientKey: NonSharedBuffer + private readonly connectionCerts: { + caCert: NonSharedBuffer; + clientCert: NonSharedBuffer; + clientKey: NonSharedBuffer; + } ) { this.token = token; this.targetAddress = targetAddress; this.direction = direction; + if ( + !connectionCerts.caCert || + !connectionCerts.clientCert || + !connectionCerts.clientKey + ) { + throw new Error( + "Connection certificates are required to create a Connection." + ); + } + // create grpc credentials const sslCreds = grpc.credentials.createSsl( - this.caCert, - this.clientKey, - this.clientCert + this.connectionCerts.caCert, + this.connectionCerts.clientKey, + this.connectionCerts.clientCert ); this.peerClient = new peerCtor(targetAddress, sslCreds); diff --git a/Tokenization/backend/wrapper/src/client/ConnectionManager/ConnectionManager.ts b/Tokenization/backend/wrapper/src/client/ConnectionManager/ConnectionManager.ts index 4db68af63..4f8ffc756 100644 --- a/Tokenization/backend/wrapper/src/client/ConnectionManager/ConnectionManager.ts +++ b/Tokenization/backend/wrapper/src/client/ConnectionManager/ConnectionManager.ts @@ -116,27 +116,19 @@ export class ConnectionManager { this.sendingConnections.set( "a", - new Connection( - "1", - "a", - ConnectionDirection.SENDING, - this.peerCtor, - this.caCert, - this.clientKey, - this.clientCert - ) + new Connection("1", "a", ConnectionDirection.SENDING, this.peerCtor, { + caCert: this.caCert, + clientCert: this.clientCert, + clientKey: this.clientKey, + }) ); this.sendingConnections.set( "b", - new Connection( - "2", - "b", - ConnectionDirection.SENDING, - this.peerCtor, - this.caCert, - this.clientKey, - this.clientCert - ) + new Connection("2", "b", ConnectionDirection.SENDING, this.peerCtor, { + caCert: this.caCert, + clientCert: this.clientCert, + clientKey: this.clientKey, + }) ); } @@ -197,15 +189,11 @@ export class ConnectionManager { } // Create new connection - conn = new Connection( - token || "", - address, - direction, - this.peerCtor, - this.caCert, - this.clientKey, - this.clientCert - ); + conn = new Connection(token || "", address, direction, this.peerCtor, { + caCert: this.caCert, + clientCert: this.clientCert, + clientKey: this.clientKey, + }); conn.updateStatus(ConnectionStatus.CONNECTING); if (direction === ConnectionDirection.RECEIVING) { @@ -287,9 +275,11 @@ export class ConnectionManager { clientAddress, ConnectionDirection.RECEIVING, this.peerCtor, - this.caCert, - this.clientKey, - this.clientCert + { + caCert: this.caCert, + clientCert: this.clientCert, + clientKey: this.clientKey, + } ); conn.updateStatus(ConnectionStatus.CONNECTED); this.receivingConnections.set(clientAddress, conn); diff --git a/Tokenization/backend/wrapper/src/client/gRPCWrapper.ts b/Tokenization/backend/wrapper/src/client/gRPCWrapper.ts index e339465b9..5be3e7ee3 100644 --- a/Tokenization/backend/wrapper/src/client/gRPCWrapper.ts +++ b/Tokenization/backend/wrapper/src/client/gRPCWrapper.ts @@ -20,8 +20,9 @@ import { } from "../models/message.model"; import { Connection } from "./Connection/Connection"; import { NewTokenHandler } from "./Commands/newToken/newToken.handler"; -import { gRPCWrapperConfig } from "../models/config.model.ts"; +import { gRPCWrapperConfig } from "../models/config.model"; import * as fs from "fs"; +import { LogManager } from "@aliceo2/web-ui"; /** * @description Wrapper class for managing secure gRPC wrapper. @@ -41,6 +42,7 @@ export class gRPCWrapper { private ConnectionManager: ConnectionManager; private listenerKey?: NonSharedBuffer; private listenerCert?: NonSharedBuffer; + private logger = LogManager.getLogger("gRPCWrapper"); /** * @description Initializes an instance of gRPCWrapper class. @@ -48,27 +50,32 @@ export class gRPCWrapper { * @param protoPath - The file path to the gRPC proto definition. * @param centralAddress - The address of the central gRPC server (default: "localhost:50051"). */ - constructor( - protoPath: string, - centralAddress: string = "localhost:50051", - private readonly clientCerts: { - keyPath: string; - certPath: string; - caPath: string; - }, - private listenerCertPaths?: { keyPath: string; certPath: string } - ) { - if (listenerCertPaths?.keyPath && listenerCertPaths?.certPath) { - this.listenerKey = fs.readFileSync(listenerCertPaths.keyPath); - this.listenerCert = fs.readFileSync(listenerCertPaths.certPath); + constructor(config: gRPCWrapperConfig) { + if ( + !config.protoPath || + !config.centralAddress || + !config.clientCerts || + !config.clientCerts.caCertPath || + !config.clientCerts.certPath || + !config.clientCerts.keyPath + ) { + throw new Error("Invalid gRPCWrapper configuration provided."); + } + + if ( + config.listenerCertPaths?.keyPath && + config.listenerCertPaths?.certPath + ) { + this.listenerKey = fs.readFileSync(config.listenerCertPaths.keyPath); + this.listenerCert = fs.readFileSync(config.listenerCertPaths.certPath); } this.ConnectionManager = new ConnectionManager( - protoPath, - centralAddress, - this.clientCerts.caPath, - this.clientCerts.certPath, - this.clientCerts.keyPath + config.protoPath, + config.centralAddress, + config.clientCerts.caCertPath, + config.clientCerts.certPath, + config.clientCerts.keyPath ); this.ConnectionManager.registerCommandHandlers([ { @@ -117,9 +124,10 @@ export class gRPCWrapper { } if (!this.listenerKey || !this.listenerCert) { - throw new Error( + this.logger.errorMessage( "Listener certificates are required to start P2P listener. Please provide valid paths." ); + return; } return this.ConnectionManager.listenForPeers( diff --git a/Tokenization/backend/wrapper/src/models/config.model.ts b/Tokenization/backend/wrapper/src/models/config.model.ts new file mode 100644 index 000000000..6d6b18555 --- /dev/null +++ b/Tokenization/backend/wrapper/src/models/config.model.ts @@ -0,0 +1,46 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +export interface CentralSystemConfig { + /** Path to the proto file defining the services. */ + protoPath: string; + /** Host/IP to bind the gRPC server on. Defaults to "0.0.0.0" which is docker-friendly. */ + host?: string; + /** Port to bind. Defaults to 50051. */ + port?: number; + + /** Central TLS certificates paths. */ + serverCerts: { + caCertPath: string; + certPath: string; + keyPath: string; + }; +} + +export interface gRPCWrapperConfig { + /** Path to the proto file defining the services. */ + protoPath: string; + /** Address of the CentralSystem server. */ + centralAddress: string; + + /** Client TLS certificates paths. */ + clientCerts: { + caCertPath: string; + keyPath: string; + certPath: string; + }; + + /** Optional listener TLS certificates paths. If provided, the gRPCWrapper will be able to accept incoming connections. */ + listenerCertPaths?: { keyPath: string; certPath: string }; +} diff --git a/Tokenization/backend/wrapper/src/test/central/CentralSystem.test.ts b/Tokenization/backend/wrapper/src/test/central/CentralSystem.test.ts index 8fe1c38d0..ef54852ae 100644 --- a/Tokenization/backend/wrapper/src/test/central/CentralSystem.test.ts +++ b/Tokenization/backend/wrapper/src/test/central/CentralSystem.test.ts @@ -21,6 +21,7 @@ const mockServerInstance = { const logger = { infoMessage: jest.fn(), + errorMessage: jest.fn(), }; jest.mock( @@ -45,7 +46,7 @@ jest.mock("@grpc/grpc-js", () => { ...original, Server: jest.fn(() => mockServerInstance), ServerCredentials: { - createInsecure: jest.fn(() => "mock-credentials"), + createSsl: jest.fn(() => "mock-credentials"), }, loadPackageDefinition: jest.fn(() => ({ webui: { @@ -59,24 +60,27 @@ jest.mock("@grpc/grpc-js", () => { }; }); +import { getTestCentralCertPaths } from "../testCerts/testCerts"; import { CentralSystemWrapper } from "../../central/CentralSystemWrapper"; import * as grpc from "@grpc/grpc-js"; describe("CentralSystemWrapper", () => { let wrapper: CentralSystemWrapper; + const testCentralCertPaths = getTestCentralCertPaths(); beforeEach(() => { jest.clearAllMocks(); wrapper = new CentralSystemWrapper({ protoPath: "dummy.proto", port: 12345, + serverCerts: testCentralCertPaths, }); }); test("should set up gRPC service and add it to the server", () => { expect(grpc.Server).toHaveBeenCalled(); expect(grpc.loadPackageDefinition).toHaveBeenCalled(); - expect(grpc.ServerCredentials.createInsecure).not.toHaveBeenCalled(); + expect(grpc.ServerCredentials.createSsl).not.toHaveBeenCalled(); expect(wrapper).toBeDefined(); }); @@ -98,7 +102,7 @@ describe("CentralSystemWrapper", () => { wrapper.listen(); - expect(logger.infoMessage).toHaveBeenCalledWith( + expect(logger.errorMessage).toHaveBeenCalledWith( "Server bind error:", error ); @@ -131,7 +135,7 @@ describe("CentralSystemWrapper", () => { expect(logger.infoMessage).toHaveBeenCalledWith( "Client client123 ended stream." ); - expect(logger.infoMessage).toHaveBeenCalledWith( + expect(logger.errorMessage).toHaveBeenCalledWith( "Stream error from client client123:", expect.any(Error) ); diff --git a/Tokenization/backend/wrapper/src/test/central/CentralSystemWrapper.test.ts b/Tokenization/backend/wrapper/src/test/central/CentralSystemWrapper.test.ts index 8fe1c38d0..d8954bcfb 100644 --- a/Tokenization/backend/wrapper/src/test/central/CentralSystemWrapper.test.ts +++ b/Tokenization/backend/wrapper/src/test/central/CentralSystemWrapper.test.ts @@ -21,6 +21,7 @@ const mockServerInstance = { const logger = { infoMessage: jest.fn(), + errorMessage: jest.fn(), }; jest.mock( @@ -45,7 +46,7 @@ jest.mock("@grpc/grpc-js", () => { ...original, Server: jest.fn(() => mockServerInstance), ServerCredentials: { - createInsecure: jest.fn(() => "mock-credentials"), + createSsl: jest.fn(() => "mock-credentials"), }, loadPackageDefinition: jest.fn(() => ({ webui: { @@ -61,22 +62,25 @@ jest.mock("@grpc/grpc-js", () => { import { CentralSystemWrapper } from "../../central/CentralSystemWrapper"; import * as grpc from "@grpc/grpc-js"; +import { getTestCentralCertPaths } from "../testCerts/testCerts"; describe("CentralSystemWrapper", () => { let wrapper: CentralSystemWrapper; + const testCentralCertPaths = getTestCentralCertPaths(); beforeEach(() => { jest.clearAllMocks(); wrapper = new CentralSystemWrapper({ protoPath: "dummy.proto", port: 12345, + serverCerts: testCentralCertPaths, }); }); test("should set up gRPC service and add it to the server", () => { expect(grpc.Server).toHaveBeenCalled(); expect(grpc.loadPackageDefinition).toHaveBeenCalled(); - expect(grpc.ServerCredentials.createInsecure).not.toHaveBeenCalled(); + expect(grpc.ServerCredentials.createSsl).not.toHaveBeenCalled(); expect(wrapper).toBeDefined(); }); @@ -98,7 +102,7 @@ describe("CentralSystemWrapper", () => { wrapper.listen(); - expect(logger.infoMessage).toHaveBeenCalledWith( + expect(logger.errorMessage).toHaveBeenCalledWith( "Server bind error:", error ); @@ -131,7 +135,7 @@ describe("CentralSystemWrapper", () => { expect(logger.infoMessage).toHaveBeenCalledWith( "Client client123 ended stream." ); - expect(logger.infoMessage).toHaveBeenCalledWith( + expect(logger.errorMessage).toHaveBeenCalledWith( "Stream error from client client123:", expect.any(Error) ); diff --git a/Tokenization/backend/wrapper/src/test/client/Commands/newToken.test.ts b/Tokenization/backend/wrapper/src/test/client/Commands/newToken.test.ts index 829dff294..37e948cf5 100644 --- a/Tokenization/backend/wrapper/src/test/client/Commands/newToken.test.ts +++ b/Tokenization/backend/wrapper/src/test/client/Commands/newToken.test.ts @@ -24,6 +24,7 @@ import { import * as grpc from "@grpc/grpc-js"; import * as protoLoader from "@grpc/proto-loader"; import path from "path"; +import { getTestCerts } from "../../testCerts/testCerts"; /** * Helper to create a new token command with given address, direction, and token. @@ -87,7 +88,13 @@ describe("NewTokenHandler", () => { dir: ConnectionDirection, token: string ) { - const conn = new Connection(token, address, dir, peerCtor); + const conn = new Connection( + token, + address, + dir, + peerCtor, + getTestCerts() + ); if (dir === ConnectionDirection.SENDING) { this.sendingConnections.set(address, conn); } else { @@ -104,8 +111,10 @@ describe("NewTokenHandler", () => { "old-token", targetAddress, ConnectionDirection.SENDING, - peerCtor + peerCtor, + getTestCerts() ); + (manager as any).sendingConnections.set(targetAddress, conn); const handler = new NewTokenHandler(manager); diff --git a/Tokenization/backend/wrapper/src/test/client/Commands/revokeToken.test.ts b/Tokenization/backend/wrapper/src/test/client/Commands/revokeToken.test.ts index 2eb821e90..85eddcc8a 100644 --- a/Tokenization/backend/wrapper/src/test/client/Commands/revokeToken.test.ts +++ b/Tokenization/backend/wrapper/src/test/client/Commands/revokeToken.test.ts @@ -25,6 +25,7 @@ import { Command } from "models/commands.model"; import * as grpc from "@grpc/grpc-js"; import * as protoLoader from "@grpc/proto-loader"; import path from "path"; +import { getTestCerts } from "../../testCerts/testCerts"; describe("RevokeToken", () => { const protoPath = path.join( @@ -78,7 +79,8 @@ describe("RevokeToken", () => { "valid-token", targetAddress, ConnectionDirection.SENDING, - peerCtor + peerCtor, + getTestCerts() ); (manager as any).sendingConnections!.set(targetAddress, conn); @@ -99,7 +101,8 @@ describe("RevokeToken", () => { "valid-token", targetAddress, ConnectionDirection.RECEIVING, - peerCtor + peerCtor, + getTestCerts() ); (manager as any).receivingConnections.set(targetAddress, conn); diff --git a/Tokenization/backend/wrapper/src/test/client/ConnectionManager/ConnectionManager.test.ts b/Tokenization/backend/wrapper/src/test/client/ConnectionManager/ConnectionManager.test.ts index 5397e99fd..503b22a50 100644 --- a/Tokenization/backend/wrapper/src/test/client/ConnectionManager/ConnectionManager.test.ts +++ b/Tokenization/backend/wrapper/src/test/client/ConnectionManager/ConnectionManager.test.ts @@ -18,6 +18,10 @@ import { ConnectionDirection, DuplexMessageEvent, } from "../../../models/message.model"; +import { + getTestCentralCertPaths, + getTestCerts, +} from "../../testCerts/testCerts"; // Mock duplex stream const mockStream = { @@ -101,10 +105,10 @@ jest.mock("@grpc/grpc-js", () => { return { ...original, credentials: { - createInsecure: jest.fn(), + createSsl: jest.fn(() => "mock-credentials"), }, ServerCredentials: { - createInsecure: jest.fn(() => ({})), + createSsl: jest.fn(() => "mock-credentials"), }, status: { ...original.status, @@ -124,12 +128,19 @@ jest.mock("@grpc/grpc-js", () => { describe("ConnectionManager", () => { let conn: ConnectionManager; + const { caCertPath, certPath, keyPath } = getTestCentralCertPaths(); beforeEach(() => { jest.clearAllMocks(); capturedServerImpl = null; global.fetch = jest.fn(); - conn = new ConnectionManager("dummy.proto", "localhost:12345"); + conn = new ConnectionManager( + "dummy.proto", + "localhost:12345", + caCertPath, + certPath, + keyPath + ); }); afterAll(() => { @@ -142,7 +153,7 @@ describe("ConnectionManager", () => { expect(grpc.loadPackageDefinition).toHaveBeenCalled(); expect(CentralSystemMock).toHaveBeenCalledWith( "localhost:12345", - undefined + "mock-credentials" ); }); @@ -212,7 +223,12 @@ describe("ConnectionManager", () => { }); test("listenForPeers() should start server and register service", async () => { - await conn.listenForPeers(50055, "http://localhost:40041/api/"); + await conn.listenForPeers( + 50055, + getTestCerts().clientCert, + getTestCerts().caCert, + "http://localhost:40041/api/" + ); const serverCtor = (grpc.Server as any).mock; expect(serverCtor).toBeDefined(); @@ -232,7 +248,12 @@ describe("ConnectionManager", () => { }); test("p2p Fetch should register incoming receiving connection and forward request", async () => { - await conn.listenForPeers(50056, "http://localhost:40041/api/"); + await conn.listenForPeers( + 50056, + getTestCerts().clientCert, + getTestCerts().caCert, + "http://localhost:40041/api/" + ); // przygotuj dane wywołania const call = { @@ -290,7 +311,12 @@ describe("ConnectionManager", () => { }); test("p2p Fetch should return INTERNAL on forward error", async () => { - await conn.listenForPeers(50057, "http://localhost:40041/api/"); + await conn.listenForPeers( + 50057, + getTestCerts().clientCert, + getTestCerts().caCert, + "http://localhost:40041/api/" + ); const call = { getPeer: () => "client-error", diff --git a/Tokenization/backend/wrapper/src/test/testCerts/ca.crt b/Tokenization/backend/wrapper/src/test/testCerts/ca.crt new file mode 100644 index 000000000..f1ac950cf --- /dev/null +++ b/Tokenization/backend/wrapper/src/test/testCerts/ca.crt @@ -0,0 +1,12 @@ +-----BEGIN CERTIFICATE----- +MIIBrjCCAVWgAwIBAgIUDrk3s+DN4r4YrgKixgOD1bLk2HMwCgYIKoZIzj0EAwIw +RTEdMBsGA1UEAwwURXhhbXBsZSBUZXN0IFJvb3QgQ0ExFzAVBgNVBAoMDkV4YW1w +bGVDbyBUZXN0MQswCQYDVQQGEwJQTDAeFw0yNTA4MjYxNTIzNDVaFw0zNTA4MjQx +NTI4NDVaMEUxHTAbBgNVBAMMFEV4YW1wbGUgVGVzdCBSb290IENBMRcwFQYDVQQK +DA5FeGFtcGxlQ28gVGVzdDELMAkGA1UEBhMCUEwwWTATBgcqhkjOPQIBBggqhkjO +PQMBBwNCAASSEFtjeisuWn0K1xjr4gD9RS/kTMMBDGMfRzw6dT5Xc8e4FfpSSfQ8 +mZs+3UOf4/tDIzz40GP5ayYIgXZGq0yhoyMwITAPBgNVHRMBAf8EBTADAQH/MA4G +A1UdDwEB/wQEAwIBhjAKBggqhkjOPQQDAgNHADBEAiAgdtqDwf7ecyj5mfw1hASm +nu1wICbvS+vBTwL8c7gJdwIgdtZer+Vw3g/e+yK55rMwj3lzHdoZXgxZg7nY/wlW +Lr4= +-----END CERTIFICATE----- diff --git a/Tokenization/backend/wrapper/src/test/testCerts/centralSystem/central.system.svc.local.crt b/Tokenization/backend/wrapper/src/test/testCerts/centralSystem/central.system.svc.local.crt new file mode 100644 index 000000000..79a6e5b2f --- /dev/null +++ b/Tokenization/backend/wrapper/src/test/testCerts/centralSystem/central.system.svc.local.crt @@ -0,0 +1,14 @@ +-----BEGIN CERTIFICATE----- +MIICDzCCAbSgAwIBAgIUMT7kDfFrbxHKvOzcUH467BHIZKAwCgYIKoZIzj0EAwIw +RTEdMBsGA1UEAwwURXhhbXBsZSBUZXN0IFJvb3QgQ0ExFzAVBgNVBAoMDkV4YW1w +bGVDbyBUZXN0MQswCQYDVQQGEwJQTDAeFw0yNTA4MjYxNTIzNDVaFw0yNjA4MjYx +NTI4NDVaMEkxITAfBgNVBAMMGGNlbnRyYWwuc3lzdGVtLnN2Yy5sb2NhbDEXMBUG +A1UECgwORXhhbXBsZUNvIFRlc3QxCzAJBgNVBAYTAlBMMFkwEwYHKoZIzj0CAQYI +KoZIzj0DAQcDQgAEJBVRkwiBKUn8osDqgeaXAlbGB8oYd0Awre++dGL70zCYHOJ6 +Av/o31sTfB9wZUpL36mvzGPbNX9KjRAxQbBzHKN+MHwwDAYDVR0TAQH/BAIwADA9 +BgNVHREENjA0ghhjZW50cmFsLnN5c3RlbS5zdmMubG9jYWyCB2NlbnRyYWyCCWxv +Y2FsaG9zdIcEfwAAATAOBgNVHQ8BAf8EBAMCA6gwHQYDVR0lBBYwFAYIKwYBBQUH +AwEGCCsGAQUFBwMCMAoGCCqGSM49BAMCA0kAMEYCIQCLOWlisiuOzD6FI46PrXrl +YYnZgaB2pAropRK6XZln6gIhANq2AEqyFrwuzuw70yBIoIiv3Z/bIyuVmGfoopzs +uYo4 +-----END CERTIFICATE----- diff --git a/Tokenization/backend/wrapper/src/test/testCerts/centralSystem/central.system.svc.local.key b/Tokenization/backend/wrapper/src/test/testCerts/centralSystem/central.system.svc.local.key new file mode 100644 index 000000000..121c305a8 --- /dev/null +++ b/Tokenization/backend/wrapper/src/test/testCerts/centralSystem/central.system.svc.local.key @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEICiXVtz9kxU7b/gyOQpEC4z9ECNYhyKz6K63+pErBRBPoAoGCCqGSM49 +AwEHoUQDQgAEJBVRkwiBKUn8osDqgeaXAlbGB8oYd0Awre++dGL70zCYHOJ6Av/o +31sTfB9wZUpL36mvzGPbNX9KjRAxQbBzHA== +-----END EC PRIVATE KEY----- diff --git a/Tokenization/backend/wrapper/src/test/testCerts/clientListener/client-b.svc.local.crt b/Tokenization/backend/wrapper/src/test/testCerts/clientListener/client-b.svc.local.crt new file mode 100644 index 000000000..d32418936 --- /dev/null +++ b/Tokenization/backend/wrapper/src/test/testCerts/clientListener/client-b.svc.local.crt @@ -0,0 +1,13 @@ +-----BEGIN CERTIFICATE----- +MIIB8jCCAZigAwIBAgIUFWvpRzaOwyQTl013JHSZr/0ZXwQwCgYIKoZIzj0EAwIw +RTEdMBsGA1UEAwwURXhhbXBsZSBUZXN0IFJvb3QgQ0ExFzAVBgNVBAoMDkV4YW1w +bGVDbyBUZXN0MQswCQYDVQQGEwJQTDAeFw0yNTA4MjYxNTIzNDVaFw0yNjA4MjYx +NTI4NDVaMEMxGzAZBgNVBAMMEmNsaWVudC1iLnN2Yy5sb2NhbDEXMBUGA1UECgwO +RXhhbXBsZUNvIFRlc3QxCzAJBgNVBAYTAlBMMFkwEwYHKoZIzj0CAQYIKoZIzj0D +AQcDQgAE2XWeWQcFI9FSD0AhcGbtq4xBCKslLGbBJxmUwTw7jS8XuWbo2R/C2W8c +CoNoqacuXOABty601v7vymh0fVvDZKNoMGYwDAYDVR0TAQH/BAIwADAnBgNVHREE +IDAeghJjbGllbnQtYi5zdmMubG9jYWyCCGNsaWVudC1iMA4GA1UdDwEB/wQEAwID +qDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwCgYIKoZIzj0EAwIDSAAw +RQIgNyD6+Fmm+0IKtCZnaRJsfbMpep55K/zJCekJsh0u+agCIQDLxllwAs2xXtCT +2Tf3Zr1gQ3qM0V20KiXhq8aBkMiKXw== +-----END CERTIFICATE----- diff --git a/Tokenization/backend/wrapper/src/test/testCerts/clientListener/client-b.svc.local.key b/Tokenization/backend/wrapper/src/test/testCerts/clientListener/client-b.svc.local.key new file mode 100644 index 000000000..68dff890a --- /dev/null +++ b/Tokenization/backend/wrapper/src/test/testCerts/clientListener/client-b.svc.local.key @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIP7Nw0K+xSwzXOpN6SL1tZsQo++26LwqWQsi33RKONiRoAoGCCqGSM49 +AwEHoUQDQgAE2XWeWQcFI9FSD0AhcGbtq4xBCKslLGbBJxmUwTw7jS8XuWbo2R/C +2W8cCoNoqacuXOABty601v7vymh0fVvDZA== +-----END EC PRIVATE KEY----- diff --git a/Tokenization/backend/wrapper/src/test/testCerts/clientSender/client-a.svc.local.crt b/Tokenization/backend/wrapper/src/test/testCerts/clientSender/client-a.svc.local.crt new file mode 100644 index 000000000..a6d5025b6 --- /dev/null +++ b/Tokenization/backend/wrapper/src/test/testCerts/clientSender/client-a.svc.local.crt @@ -0,0 +1,13 @@ +-----BEGIN CERTIFICATE----- +MIIB8jCCAZigAwIBAgIUa4ibeAgx6Ga3UklyqWn8uEtEcQcwCgYIKoZIzj0EAwIw +RTEdMBsGA1UEAwwURXhhbXBsZSBUZXN0IFJvb3QgQ0ExFzAVBgNVBAoMDkV4YW1w +bGVDbyBUZXN0MQswCQYDVQQGEwJQTDAeFw0yNTA4MjYxNTIzNDVaFw0yNjA4MjYx +NTI4NDVaMEMxGzAZBgNVBAMMEmNsaWVudC1hLnN2Yy5sb2NhbDEXMBUGA1UECgwO +RXhhbXBsZUNvIFRlc3QxCzAJBgNVBAYTAlBMMFkwEwYHKoZIzj0CAQYIKoZIzj0D +AQcDQgAEU9voQyzju4VEGHJd/5KWROHG1ZqjP8ClUT8qxQ4fo7vQTQAitXYxr4sh +jfI4LvX63Yd/FzbTiFD3vlIJHdMrnqNoMGYwDAYDVR0TAQH/BAIwADAnBgNVHREE +IDAeghJjbGllbnQtYS5zdmMubG9jYWyCCGNsaWVudC1hMA4GA1UdDwEB/wQEAwID +qDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwCgYIKoZIzj0EAwIDSAAw +RQIgCqma8A91M48a9t4f/b38nCEgG2LQ0TRLiWqMw7G+3vwCIQDcMDbM9Beuh1ap +JudMwo61iUWxeF+TLgapxxO/R4jWqA== +-----END CERTIFICATE----- diff --git a/Tokenization/backend/wrapper/src/test/testCerts/clientSender/client-a.svc.local.key b/Tokenization/backend/wrapper/src/test/testCerts/clientSender/client-a.svc.local.key new file mode 100644 index 000000000..9531bc087 --- /dev/null +++ b/Tokenization/backend/wrapper/src/test/testCerts/clientSender/client-a.svc.local.key @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIL9ACEBWYWkPUs6t9ZbrWETL1kpsPe/pxKQIqnqHlWyroAoGCCqGSM49 +AwEHoUQDQgAEU9voQyzju4VEGHJd/5KWROHG1ZqjP8ClUT8qxQ4fo7vQTQAitXYx +r4shjfI4LvX63Yd/FzbTiFD3vlIJHdMrng== +-----END EC PRIVATE KEY----- diff --git a/Tokenization/backend/wrapper/src/test/testCerts/testCerts.ts b/Tokenization/backend/wrapper/src/test/testCerts/testCerts.ts new file mode 100644 index 000000000..c37742655 --- /dev/null +++ b/Tokenization/backend/wrapper/src/test/testCerts/testCerts.ts @@ -0,0 +1,78 @@ +import { CentralSystemConfig, gRPCWrapperConfig } from "models/config.model"; +import path from "path"; +import * as fs from "fs"; + +export const getTestCentralCertPaths = + (): CentralSystemConfig["serverCerts"] => { + const CA_CERT_PATH = path.join(__dirname, "./ca.crt"); + const SERVER_CERT_PATH = path.join( + __dirname, + "./centralSystem/central.system.svc.local.crt" + ); + const SERVER_KEY_PATH = path.join( + __dirname, + "./centralSystem/central.system.svc.local.key" + ); + + return { + caCertPath: CA_CERT_PATH, + certPath: SERVER_CERT_PATH, + keyPath: SERVER_KEY_PATH, + }; + }; + +export const getTestClientListenerCertPaths = + (): gRPCWrapperConfig["clientCerts"] => { + const CA_CERT_PATH = path.join(__dirname, "./ca.crt"); + const CLIENT_CERT_PATH = path.join( + __dirname, + "./clientListener/client-b.svc.local.crt" + ); + const CLIENT_KEY_PATH = path.join( + __dirname, + "./clientListener/client-b.svc.local.key" + ); + + return { + caCertPath: CA_CERT_PATH, + certPath: CLIENT_CERT_PATH, + keyPath: CLIENT_KEY_PATH, + }; + }; + +export const getTestClientSenderCertPaths = + (): gRPCWrapperConfig["clientCerts"] => { + const CA_CERT_PATH = path.join(__dirname, "./ca.crt"); + const CLIENT_CERT_PATH = path.join( + __dirname, + "./clientSender/client-a.svc.local.crt" + ); + const CLIENT_KEY_PATH = path.join( + __dirname, + "./clientSender/client-a.svc.local.key" + ); + + return { + caCertPath: CA_CERT_PATH, + certPath: CLIENT_CERT_PATH, + keyPath: CLIENT_KEY_PATH, + }; + }; + +export const getTestCerts = () => { + const CA_CERT_PATH = path.join(__dirname, "./ca.crt"); + const SERVER_CERT_PATH = path.join( + __dirname, + "./centralSystem/central.system.svc.local.crt" + ); + const SERVER_KEY_PATH = path.join( + __dirname, + "./centralSystem/central.system.svc.local.key" + ); + + const caCert = fs.readFileSync(CA_CERT_PATH); + const clientCert = fs.readFileSync(SERVER_CERT_PATH); + const clientKey = fs.readFileSync(SERVER_KEY_PATH); + + return { caCert, clientCert, clientKey }; +}; From 8c7bceb81e9efe7471b8e7ca5008db9f9356bf1f Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Thu, 25 Sep 2025 23:45:56 +0200 Subject: [PATCH 63/81] feat: add testing certificates --- .../backend/wrapper/src/test/testCerts/ca.crt | 39 ++++++++++++++----- .../centralSystem/central-system.crt | 28 +++++++++++++ .../centralSystem/central-system.key | 28 +++++++++++++ .../central.system.svc.local.crt | 14 ------- .../central.system.svc.local.key | 5 --- .../clientListener/client-b-client.crt | 28 +++++++++++++ .../testCerts/clientListener/client-b.key | 28 +++++++++++++ .../testCerts/clientListener/client-b.pub.pem | 9 +++++ .../clientListener/client-b.svc.local.crt | 13 ------- .../clientListener/client-b.svc.local.key | 5 --- .../clientListenerServer/client-b-server.crt | 27 +++++++++++++ .../clientListenerServer/client-b.key | 28 +++++++++++++ .../clientSender/client-a-client.crt | 28 +++++++++++++ .../clientSender/client-a-server.crt | 27 +++++++++++++ .../test/testCerts/clientSender/client-a.key | 28 +++++++++++++ .../testCerts/clientSender/client-a.pub.pem | 9 +++++ .../clientSender/client-a.svc.local.crt | 13 ------- .../clientSender/client-a.svc.local.key | 5 --- .../wrapper/src/test/testCerts/testCerts.ts | 34 +++++++++++----- 19 files changed, 322 insertions(+), 74 deletions(-) create mode 100644 Tokenization/backend/wrapper/src/test/testCerts/centralSystem/central-system.crt create mode 100644 Tokenization/backend/wrapper/src/test/testCerts/centralSystem/central-system.key delete mode 100644 Tokenization/backend/wrapper/src/test/testCerts/centralSystem/central.system.svc.local.crt delete mode 100644 Tokenization/backend/wrapper/src/test/testCerts/centralSystem/central.system.svc.local.key create mode 100644 Tokenization/backend/wrapper/src/test/testCerts/clientListener/client-b-client.crt create mode 100644 Tokenization/backend/wrapper/src/test/testCerts/clientListener/client-b.key create mode 100644 Tokenization/backend/wrapper/src/test/testCerts/clientListener/client-b.pub.pem delete mode 100644 Tokenization/backend/wrapper/src/test/testCerts/clientListener/client-b.svc.local.crt delete mode 100644 Tokenization/backend/wrapper/src/test/testCerts/clientListener/client-b.svc.local.key create mode 100644 Tokenization/backend/wrapper/src/test/testCerts/clientListenerServer/client-b-server.crt create mode 100644 Tokenization/backend/wrapper/src/test/testCerts/clientListenerServer/client-b.key create mode 100644 Tokenization/backend/wrapper/src/test/testCerts/clientSender/client-a-client.crt create mode 100644 Tokenization/backend/wrapper/src/test/testCerts/clientSender/client-a-server.crt create mode 100644 Tokenization/backend/wrapper/src/test/testCerts/clientSender/client-a.key create mode 100644 Tokenization/backend/wrapper/src/test/testCerts/clientSender/client-a.pub.pem delete mode 100644 Tokenization/backend/wrapper/src/test/testCerts/clientSender/client-a.svc.local.crt delete mode 100644 Tokenization/backend/wrapper/src/test/testCerts/clientSender/client-a.svc.local.key diff --git a/Tokenization/backend/wrapper/src/test/testCerts/ca.crt b/Tokenization/backend/wrapper/src/test/testCerts/ca.crt index f1ac950cf..899799b3c 100644 --- a/Tokenization/backend/wrapper/src/test/testCerts/ca.crt +++ b/Tokenization/backend/wrapper/src/test/testCerts/ca.crt @@ -1,12 +1,31 @@ -----BEGIN CERTIFICATE----- -MIIBrjCCAVWgAwIBAgIUDrk3s+DN4r4YrgKixgOD1bLk2HMwCgYIKoZIzj0EAwIw -RTEdMBsGA1UEAwwURXhhbXBsZSBUZXN0IFJvb3QgQ0ExFzAVBgNVBAoMDkV4YW1w -bGVDbyBUZXN0MQswCQYDVQQGEwJQTDAeFw0yNTA4MjYxNTIzNDVaFw0zNTA4MjQx -NTI4NDVaMEUxHTAbBgNVBAMMFEV4YW1wbGUgVGVzdCBSb290IENBMRcwFQYDVQQK -DA5FeGFtcGxlQ28gVGVzdDELMAkGA1UEBhMCUEwwWTATBgcqhkjOPQIBBggqhkjO -PQMBBwNCAASSEFtjeisuWn0K1xjr4gD9RS/kTMMBDGMfRzw6dT5Xc8e4FfpSSfQ8 -mZs+3UOf4/tDIzz40GP5ayYIgXZGq0yhoyMwITAPBgNVHRMBAf8EBTADAQH/MA4G -A1UdDwEB/wQEAwIBhjAKBggqhkjOPQQDAgNHADBEAiAgdtqDwf7ecyj5mfw1hASm -nu1wICbvS+vBTwL8c7gJdwIgdtZer+Vw3g/e+yK55rMwj3lzHdoZXgxZg7nY/wlW -Lr4= +MIIFWjCCA0KgAwIBAgIUA3wFlpIAu9PcCYrsZQwml1VBbBIwDQYJKoZIhvcNAQEL +BQAwMzELMAkGA1UEBhMCUEwxDTALBgNVBAoMBFRlc3QxFTATBgNVBAMMDFRlc3Qg +Um9vdCBDQTAeFw0yNTA5MjUyMDQyMDlaFw0zNTA5MjMyMDQyMDlaMDMxCzAJBgNV +BAYTAlBMMQ0wCwYDVQQKDARUZXN0MRUwEwYDVQQDDAxUZXN0IFJvb3QgQ0EwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDwQYsYLOQyRG5nXHLzTuAXIDJl +nV6eoAOsDItqGCiOVy9T6Y0g+4RcSf99i2DRsOVLGg/jQHpAmo765XVObPiEmE+f +rxgLclXqXNlRQdiaK9LRPIUF0TCcyFJGTVMzHTFt0jHEdiP/2++egoAA/pP1al4y +XwJjWkGgLU7GA/Eou2Gfdc7mxZ4ZtmxkEh7OEqSOVgR+qi1fTfXfTyXF+s1PG4pu +tGNCg/zTh1rlMS9kWcoTF4Bk/RU5nKZ1zOAWOgffd72JiWyoGSYy0Yk2e1fhGgG5 +cWMZCDiOCAdNgwTtVKn8IfTgoZJpXhoegMVH44CDkfb7bkp3ETGfMzKncv4v5C2P +11HjWYOrmQ1bgpy+lR9RR+7Oem9So49UsayqqAYmquOCycnGT9wOX+4qsalnZd/O +J4mHtmUGiK0Lkvfh3X5T7wE7yLuiYJtG4XYwREZkBlusxGyX0lRvWJq3lI93EFqt +p7UWtB+1OjabUpCadypzkbvA19DJ4fhzaPh+A3tfn42RnlVAYAazNRiAy90G4/Mh +MPZzKqe0DTp2i2WG5/NTEivKoSVD27vVKp5Tk4LhgAMmZ/F4uFT0TBGva4q+tEUW +jv6mLKpCtQzgqsjNCSKL5pWnymDme7UN/mGe1ttU8xrI9pyGA5tSb9kO0VkBCcVd +R1mOOjZKx0iR3ctG+QIDAQABo2YwZDASBgNVHRMBAf8ECDAGAQH/AgEBMA4GA1Ud +DwEB/wQEAwIBBjAdBgNVHQ4EFgQU41jbLgGadHiLx0921bLDou0kHQEwHwYDVR0j +BBgwFoAU41jbLgGadHiLx0921bLDou0kHQEwDQYJKoZIhvcNAQELBQADggIBAN+W +xYNn4nGtkEfNaVP46GckMQU8Lq6asXIj/L830JHq+R0gJeiURBqBs5snRqBSx6/W +3Xn+IfFGUttnEPlAkxXsg5R/JnQnggfyQX7Hk3SwedKH0uUqX7NLqAv7tfZJPGU9 +HSuvOzThvSX8N3Vr0zLVJDT94WqXX69HoSp6BZnriVss5RWMvM51QsNirj5379CX +iU7BCQdKBQfGSCQW0Qr4GWZYZHuhpXHcsfrrQ66krdqGLgkOxe3xQgvFQSWEf4OU +d/pqlRIOCnku4g2JR/ph+tuLtxmHdidNBjP27mrtrKx4MsaqimxAYOuHTkni8cLF +01IDq95txBs1fShWE5ritJh5b03ZVmDS0uiVH2IGPBmxz08ysJdUAm6uGJWg3D5X +nJBpJbqzYe6wrZDB48s0yZwo6FX5gfoAG6OR0iWfXMsOrpMOxFz/A739JjxcoFDT +P5qct/z92obgFqp0w/RN/8Dotaw00l5P1IenCE42fLuARelrS8jFKrrjUr2+0Occ +CJ/3us1j7Ln5gYWSlWHTjDRwSyaji3Gi4mnduQUsdkIpI7grh4FGULNOLOZZf3Rj +fKlP9kW5m7MB196MYjQrQXTZM1ZUY5yEeCspsb0UaD78Oq5qXSFfGFZ63BmxPMvi +RzP8neThIVB948nZ0GYMc3SIHBFvwQpFZgkuz2+0 -----END CERTIFICATE----- diff --git a/Tokenization/backend/wrapper/src/test/testCerts/centralSystem/central-system.crt b/Tokenization/backend/wrapper/src/test/testCerts/centralSystem/central-system.crt new file mode 100644 index 000000000..dd67e5dac --- /dev/null +++ b/Tokenization/backend/wrapper/src/test/testCerts/centralSystem/central-system.crt @@ -0,0 +1,28 @@ +-----BEGIN CERTIFICATE----- +MIIExzCCAq+gAwIBAgIUG2HcUzPbDD8biqumq+ISyohyeYIwDQYJKoZIhvcNAQEL +BQAwMzELMAkGA1UEBhMCUEwxDTALBgNVBAoMBFRlc3QxFTATBgNVBAMMDFRlc3Qg +Um9vdCBDQTAeFw0yNTA5MjUyMDQ5NDRaFw0yNjA5MjUyMDQ5NDRaMEkxCzAJBgNV +BAYTAlBMMRcwFQYDVQQKDA5FeGFtcGxlQ28gVGVzdDEhMB8GA1UEAwwYY2VudHJh +bC5zeXN0ZW0uc3ZjLmxvY2FsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEA6PdQdC5ol4niS1fiXwiJjEhfvPIAhj+LLLaHv1VG71sS9VmnsoEVJMfGqrkO +FkHrHFRFc1UJKi1se1r4NSlPsSPmTOj9KCgp5WSTxrww6X9hniWLCgC1S2fbmrWQ +O07D/qOGpO1GRgqL1KbVdHDZxhLa4MXevRnlgd2VcY1KaXT15BTQcJRJR+I9iIJF +HkUCdrLPjoJvS2G8gRezrRVC+EgrxTfJOQ2rcUunDDhn+f//cTulWjZ/R/Jy9Byy +qFTGPiwwnLkVBQGLhBSRNEYSvzxpgxipVOTLKPZYHNKyITibno3cYiaS+qCI0GG2 +wY7jrh44I6yQ/dCYMyuu7yxT0QIDAQABo4G8MIG5MAkGA1UdEwQCMAAwDgYDVR0P +AQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjA9BgNVHREE +NjA0ghhjZW50cmFsLnN5c3RlbS5zdmMubG9jYWyCB2NlbnRyYWyCCWxvY2FsaG9z +dIcEfwAAATAdBgNVHQ4EFgQUYLT4W+0UJcnlfU4VQMjbSHS5gYkwHwYDVR0jBBgw +FoAU41jbLgGadHiLx0921bLDou0kHQEwDQYJKoZIhvcNAQELBQADggIBALXQVFHu +z1GDyGNUNN5fEonbv4V8MGO8RU6I0O4ccuYqVGBvDUWVSMFtT4Qc60CcXZF2V8p6 +3FP6PAmDK12mMswWuc7lvgAldxOC+PwC4K8fbu9fl+KbP8lsEikVwqiLWXWOGiRh +xlgFhzYTvgEwa3Ta5hqQPsCnY5+/ybF4l7yxZgL0Qp/OWQJgtYd8AeAWpVayhpzw +WojpJ6x56PIZI9vJ00RmMOQib5fl6e4fKKj2ACt8uorG9kL/sWId2BnCJKFjSl8d +4krZr4ocGYK+yK7KgrunqAXy/NPk1hC/oRryaSznC3oh+83P3emjuf//t0FYhSUQ +g8Urku1v9916ulTM8DsF/eSr8z6BMod60fDrpaDnSY+4hcpJuOMfN1pOWcmQVejW +TgX+pwyKpRnvIOlm7NRz+gv31xMEu/McMQQ/oC9qYh7frOsO5pHt1kI7bJ2X69Dj +rz+y7SoW4Ur/pbevfyWu4kBMdo8Dj1zF2GwYFHDzjU0R814fBHEnkN5Jxnk+ahYl +yNwWnjphabPSGRx3nIgCJ600HvgAK1uKgdTCRoiYhkDxcve3m5wEE9UCgLrcp8HL +ushY02iXu9TsPuIA/3bBeLVeI0JxnyxYjP1YuGF2i2fxF/rZxhVXl12xc2oJAytA +6CRc2j0K3JcjgG5jdt3H36LwPnvQ9HPfF200 +-----END CERTIFICATE----- diff --git a/Tokenization/backend/wrapper/src/test/testCerts/centralSystem/central-system.key b/Tokenization/backend/wrapper/src/test/testCerts/centralSystem/central-system.key new file mode 100644 index 000000000..22044072d --- /dev/null +++ b/Tokenization/backend/wrapper/src/test/testCerts/centralSystem/central-system.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDo91B0LmiXieJL +V+JfCImMSF+88gCGP4sstoe/VUbvWxL1WaeygRUkx8aquQ4WQescVEVzVQkqLWx7 +Wvg1KU+xI+ZM6P0oKCnlZJPGvDDpf2GeJYsKALVLZ9uatZA7TsP+o4ak7UZGCovU +ptV0cNnGEtrgxd69GeWB3ZVxjUppdPXkFNBwlElH4j2IgkUeRQJ2ss+Ogm9LYbyB +F7OtFUL4SCvFN8k5DatxS6cMOGf5//9xO6VaNn9H8nL0HLKoVMY+LDCcuRUFAYuE +FJE0RhK/PGmDGKlU5Mso9lgc0rIhOJuejdxiJpL6oIjQYbbBjuOuHjgjrJD90Jgz +K67vLFPRAgMBAAECggEABeW00KwYE7X214drAJLbwIRYgBT0NHHJWSFpwEstV4PL +sBBL8XXZDixMeCflFmUmyXnMpEXDzKCHvXupCtd33/kTrGC9f9W8ccUhBIfhCRgj +ZXh305H/BOClK35rH0U4KusCzov/GmjL718lyiPNL3lstwHrSIguSiJM1SoJdy/l +aCIif6v5l8/DItDSavQxgI97AC0u7lLJadB460XqeJi3vYPzBg6WxEMMdqRhzzOH +1XIsv+IzHabJmt5J3wFsv2lk1v0Irny+CtWtZTM5mTVr4FcgefNx0t8pVSxRXf9F +DjXXbTSrlPVjZVPENrAr6Sl5YyJeK/UABiRl/BLxDQKBgQD3SQ0qZw41JsCFj6W7 +DGyKwFVNvFibzO2Hb6grHwV3iJKHnppFFpanEMhLiZzgdeTGpQFpR4iM6Ne8ewFQ +zu2P91cGjMBP4HqP/RWtGStZ6X/Br2I66sra6BXTsXGNXSr330xzYiwbDrKtr7rv +Q79ySfRRlwWpTeT6RubbqxB3dwKBgQDxLRPlVYaALeJ1rxuds59NeC5bJGHS8+1h +kiXQgKmG1/5saLUyWXWAT5FJqG/xvtiN+vtmf8jL+KhUYpga0kJeuykR7loJRGdr +7h4uMYmzrP7+5P6tNOBqawGCDZzutXMq2TIJAwy1s+tVE4KWWcv0qE8op4on+/f2 +8A+5HWNw9wKBgHGjU3aJ92B7l3uJQMsNcY/txQW9KScn7HwR1sFCNzvwOg4y14gq +Uj8iGjmEWuBXrTOQPm7IHbtLgWCvUjJ1dXx0WLy8z9+lNA2Za22ppF9kS36Rf129 +6kzg3K70207wYr+YEUTw933Tqk7g89HiW0dFLw6TjVl5X2GYVZzbJu0PAoGBAL6b +BLVkIXeeS/L8YJQDSOx+BgzsNQ/2zm4lhhNCDDlQ7XgaTNItF4s/1zBimY5yaU3U +xOmeJkDmFYsTnOjdsaySuIO+X5QhZqdLOrkBV7YUDDfBHXIgbxhL15ZEUfnql8mO +fFfY/CuCtYO4dqWC9Ik4l88mki7FmZSk55hCnLvDAoGBAKEjQS4GVd+9Ku/HVi71 +OG2vfKEyGfTyyOB/3c99BMWNOkQfMNrxuHR8XhTPi2LvFE9nndBlMuCLcRS417iL +Gvd7FazAaO1lRO1tlqOym1z2gx/j2k+2BrIV+vOzg3PEJeXZAihLHTSsfhNIc4xW +ZeR3z5nE4Pz9AOr/YFKi9Xqm +-----END PRIVATE KEY----- diff --git a/Tokenization/backend/wrapper/src/test/testCerts/centralSystem/central.system.svc.local.crt b/Tokenization/backend/wrapper/src/test/testCerts/centralSystem/central.system.svc.local.crt deleted file mode 100644 index 79a6e5b2f..000000000 --- a/Tokenization/backend/wrapper/src/test/testCerts/centralSystem/central.system.svc.local.crt +++ /dev/null @@ -1,14 +0,0 @@ ------BEGIN CERTIFICATE----- -MIICDzCCAbSgAwIBAgIUMT7kDfFrbxHKvOzcUH467BHIZKAwCgYIKoZIzj0EAwIw -RTEdMBsGA1UEAwwURXhhbXBsZSBUZXN0IFJvb3QgQ0ExFzAVBgNVBAoMDkV4YW1w -bGVDbyBUZXN0MQswCQYDVQQGEwJQTDAeFw0yNTA4MjYxNTIzNDVaFw0yNjA4MjYx -NTI4NDVaMEkxITAfBgNVBAMMGGNlbnRyYWwuc3lzdGVtLnN2Yy5sb2NhbDEXMBUG -A1UECgwORXhhbXBsZUNvIFRlc3QxCzAJBgNVBAYTAlBMMFkwEwYHKoZIzj0CAQYI -KoZIzj0DAQcDQgAEJBVRkwiBKUn8osDqgeaXAlbGB8oYd0Awre++dGL70zCYHOJ6 -Av/o31sTfB9wZUpL36mvzGPbNX9KjRAxQbBzHKN+MHwwDAYDVR0TAQH/BAIwADA9 -BgNVHREENjA0ghhjZW50cmFsLnN5c3RlbS5zdmMubG9jYWyCB2NlbnRyYWyCCWxv -Y2FsaG9zdIcEfwAAATAOBgNVHQ8BAf8EBAMCA6gwHQYDVR0lBBYwFAYIKwYBBQUH -AwEGCCsGAQUFBwMCMAoGCCqGSM49BAMCA0kAMEYCIQCLOWlisiuOzD6FI46PrXrl -YYnZgaB2pAropRK6XZln6gIhANq2AEqyFrwuzuw70yBIoIiv3Z/bIyuVmGfoopzs -uYo4 ------END CERTIFICATE----- diff --git a/Tokenization/backend/wrapper/src/test/testCerts/centralSystem/central.system.svc.local.key b/Tokenization/backend/wrapper/src/test/testCerts/centralSystem/central.system.svc.local.key deleted file mode 100644 index 121c305a8..000000000 --- a/Tokenization/backend/wrapper/src/test/testCerts/centralSystem/central.system.svc.local.key +++ /dev/null @@ -1,5 +0,0 @@ ------BEGIN EC PRIVATE KEY----- -MHcCAQEEICiXVtz9kxU7b/gyOQpEC4z9ECNYhyKz6K63+pErBRBPoAoGCCqGSM49 -AwEHoUQDQgAEJBVRkwiBKUn8osDqgeaXAlbGB8oYd0Awre++dGL70zCYHOJ6Av/o -31sTfB9wZUpL36mvzGPbNX9KjRAxQbBzHA== ------END EC PRIVATE KEY----- diff --git a/Tokenization/backend/wrapper/src/test/testCerts/clientListener/client-b-client.crt b/Tokenization/backend/wrapper/src/test/testCerts/clientListener/client-b-client.crt new file mode 100644 index 000000000..08482178c --- /dev/null +++ b/Tokenization/backend/wrapper/src/test/testCerts/clientListener/client-b-client.crt @@ -0,0 +1,28 @@ +-----BEGIN CERTIFICATE----- +MIIEuDCCAqCgAwIBAgIURLvrnhcyzZ0UTbuI1tHGoXXFWc0wDQYJKoZIhvcNAQEL +BQAwMzELMAkGA1UEBhMCUEwxDTALBgNVBAoMBFRlc3QxFTATBgNVBAMMDFRlc3Qg +Um9vdCBDQTAeFw0yNTA5MjUyMDU2MTdaFw0yNjA5MjUyMDU2MTdaMEYxCzAJBgNV +BAYTAlBMMRcwFQYDVQQKDA5FeGFtcGxlQ28gVGVzdDEeMBwGA1UEAwwVY2xpZW50 +LWItY2xpZW50LmxvY2FsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +xxqgLBJFeQgLcRvkPj6S16guSJsJN/MJxGAnmWQF3utr9neUXeID2KUEMNUT2b73 +EO/LD3z+8JvDhfG4J9Y1/ny/CuaAbmHKXH/ot0nIwWjd11yXjhWyXQuNqGHiuniX +2KaDH5+rIPXYU6eUjx+V3VX0iiYEFUizPRUhzmJ8AONSu43OuMdhZo7Frb4qUc4/ +ioVqhiAb3Sdm/nKDWI9OQR6Ux8Mc2OaPY3wQA4r3ZBz9oJU3G7BL85bclmk1PDZh +R6T0/oo09FiTj8zTZ8vooarJH+TAD2EBsTwfIdBT1yuECAueztxaos2q2TUsJz3f +6hQaEnOqiz6nZ8FceXudgQIDAQABo4GwMIGtMAkGA1UdEwQCMAAwDgYDVR0PAQH/ +BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMCMDsGA1UdEQQ0MDKCFWNsaWVudC1i +LWNsaWVudC5sb2NhbIIIY2xpZW50LWKCCWxvY2FsaG9zdIcEfwAABTAdBgNVHQ4E +FgQUTzAFrkegQMRXfjt4djXC5N4YHnYwHwYDVR0jBBgwFoAU41jbLgGadHiLx092 +1bLDou0kHQEwDQYJKoZIhvcNAQELBQADggIBAB+vXi5ThGYQt7RUx92Vjy2++Yiz +WztajTms/ac2B2JZbW9dl1PYv+t5hrkiXp8Q21eWq7ZPbd+7QbzvQsOxSGEKNBQp +0A4CeI67YkbYjJPzxjpDxiYo8+wmOtGgWRok9P4FLN5dGVo9/3JQHrn+3m20/6DT +FqTqm+mC3FkSjAJzPnd1wOmPxS2xENE/L5x4KHR91xkCeaHXjQky9gQs7vzY4uto +kiuB/elxFq2l4XN4BI6A65261AyelB15qpTKBGICGNO56hIcrGIjoFyYQnKyJrk4 +yylH0LtACnEV0lzS9l7FtRNERQ5xDBRvcqK7X7XCwkKqXfYN04STiVEEqhSoYDF2 +INJeV2FN7CJQueCuiKqG1S+zd7uXJ9a7dx5Qk3boJVBfX2WjK5BbiSpjoq+x7tCG +zwtHpBDaB8K6Ee0XICFcDZlPB214XaGjrx6iavs5ppP6261f20QMI3xtTVPapMjT +bmC2AoARTdaIxoQaSyCE+QImVxkhHYBePpIHAnAZhBPzF46u+APRAOpklTJ6J+uv +VWCwm71ebXUTSZ7alsScqH+zDmYMYpGuar3qcPHnOJDo0XDoSMYl3kLDrK0UoHci +mtjRzuYBlKyEX8LcqY9teVuarnvxaNtYGbF98L3nFSLGnUfzaCMIbA4xtJcZ7DXu +J9l3OBcmHYWG52f2 +-----END CERTIFICATE----- diff --git a/Tokenization/backend/wrapper/src/test/testCerts/clientListener/client-b.key b/Tokenization/backend/wrapper/src/test/testCerts/clientListener/client-b.key new file mode 100644 index 000000000..b2fc830da --- /dev/null +++ b/Tokenization/backend/wrapper/src/test/testCerts/clientListener/client-b.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDHGqAsEkV5CAtx +G+Q+PpLXqC5Imwk38wnEYCeZZAXe62v2d5Rd4gPYpQQw1RPZvvcQ78sPfP7wm8OF +8bgn1jX+fL8K5oBuYcpcf+i3ScjBaN3XXJeOFbJdC42oYeK6eJfYpoMfn6sg9dhT +p5SPH5XdVfSKJgQVSLM9FSHOYnwA41K7jc64x2FmjsWtvipRzj+KhWqGIBvdJ2b+ +coNYj05BHpTHwxzY5o9jfBADivdkHP2glTcbsEvzltyWaTU8NmFHpPT+ijT0WJOP +zNNny+ihqskf5MAPYQGxPB8h0FPXK4QIC57O3FqizarZNSwnPd/qFBoSc6qLPqdn +wVx5e52BAgMBAAECggEAA0i8JZ3ziWiJj8cO/7vWfjom8UmlYEfg/F09qfkNY7zs +Xfdg+h91QsiOBiQtnKTavGvIJKxCJEPdeMMg739ICreSCyL8MVXpmZb+hq9v4UjS +h+/eDBjthT1gi8t5iuvcTVWJyia/Et8bP13/RFEYDruROgogfR1i33oOwbG8K+OM +iDSIRo9swBaVNqFuWZKqZr1pjY9KUfh4jnSA+x1hueVHREJYt6qvX2KNhHFhyfyo +RRuLSKmz7Xx3sTu7qQD1NRKsD/8lM4oZWGPTit0F6qx+FfzbI0nBn9In8czmrPcm +VqwdgT5aR5I6lfzAj6kMHlmY2n0f4J0Hk6Ha6MgkJQKBgQD6AnjeToUw6PcIbTrJ +18mDfzOCMhQjQHDXipHrJ06K65B+4XX8/oTkRahyeqn4sbKKHP11e6bsDPVNRK0P +9G3wYw7nPNcGtF1pojWvh0HxBTJ/iugDgZT5ngtwbydvAT7+m6De3DicEJo5ZUPP +IAweL+qf9Nh30PtKiOSgJ2GrRwKBgQDL3+eX0sQg+dfYb3zAJmsLwXoY4fAOGc1o +KI4UI26Bq4TPrJv9guaDjSYNE62M7+H7vWudlG+KPxt6jNaxxpIo5J1c7qVl+gx6 +kTDZ053peVLtWJzLrRT9/e238bCYnKfFpKwRzUGT9kEkMvmFqb56lnhKBXS/OPuP +3dAOWHvE9wKBgAJM4YXSHSGdEyDNuHvA84a1NekdwtesMR2alcsfGnbmwfaY5ngE +c36SMYGUJVo3cFga+i4JjDihyeQDHMCH1DchAjMYeTYDlNRy/KF30iCAlr1brtTR +bWh6jspjC27XCRhYoDtMtWyiLnkWuHAAcHwansMIArHfh2BhMBFVK23jAoGAGwrf +GFdfppQdWlsnbAFsj4mhXW2SvvwTL+65MdilTtPmcPmPU2gqlWaClpd2nMww6Ihu +nt9SkD7gsTe/PqN9PaldajdJfyZUw2lA1pPoTVDHfC4V1jpmH26wOob3ira01lWK +cW4NdcfjSh7s1Br45h/RYtgobTjsvV+Jum1oNW8CgYAeY82AEydYzqjZRH66xAjQ +pSxaT6B2YS5JqJFbRAtHA/ndApHbW7ALX5vqoJ6EkJ+aORQQMZUkX32n3Y0WFNE2 +FR9JEcnBjimwmG+JTBRCk1luamsZZpH/sHZohH1bsq9+dGo9xIeeBdi5DPxpdsKM +6NuWljVhDbuJeRiamlo5tg== +-----END PRIVATE KEY----- diff --git a/Tokenization/backend/wrapper/src/test/testCerts/clientListener/client-b.pub.pem b/Tokenization/backend/wrapper/src/test/testCerts/clientListener/client-b.pub.pem new file mode 100644 index 000000000..cf74b90e7 --- /dev/null +++ b/Tokenization/backend/wrapper/src/test/testCerts/clientListener/client-b.pub.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxxqgLBJFeQgLcRvkPj6S +16guSJsJN/MJxGAnmWQF3utr9neUXeID2KUEMNUT2b73EO/LD3z+8JvDhfG4J9Y1 +/ny/CuaAbmHKXH/ot0nIwWjd11yXjhWyXQuNqGHiuniX2KaDH5+rIPXYU6eUjx+V +3VX0iiYEFUizPRUhzmJ8AONSu43OuMdhZo7Frb4qUc4/ioVqhiAb3Sdm/nKDWI9O +QR6Ux8Mc2OaPY3wQA4r3ZBz9oJU3G7BL85bclmk1PDZhR6T0/oo09FiTj8zTZ8vo +oarJH+TAD2EBsTwfIdBT1yuECAueztxaos2q2TUsJz3f6hQaEnOqiz6nZ8FceXud +gQIDAQAB +-----END PUBLIC KEY----- diff --git a/Tokenization/backend/wrapper/src/test/testCerts/clientListener/client-b.svc.local.crt b/Tokenization/backend/wrapper/src/test/testCerts/clientListener/client-b.svc.local.crt deleted file mode 100644 index d32418936..000000000 --- a/Tokenization/backend/wrapper/src/test/testCerts/clientListener/client-b.svc.local.crt +++ /dev/null @@ -1,13 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIB8jCCAZigAwIBAgIUFWvpRzaOwyQTl013JHSZr/0ZXwQwCgYIKoZIzj0EAwIw -RTEdMBsGA1UEAwwURXhhbXBsZSBUZXN0IFJvb3QgQ0ExFzAVBgNVBAoMDkV4YW1w -bGVDbyBUZXN0MQswCQYDVQQGEwJQTDAeFw0yNTA4MjYxNTIzNDVaFw0yNjA4MjYx -NTI4NDVaMEMxGzAZBgNVBAMMEmNsaWVudC1iLnN2Yy5sb2NhbDEXMBUGA1UECgwO -RXhhbXBsZUNvIFRlc3QxCzAJBgNVBAYTAlBMMFkwEwYHKoZIzj0CAQYIKoZIzj0D -AQcDQgAE2XWeWQcFI9FSD0AhcGbtq4xBCKslLGbBJxmUwTw7jS8XuWbo2R/C2W8c -CoNoqacuXOABty601v7vymh0fVvDZKNoMGYwDAYDVR0TAQH/BAIwADAnBgNVHREE -IDAeghJjbGllbnQtYi5zdmMubG9jYWyCCGNsaWVudC1iMA4GA1UdDwEB/wQEAwID -qDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwCgYIKoZIzj0EAwIDSAAw -RQIgNyD6+Fmm+0IKtCZnaRJsfbMpep55K/zJCekJsh0u+agCIQDLxllwAs2xXtCT -2Tf3Zr1gQ3qM0V20KiXhq8aBkMiKXw== ------END CERTIFICATE----- diff --git a/Tokenization/backend/wrapper/src/test/testCerts/clientListener/client-b.svc.local.key b/Tokenization/backend/wrapper/src/test/testCerts/clientListener/client-b.svc.local.key deleted file mode 100644 index 68dff890a..000000000 --- a/Tokenization/backend/wrapper/src/test/testCerts/clientListener/client-b.svc.local.key +++ /dev/null @@ -1,5 +0,0 @@ ------BEGIN EC PRIVATE KEY----- -MHcCAQEEIP7Nw0K+xSwzXOpN6SL1tZsQo++26LwqWQsi33RKONiRoAoGCCqGSM49 -AwEHoUQDQgAE2XWeWQcFI9FSD0AhcGbtq4xBCKslLGbBJxmUwTw7jS8XuWbo2R/C -2W8cCoNoqacuXOABty601v7vymh0fVvDZA== ------END EC PRIVATE KEY----- diff --git a/Tokenization/backend/wrapper/src/test/testCerts/clientListenerServer/client-b-server.crt b/Tokenization/backend/wrapper/src/test/testCerts/clientListenerServer/client-b-server.crt new file mode 100644 index 000000000..949b07297 --- /dev/null +++ b/Tokenization/backend/wrapper/src/test/testCerts/clientListenerServer/client-b-server.crt @@ -0,0 +1,27 @@ +-----BEGIN CERTIFICATE----- +MIIEqjCCApKgAwIBAgIUSKJ0QtJi4yK32QhGXdgKbfADC3owDQYJKoZIhvcNAQEL +BQAwMzELMAkGA1UEBhMCUEwxDTALBgNVBAoMBFRlc3QxFTATBgNVBAMMDFRlc3Qg +Um9vdCBDQTAeFw0yNTA5MjUyMDU2MDhaFw0yNjA5MjUyMDU2MDhaMD8xCzAJBgNV +BAYTAlBMMRcwFQYDVQQKDA5FeGFtcGxlQ28gVGVzdDEXMBUGA1UEAwwOY2xpZW50 +LWIubG9jYWwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDHGqAsEkV5 +CAtxG+Q+PpLXqC5Imwk38wnEYCeZZAXe62v2d5Rd4gPYpQQw1RPZvvcQ78sPfP7w +m8OF8bgn1jX+fL8K5oBuYcpcf+i3ScjBaN3XXJeOFbJdC42oYeK6eJfYpoMfn6sg +9dhTp5SPH5XdVfSKJgQVSLM9FSHOYnwA41K7jc64x2FmjsWtvipRzj+KhWqGIBvd +J2b+coNYj05BHpTHwxzY5o9jfBADivdkHP2glTcbsEvzltyWaTU8NmFHpPT+ijT0 +WJOPzNNny+ihqskf5MAPYQGxPB8h0FPXK4QIC57O3FqizarZNSwnPd/qFBoSc6qL +PqdnwVx5e52BAgMBAAGjgakwgaYwCQYDVR0TBAIwADAOBgNVHQ8BAf8EBAMCBaAw +EwYDVR0lBAwwCgYIKwYBBQUHAwEwNAYDVR0RBC0wK4IOY2xpZW50LWIubG9jYWyC +CGNsaWVudC1igglsb2NhbGhvc3SHBH8AAAQwHQYDVR0OBBYEFE8wBa5HoEDEV347 +eHY1wuTeGB52MB8GA1UdIwQYMBaAFONY2y4BmnR4i8dPdtWyw6LtJB0BMA0GCSqG +SIb3DQEBCwUAA4ICAQAfNbYmzhNKTJT+e4VZaJdqxFmsm2oHUtRXHVKHcPKYZEd5 +ujKtIjbdhjQ82Rhfmof9cydvAK8qEm+ydwUBvN/9q7Dd4V3rafKbsrVizB63HbSl +AZujvRxwIKF9Gzc3Sqliy1/LZYfk+FHHooUtzmL/K5cTVlHaBqT8m4zmqp4djFjQ +YnshmdaMBmgmgluO4/JyPswFHpRlKcp29GA8n39/+25yFyiIunryypCwPAFb/owh +sXVshhs04+JUwEdWGHoesbhjbIik706poPOlvUf9xHDcB6PXIwPmo08+1u2QEaV3 +Dqw4TjNcUA7OJdxzKhF0J4tVXAD1Hg2yrOYedtTeXDPntjgb3Uq4DWnAAJ+fMF1U +T1vJogzgzq6y5jl0KClqpSA9dOKt4IG2hL5WcoudyTk0ao4wkVdOQwR1vNz25Fub +LSl582PpHxvK3GYk4PegoPnHzz02IN4B+AxUDvXPH4HIZ4QtubnX4x4j8Kk4bPCK +ZBBA3t8K/6W/bjOB0Xh+LyXf9dIndVmFHC0iOf8YQgyNHhXhzpbpM/LWdDk3aEiG +6y99wNdWuu7F3T7wFk7dDhxxroHqxUjsP8921LD0JDqdryWQK6wE23fSpiThOEDN +o2FnBG3514QI/v6zlUWY4LtqJO2UCwPLbWAsuXd+AxWOO3urg0ucW0r1UNzmIQ== +-----END CERTIFICATE----- diff --git a/Tokenization/backend/wrapper/src/test/testCerts/clientListenerServer/client-b.key b/Tokenization/backend/wrapper/src/test/testCerts/clientListenerServer/client-b.key new file mode 100644 index 000000000..b2fc830da --- /dev/null +++ b/Tokenization/backend/wrapper/src/test/testCerts/clientListenerServer/client-b.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDHGqAsEkV5CAtx +G+Q+PpLXqC5Imwk38wnEYCeZZAXe62v2d5Rd4gPYpQQw1RPZvvcQ78sPfP7wm8OF +8bgn1jX+fL8K5oBuYcpcf+i3ScjBaN3XXJeOFbJdC42oYeK6eJfYpoMfn6sg9dhT +p5SPH5XdVfSKJgQVSLM9FSHOYnwA41K7jc64x2FmjsWtvipRzj+KhWqGIBvdJ2b+ +coNYj05BHpTHwxzY5o9jfBADivdkHP2glTcbsEvzltyWaTU8NmFHpPT+ijT0WJOP +zNNny+ihqskf5MAPYQGxPB8h0FPXK4QIC57O3FqizarZNSwnPd/qFBoSc6qLPqdn +wVx5e52BAgMBAAECggEAA0i8JZ3ziWiJj8cO/7vWfjom8UmlYEfg/F09qfkNY7zs +Xfdg+h91QsiOBiQtnKTavGvIJKxCJEPdeMMg739ICreSCyL8MVXpmZb+hq9v4UjS +h+/eDBjthT1gi8t5iuvcTVWJyia/Et8bP13/RFEYDruROgogfR1i33oOwbG8K+OM +iDSIRo9swBaVNqFuWZKqZr1pjY9KUfh4jnSA+x1hueVHREJYt6qvX2KNhHFhyfyo +RRuLSKmz7Xx3sTu7qQD1NRKsD/8lM4oZWGPTit0F6qx+FfzbI0nBn9In8czmrPcm +VqwdgT5aR5I6lfzAj6kMHlmY2n0f4J0Hk6Ha6MgkJQKBgQD6AnjeToUw6PcIbTrJ +18mDfzOCMhQjQHDXipHrJ06K65B+4XX8/oTkRahyeqn4sbKKHP11e6bsDPVNRK0P +9G3wYw7nPNcGtF1pojWvh0HxBTJ/iugDgZT5ngtwbydvAT7+m6De3DicEJo5ZUPP +IAweL+qf9Nh30PtKiOSgJ2GrRwKBgQDL3+eX0sQg+dfYb3zAJmsLwXoY4fAOGc1o +KI4UI26Bq4TPrJv9guaDjSYNE62M7+H7vWudlG+KPxt6jNaxxpIo5J1c7qVl+gx6 +kTDZ053peVLtWJzLrRT9/e238bCYnKfFpKwRzUGT9kEkMvmFqb56lnhKBXS/OPuP +3dAOWHvE9wKBgAJM4YXSHSGdEyDNuHvA84a1NekdwtesMR2alcsfGnbmwfaY5ngE +c36SMYGUJVo3cFga+i4JjDihyeQDHMCH1DchAjMYeTYDlNRy/KF30iCAlr1brtTR +bWh6jspjC27XCRhYoDtMtWyiLnkWuHAAcHwansMIArHfh2BhMBFVK23jAoGAGwrf +GFdfppQdWlsnbAFsj4mhXW2SvvwTL+65MdilTtPmcPmPU2gqlWaClpd2nMww6Ihu +nt9SkD7gsTe/PqN9PaldajdJfyZUw2lA1pPoTVDHfC4V1jpmH26wOob3ira01lWK +cW4NdcfjSh7s1Br45h/RYtgobTjsvV+Jum1oNW8CgYAeY82AEydYzqjZRH66xAjQ +pSxaT6B2YS5JqJFbRAtHA/ndApHbW7ALX5vqoJ6EkJ+aORQQMZUkX32n3Y0WFNE2 +FR9JEcnBjimwmG+JTBRCk1luamsZZpH/sHZohH1bsq9+dGo9xIeeBdi5DPxpdsKM +6NuWljVhDbuJeRiamlo5tg== +-----END PRIVATE KEY----- diff --git a/Tokenization/backend/wrapper/src/test/testCerts/clientSender/client-a-client.crt b/Tokenization/backend/wrapper/src/test/testCerts/clientSender/client-a-client.crt new file mode 100644 index 000000000..72ddb5561 --- /dev/null +++ b/Tokenization/backend/wrapper/src/test/testCerts/clientSender/client-a-client.crt @@ -0,0 +1,28 @@ +-----BEGIN CERTIFICATE----- +MIIEuDCCAqCgAwIBAgIUTk9RrYkrtmjMiKjWusjyURgmSUEwDQYJKoZIhvcNAQEL +BQAwMzELMAkGA1UEBhMCUEwxDTALBgNVBAoMBFRlc3QxFTATBgNVBAMMDFRlc3Qg +Um9vdCBDQTAeFw0yNTA5MjUyMDU1MTVaFw0yNjA5MjUyMDU1MTVaMEYxCzAJBgNV +BAYTAlBMMRcwFQYDVQQKDA5FeGFtcGxlQ28gVGVzdDEeMBwGA1UEAwwVY2xpZW50 +LWEtY2xpZW50LmxvY2FsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +nJySLgwWLpLbt8EqFDfhKciKJGlxRUfcn/2u2EyxnkpKwwiLDSzKguZZ50xKviDt +Jao2FuZ251uyJDTfuPYt549OklkKIQYFSmP4MxNDAz501TVSJ45a9WQugScGc9lk +invmIADGBEa0rj2keRkT4MYvnWT2IGQJ91N99g9tDoQVPem5naHU1PxIwyxVVRIv +6mVCaro6OULqx6iFvDffvL0ef/5lbt8+vqX4QWwPH8rF5CvaV1KYYMkvQEXSn615 +8FE2YUepEN6wGEEDAIr98D5vNlgabkpLzxDULFT+tdk7v6Shb7bKql1W0QlBkEh/ +mFDCEkqOesGXLuzPG8pVAwIDAQABo4GwMIGtMAkGA1UdEwQCMAAwDgYDVR0PAQH/ +BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMCMDsGA1UdEQQ0MDKCFWNsaWVudC1h +LWNsaWVudC5sb2NhbIIIY2xpZW50LWGCCWxvY2FsaG9zdIcEfwAAAzAdBgNVHQ4E +FgQUScQWL/kot0/d3H9JQ9sZsO3xniAwHwYDVR0jBBgwFoAU41jbLgGadHiLx092 +1bLDou0kHQEwDQYJKoZIhvcNAQELBQADggIBALVJfl1YWb3mrXT51+pZ4pYJfY0Z +iXCGAMVLUtdnhgMmX5GVJhWvCAt+vwHggOJ+EA+1vw5CRe/GLDa5QurOtLtH6uBt +OjenYmOKRZ5f+mG8ZR20PyhH70m8DZ01OGlfuieFb8KgyYtEFNLCFZWxVDlvdwNL +2HVPSjM0JYudBELnTs4N8YZUTLGDRDZ9sz/KQYMJrSojN45k05qqr/EXWHwVDzVL +LefvuKi6H6DLzmU+oDy9TRcCydV4/h6i3MUxWm/IBrgoKIg4fmd/Evnen9KSc6md +yMHoKR6iHcha526txtUu4w0/0Le45yYxE1/eNm5jfMNwTFuoTbmlk+lItXd5Upn7 +1Pk+TF81WLl8prdvVoZPVYaKbj80JQlleQlWu76SaaY9ofkbRwP85npJmdl0wDCu +Bmil+ziBxzK73TT+UMBbRKRmXdeEh3fjQ8X4qlRNVPEarj1f2UiZ+45G/eFogA2r +EtVTqQ5MetNtWssgK0GFf2KeUIfXRdvYuFvLOhcd7uccxThq+o9KDFIulPzhy6uq +nu2AS8NELydQeHh6GjKqsxNoMS5l+YSzTGPvFWTYqzfRH5+h2J0H8Oex2Grb5C9A +35F8f35zLViv8C9mU32W9bSgcJElKaOumgBLbRtfrzHesFBFyOkTbtWnKJJJwXQH +7QZyDraKRsXdHAMr +-----END CERTIFICATE----- diff --git a/Tokenization/backend/wrapper/src/test/testCerts/clientSender/client-a-server.crt b/Tokenization/backend/wrapper/src/test/testCerts/clientSender/client-a-server.crt new file mode 100644 index 000000000..23fc77a5d --- /dev/null +++ b/Tokenization/backend/wrapper/src/test/testCerts/clientSender/client-a-server.crt @@ -0,0 +1,27 @@ +-----BEGIN CERTIFICATE----- +MIIEqjCCApKgAwIBAgIULDdlrJiTv9xZIx3QqggR9bBIgwswDQYJKoZIhvcNAQEL +BQAwMzELMAkGA1UEBhMCUEwxDTALBgNVBAoMBFRlc3QxFTATBgNVBAMMDFRlc3Qg +Um9vdCBDQTAeFw0yNTA5MjUyMDU1MDJaFw0yNjA5MjUyMDU1MDJaMD8xCzAJBgNV +BAYTAlBMMRcwFQYDVQQKDA5FeGFtcGxlQ28gVGVzdDEXMBUGA1UEAwwOY2xpZW50 +LWEubG9jYWwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCcnJIuDBYu +ktu3wSoUN+EpyIokaXFFR9yf/a7YTLGeSkrDCIsNLMqC5lnnTEq+IO0lqjYW5nbn +W7IkNN+49i3nj06SWQohBgVKY/gzE0MDPnTVNVInjlr1ZC6BJwZz2WSKe+YgAMYE +RrSuPaR5GRPgxi+dZPYgZAn3U332D20OhBU96bmdodTU/EjDLFVVEi/qZUJqujo5 +QurHqIW8N9+8vR5//mVu3z6+pfhBbA8fysXkK9pXUphgyS9ARdKfrXnwUTZhR6kQ +3rAYQQMAiv3wPm82WBpuSkvPENQsVP612Tu/pKFvtsqqXVbRCUGQSH+YUMISSo56 +wZcu7M8bylUDAgMBAAGjgakwgaYwCQYDVR0TBAIwADAOBgNVHQ8BAf8EBAMCBaAw +EwYDVR0lBAwwCgYIKwYBBQUHAwEwNAYDVR0RBC0wK4IOY2xpZW50LWEubG9jYWyC +CGNsaWVudC1hgglsb2NhbGhvc3SHBH8AAAIwHQYDVR0OBBYEFEnEFi/5KLdP3dx/ +SUPbGbDt8Z4gMB8GA1UdIwQYMBaAFONY2y4BmnR4i8dPdtWyw6LtJB0BMA0GCSqG +SIb3DQEBCwUAA4ICAQAKtTGDurzAkajMDN+WqhiA6daqIstsRzLz9VnBwqIlWcOr +c1As4ah+YZSf2Qw1AMZ387fpk4oF2QZD4ZG7kigZdn5ricFVhBRMZUzJV1ommu2H +8Mub+oRyKQ/TtRqkq1JJqLKz7rDxBMM9LxSBPR4Nj2C4IVioxI5KYXYxlmqMeoYA +sMglGi8c3loRSy9LNwvcQu+UPI6kcFG+J0rfXJlWx10GRWIURudXt8oAAIVBLvSt +HR29TXWjOTULwqun0y5V4eksJek5jEhGTWuODAdPmCSSjAE4VSLECex/jql6jNFB +zmE9Q7vcss4zR9TASMeJYT3S+mXVb9sNf4ps+9rhx63tluSCH1vwtpMoQXucbIgo +tBUz+5gCIA7n1bMUJ8b1MajnTVH0nJa1ZWi0zTYnSd6WL0S0Se5exZ5Ws1ZWnFl9 +lVPCn2Mt8agRu0s0VAT7t4nY4VjHTDqjj9Z99tcfUWCO8gAAR28kkqdRYxrgVMkx +pv8IwTt0tBldDnpwdCqBnXP75sta4Gq7IOpe0oQB6kizWqbII84tYSxUch9SkkaH +rE7BhGtUywAJxc+dnAFuePuu6BE2ZsQK86FpuHYIxR6DU7hH1i8258qxGt0/EVBg +ekhyT6tFaAWl5N+OVmEu1JvNdqNiw6sJc+xy9AcviWlAOvkGd2Aw0eTRNQLboQ== +-----END CERTIFICATE----- diff --git a/Tokenization/backend/wrapper/src/test/testCerts/clientSender/client-a.key b/Tokenization/backend/wrapper/src/test/testCerts/clientSender/client-a.key new file mode 100644 index 000000000..7377803ae --- /dev/null +++ b/Tokenization/backend/wrapper/src/test/testCerts/clientSender/client-a.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCcnJIuDBYuktu3 +wSoUN+EpyIokaXFFR9yf/a7YTLGeSkrDCIsNLMqC5lnnTEq+IO0lqjYW5nbnW7Ik +NN+49i3nj06SWQohBgVKY/gzE0MDPnTVNVInjlr1ZC6BJwZz2WSKe+YgAMYERrSu +PaR5GRPgxi+dZPYgZAn3U332D20OhBU96bmdodTU/EjDLFVVEi/qZUJqujo5QurH +qIW8N9+8vR5//mVu3z6+pfhBbA8fysXkK9pXUphgyS9ARdKfrXnwUTZhR6kQ3rAY +QQMAiv3wPm82WBpuSkvPENQsVP612Tu/pKFvtsqqXVbRCUGQSH+YUMISSo56wZcu +7M8bylUDAgMBAAECggEANyBDsjKt8inebjFvmttKhfchbQyygsT3S1ez3k4srT+Q +TlNpArOz+tyTY7+uhXs4jmv6CxiHXQuhSm5UG5qH8Py4FvqBfrtMTHGg8XWDvpYS +8OOKbgMFUGA5oFt4wXmRks9m4vfyu5mZysVG6htiLFoGc5wQqLkd6vF4Io8uf4+A +Dd/oY85K7x/JnrNQaF7LmpeqTMRlhtzSzGNVqEwT9b9FoQRADGHrg1ksY5xXixAw +Jf+wvJqd6AaoxzK0rDuZiQpVnjepJ6aeip61RL7coc+yAlkXhfDOWvzWCefRFUTf +Q/iTyOset8ejRg2mF8InLUKYC2kwKLeucVvFkijz2QKBgQDbgf5L2aK/lUEQVtQf +ueqCc89PJZHU34tEXRC0doGYLQnhZQPVvLeTv+CkZ7GibUxPiMLdtzzfyFxlLbs+ +gUxBXV540hI3acpDqdiiZmHeYtMPvyjUkkP1ymQSnLwwjOkBh7Wt/+zMBEHtO8hz +Mo5vfoV6JV+b+JQg2f7cyd1t2wKBgQC2pclNJ1Dj58Vgmfd2EZhZgG0XRVPfR7PT +QjEIcnFmmNvEiN83dYqYw8fVOegcXrCFMTP3aW6ONoGnk+owhTSBtEfYNCKOwIi9 +GHM+MJFNIgxxPM+xSvpxHtAF9meYFkMRqjMJPM8ICz04Uz3AsfbdveYwUNMudSap +znigNKfh+QKBgDCvvH+GXhqwOCYvnA0NZ35XwXuEkbvteS5IlhPw1P2zv6VGins1 +yGH1BRZyCWxFYc+iPdZ/dfkMr7GhWw6aDxfQZcvWjEPOKxam7W3X141DzhyIAb5k +Ur6JjXizWupJ1sSIHTvir9rwds7vm54xcHY6UdCtyW8Gy5QdxfGitIJRAoGAbJ2I +aT5RJ0bEJJ9K/saV39u0hBsxNl2QfbgmKozMDSQnxOdUPsnCgvgiVRXbh0t0E7Df +42iqWx3k2n/my7XbNKq98r+GMXgjmLf6iGgfcEwoNAriw97/sdeOA421q0bJ2a5q +LTshLvpoDJ/L4FS0psbwJZlbDIyUUnS7XSITGBkCgYBNvMefT86P85yN5nDyxRjO +lCitw08NjE+6WZWL6BTbRqVovFG2HAaWGEjG2+bpApy4S4AN3NmmOby38ZZDR7bJ +bQxCHRt61yqX6IphkRrzv8K7DbrN3jnKO2FN8TBWwcvFzx2d5hl8Nsv2OmEyixZZ +ySbU4WOBCdu/mJy/+Xb9gA== +-----END PRIVATE KEY----- diff --git a/Tokenization/backend/wrapper/src/test/testCerts/clientSender/client-a.pub.pem b/Tokenization/backend/wrapper/src/test/testCerts/clientSender/client-a.pub.pem new file mode 100644 index 000000000..f565de4a2 --- /dev/null +++ b/Tokenization/backend/wrapper/src/test/testCerts/clientSender/client-a.pub.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnJySLgwWLpLbt8EqFDfh +KciKJGlxRUfcn/2u2EyxnkpKwwiLDSzKguZZ50xKviDtJao2FuZ251uyJDTfuPYt +549OklkKIQYFSmP4MxNDAz501TVSJ45a9WQugScGc9lkinvmIADGBEa0rj2keRkT +4MYvnWT2IGQJ91N99g9tDoQVPem5naHU1PxIwyxVVRIv6mVCaro6OULqx6iFvDff +vL0ef/5lbt8+vqX4QWwPH8rF5CvaV1KYYMkvQEXSn6158FE2YUepEN6wGEEDAIr9 +8D5vNlgabkpLzxDULFT+tdk7v6Shb7bKql1W0QlBkEh/mFDCEkqOesGXLuzPG8pV +AwIDAQAB +-----END PUBLIC KEY----- diff --git a/Tokenization/backend/wrapper/src/test/testCerts/clientSender/client-a.svc.local.crt b/Tokenization/backend/wrapper/src/test/testCerts/clientSender/client-a.svc.local.crt deleted file mode 100644 index a6d5025b6..000000000 --- a/Tokenization/backend/wrapper/src/test/testCerts/clientSender/client-a.svc.local.crt +++ /dev/null @@ -1,13 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIB8jCCAZigAwIBAgIUa4ibeAgx6Ga3UklyqWn8uEtEcQcwCgYIKoZIzj0EAwIw -RTEdMBsGA1UEAwwURXhhbXBsZSBUZXN0IFJvb3QgQ0ExFzAVBgNVBAoMDkV4YW1w -bGVDbyBUZXN0MQswCQYDVQQGEwJQTDAeFw0yNTA4MjYxNTIzNDVaFw0yNjA4MjYx -NTI4NDVaMEMxGzAZBgNVBAMMEmNsaWVudC1hLnN2Yy5sb2NhbDEXMBUGA1UECgwO -RXhhbXBsZUNvIFRlc3QxCzAJBgNVBAYTAlBMMFkwEwYHKoZIzj0CAQYIKoZIzj0D -AQcDQgAEU9voQyzju4VEGHJd/5KWROHG1ZqjP8ClUT8qxQ4fo7vQTQAitXYxr4sh -jfI4LvX63Yd/FzbTiFD3vlIJHdMrnqNoMGYwDAYDVR0TAQH/BAIwADAnBgNVHREE -IDAeghJjbGllbnQtYS5zdmMubG9jYWyCCGNsaWVudC1hMA4GA1UdDwEB/wQEAwID -qDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwCgYIKoZIzj0EAwIDSAAw -RQIgCqma8A91M48a9t4f/b38nCEgG2LQ0TRLiWqMw7G+3vwCIQDcMDbM9Beuh1ap -JudMwo61iUWxeF+TLgapxxO/R4jWqA== ------END CERTIFICATE----- diff --git a/Tokenization/backend/wrapper/src/test/testCerts/clientSender/client-a.svc.local.key b/Tokenization/backend/wrapper/src/test/testCerts/clientSender/client-a.svc.local.key deleted file mode 100644 index 9531bc087..000000000 --- a/Tokenization/backend/wrapper/src/test/testCerts/clientSender/client-a.svc.local.key +++ /dev/null @@ -1,5 +0,0 @@ ------BEGIN EC PRIVATE KEY----- -MHcCAQEEIL9ACEBWYWkPUs6t9ZbrWETL1kpsPe/pxKQIqnqHlWyroAoGCCqGSM49 -AwEHoUQDQgAEU9voQyzju4VEGHJd/5KWROHG1ZqjP8ClUT8qxQ4fo7vQTQAitXYx -r4shjfI4LvX63Yd/FzbTiFD3vlIJHdMrng== ------END EC PRIVATE KEY----- diff --git a/Tokenization/backend/wrapper/src/test/testCerts/testCerts.ts b/Tokenization/backend/wrapper/src/test/testCerts/testCerts.ts index c37742655..dcb90aa19 100644 --- a/Tokenization/backend/wrapper/src/test/testCerts/testCerts.ts +++ b/Tokenization/backend/wrapper/src/test/testCerts/testCerts.ts @@ -7,11 +7,11 @@ export const getTestCentralCertPaths = const CA_CERT_PATH = path.join(__dirname, "./ca.crt"); const SERVER_CERT_PATH = path.join( __dirname, - "./centralSystem/central.system.svc.local.crt" + "./centralSystem/central-system.crt" ); const SERVER_KEY_PATH = path.join( __dirname, - "./centralSystem/central.system.svc.local.key" + "./centralSystem/central-system.key" ); return { @@ -26,11 +26,11 @@ export const getTestClientListenerCertPaths = const CA_CERT_PATH = path.join(__dirname, "./ca.crt"); const CLIENT_CERT_PATH = path.join( __dirname, - "./clientListener/client-b.svc.local.crt" + "./clientListener/client-b-client.crt" ); const CLIENT_KEY_PATH = path.join( __dirname, - "./clientListener/client-b.svc.local.key" + "./clientListener/client-b.key" ); return { @@ -40,17 +40,33 @@ export const getTestClientListenerCertPaths = }; }; -export const getTestClientSenderCertPaths = +export const getTestClientListenerServerCertPaths = (): gRPCWrapperConfig["clientCerts"] => { const CA_CERT_PATH = path.join(__dirname, "./ca.crt"); const CLIENT_CERT_PATH = path.join( __dirname, - "./clientSender/client-a.svc.local.crt" + "./clientListenerServer/client-b-server.crt" ); const CLIENT_KEY_PATH = path.join( __dirname, - "./clientSender/client-a.svc.local.key" + "./clientListenerServer/client-b.key" + ); + + return { + caCertPath: CA_CERT_PATH, + certPath: CLIENT_CERT_PATH, + keyPath: CLIENT_KEY_PATH, + }; + }; + +export const getTestClientSenderCertPaths = + (): gRPCWrapperConfig["clientCerts"] => { + const CA_CERT_PATH = path.join(__dirname, "./ca.crt"); + const CLIENT_CERT_PATH = path.join( + __dirname, + "./clientSender/client-a-client.crt" ); + const CLIENT_KEY_PATH = path.join(__dirname, "./clientSender/client-a.key"); return { caCertPath: CA_CERT_PATH, @@ -63,11 +79,11 @@ export const getTestCerts = () => { const CA_CERT_PATH = path.join(__dirname, "./ca.crt"); const SERVER_CERT_PATH = path.join( __dirname, - "./centralSystem/central.system.svc.local.crt" + "./centralSystem/central-system.crt" ); const SERVER_KEY_PATH = path.join( __dirname, - "./centralSystem/central.system.svc.local.key" + "./centralSystem/central-system.key" ); const caCert = fs.readFileSync(CA_CERT_PATH); From 1ce05882d273d5cc73e1adac09cb3c95668fc15d Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki <42175519+OmegaCreations@users.noreply.github.com> Date: Fri, 26 Sep 2025 09:46:24 +0200 Subject: [PATCH 64/81] Potential fix for code scanning alert no. 252: Unused variable, import, function or class Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- Tokenization/backend/wrapper/src/client/Connection/Connection.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/Tokenization/backend/wrapper/src/client/Connection/Connection.ts b/Tokenization/backend/wrapper/src/client/Connection/Connection.ts index b4ca2e597..7e4a75e40 100644 --- a/Tokenization/backend/wrapper/src/client/Connection/Connection.ts +++ b/Tokenization/backend/wrapper/src/client/Connection/Connection.ts @@ -20,7 +20,6 @@ import { FetchResponse, } from "../../models/connection.model"; import * as grpc from "@grpc/grpc-js"; -import * as fs from "fs"; /** * @description This class represents a connection to a target client and manages sending messages to it. From 27f7015d4ad4079fa645a5b1cc8dc9911ce76d32 Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Sat, 27 Sep 2025 19:50:28 +0200 Subject: [PATCH 65/81] feat: add implementation of auth interceptor --- .../backend/wrapper/package-lock.json | 29 ++++ Tokenization/backend/wrapper/package.json | 1 + .../ConnectionManager/ConnectionManager.ts | 17 ++- .../Interceptors/grpc.auth.interceptor.ts | 140 ++++++++++++++++++ .../backend/wrapper/src/client/gRPCWrapper.ts | 27 +++- .../wrapper/src/models/config.model.ts | 8 +- .../wrapper/src/models/connection.model.ts | 2 + 7 files changed, 212 insertions(+), 12 deletions(-) create mode 100644 Tokenization/backend/wrapper/src/client/ConnectionManager/Interceptors/grpc.auth.interceptor.ts diff --git a/Tokenization/backend/wrapper/package-lock.json b/Tokenization/backend/wrapper/package-lock.json index cd3e9595a..4a2bef0f0 100644 --- a/Tokenization/backend/wrapper/package-lock.json +++ b/Tokenization/backend/wrapper/package-lock.json @@ -11,12 +11,14 @@ "@grpc/grpc-js": "^1.13.4", "@grpc/proto-loader": "^0.7.15", "express": "^5.1.0", + "jose": "^6.1.0", "ts-node": "^10.9.2", "typescript": "^5.8.3" }, "devDependencies": { "@types/express": "^5.0.3", "@types/jest": "^29.5.14", + "@types/jsonwebtoken": "^9.0.10", "jest": "^29.7.0", "ts-jest": "^29.4.0", "tsc-alias": "^1.8.16" @@ -1254,6 +1256,17 @@ "pretty-format": "^29.0.0" } }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -1261,6 +1274,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "24.0.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.0.tgz", @@ -3657,6 +3677,15 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/jose": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.0.tgz", + "integrity": "sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", diff --git a/Tokenization/backend/wrapper/package.json b/Tokenization/backend/wrapper/package.json index e099eeaab..9a23e0b19 100644 --- a/Tokenization/backend/wrapper/package.json +++ b/Tokenization/backend/wrapper/package.json @@ -19,6 +19,7 @@ "@grpc/grpc-js": "^1.13.4", "@grpc/proto-loader": "^0.7.15", "express": "^5.1.0", + "jose": "^6.1.0", "ts-node": "^10.9.2", "typescript": "^5.8.3" } diff --git a/Tokenization/backend/wrapper/src/client/ConnectionManager/ConnectionManager.ts b/Tokenization/backend/wrapper/src/client/ConnectionManager/ConnectionManager.ts index 4f8ffc756..27f4a8842 100644 --- a/Tokenization/backend/wrapper/src/client/ConnectionManager/ConnectionManager.ts +++ b/Tokenization/backend/wrapper/src/client/ConnectionManager/ConnectionManager.ts @@ -25,6 +25,7 @@ import { } from "../../models/message.model"; import { ConnectionStatus } from "../../models/connection.model"; import * as fs from "fs"; +import { gRPCAuthInterceptor } from "./Interceptors/grpc.auth.interceptor"; /** * @description Manages all the connection between clients and central system. @@ -245,8 +246,9 @@ export class ConnectionManager { /** Starts a listener server for p2p connections */ public async listenForPeers( port: number, - listenerKey: NonSharedBuffer, + listenerPublicKey: NonSharedBuffer, listenerCert: NonSharedBuffer, + listenerPrivateKey: NonSharedBuffer, baseAPIPath?: string ): Promise { if (baseAPIPath) this.baseAPIPath = baseAPIPath; @@ -262,6 +264,15 @@ export class ConnectionManager { call: grpc.ServerUnaryCall, callback: grpc.sendUnaryData ) => { + // run auth interceptor + gRPCAuthInterceptor( + call, + callback, + this.receivingConnections, + listenerPrivateKey, + listenerPublicKey + ); + try { const clientAddress = call.getPeer(); this.logger.infoMessage(`Incoming request from ${clientAddress}`); @@ -334,7 +345,7 @@ export class ConnectionManager { this.caCert, [ { - private_key: listenerKey, + private_key: listenerPublicKey, cert_chain: listenerCert, }, ], @@ -349,4 +360,6 @@ export class ConnectionManager { this.logger.infoMessage(`Peer server listening on localhost:${port}`); } + + private createPeerAuthInterceptor() {} } diff --git a/Tokenization/backend/wrapper/src/client/ConnectionManager/Interceptors/grpc.auth.interceptor.ts b/Tokenization/backend/wrapper/src/client/ConnectionManager/Interceptors/grpc.auth.interceptor.ts new file mode 100644 index 000000000..938097a62 --- /dev/null +++ b/Tokenization/backend/wrapper/src/client/ConnectionManager/Interceptors/grpc.auth.interceptor.ts @@ -0,0 +1,140 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +import * as grpc from "@grpc/grpc-js"; +import { Connection } from "client/Connection/Connection"; +import { importPKCS8, importJWK, compactDecrypt, compactVerify } from "jose"; + +interface TokenPayload { + serialNumber: string; + allowedRequests: ("POST" | "GET" | "PUT" | "DELETE" | "PATCH")[]; +} + +export const gRPCAuthInterceptor = async ( + call: grpc.ServerUnaryCall, + callback: grpc.sendUnaryData, + clientConnections: Map, + privateKeyBuffer: NonSharedBuffer, + publicKeyBuffer: NonSharedBuffer +): Promise => { + const metadata = call.metadata.getMap(); + const jweToken = metadata.token as string; + + // check if token exists + if (!jweToken) { + const error = { + name: "AuthenticationError", + message: "No token provided", + code: grpc.status.UNAUTHENTICATED, + }; + + callback(error, null); + return false; + } + + // validate JWE (encrypted JWS) - decode JWE -> JWS + let privateKey: any; + let jwsToken: any; + try { + privateKey = await importPKCS8( + privateKeyBuffer.toString("utf-8"), + "RSA-OAEP-256" + ); + const { plaintext } = await compactDecrypt(jweToken, privateKey); // decrypt JWE token + jwsToken = plaintext.toString(); + } catch (_e) { + const error = { + name: "AuthenticationError", + message: "Incorrect token provided", + code: grpc.status.UNAUTHENTICATED, + }; + + // TODO?: inform central system about incorrect token coming for peer + // or create counter with incorrect tries and then inform central system + // it potentially might be an attack here. + + callback(error, null); + return false; + } + + // check if connection is blocked + const conn = clientConnections.get(); + + // validate JWS signature + let publicKey: any; + let payload: TokenPayload; + try { + publicKey = await importJWK(JSON.parse(publicKeyBuffer.toString())); + const { payload: jwtPayload } = await compactVerify(jwsToken, publicKey); + + const payloadString = new TextDecoder().decode(jwtPayload); + payload = JSON.parse(payloadString); + } catch (e: any) { + const error = { + name: "AuthenticationError", + message: `JWS ${ + e.message.includes("expired") ? "Expiration" : "Verification" + } error`, + code: e.message.includes("expired") + ? grpc.status.UNAUTHENTICATED + : grpc.status.PERMISSION_DENIED, + }; + + // TODO?: inform central system about incorrect token coming for peer + // or create counter with incorrect tries and then inform central system + // it potentially might be an attack here. + + callback(error, null); + return false; + } + + // Connection tunnel verification with SN + const peerCert = (call as any).getPeerCertificate(); // its not publicly exposed + const clientSerialNumber = peerCert ? peerCert.serialNumber : null; + const tokenSerialNumber = payload.serialNumber; // Serial number is inside payload + + if (!clientSerialNumber || tokenSerialNumber !== clientSerialNumber) { + const error = { + name: "AuthenticationError", + code: grpc.status.PERMISSION_DENIED, + message: "Serial number mismatch.", + } as any; + + // TODO?: inform central system about incorrect token coming for peer + // or create counter with incorrect tries and then inform central system + // it potentially might be an attack here. + + callback(error, null); + return false; + } + + // Validate permission for request method + const method = String(call.request?.method || "POST").toUpperCase(); + if (!payload.allowedRequests.includes(method as any)) { + const error = { + name: "AuthorizationError", + code: grpc.status.PERMISSION_DENIED, + message: `Request of type ${method} is not allowed.`, + } as any; + + // TODO?: inform central system about incorrect token coming for peer + // or create counter with incorrect tries and then inform central system + // it potentially might be an attack here. + + callback(error, null); + return false; + } + + return true; +}; diff --git a/Tokenization/backend/wrapper/src/client/gRPCWrapper.ts b/Tokenization/backend/wrapper/src/client/gRPCWrapper.ts index 5be3e7ee3..b8d617abc 100644 --- a/Tokenization/backend/wrapper/src/client/gRPCWrapper.ts +++ b/Tokenization/backend/wrapper/src/client/gRPCWrapper.ts @@ -40,7 +40,8 @@ import { LogManager } from "@aliceo2/web-ui"; */ export class gRPCWrapper { private ConnectionManager: ConnectionManager; - private listenerKey?: NonSharedBuffer; + private listenerPublicKey?: NonSharedBuffer; + private listenerPrivateKey?: NonSharedBuffer; private listenerCert?: NonSharedBuffer; private logger = LogManager.getLogger("gRPCWrapper"); @@ -57,16 +58,21 @@ export class gRPCWrapper { !config.clientCerts || !config.clientCerts.caCertPath || !config.clientCerts.certPath || - !config.clientCerts.keyPath + !config.clientCerts.publicKeyPath ) { throw new Error("Invalid gRPCWrapper configuration provided."); } if ( - config.listenerCertPaths?.keyPath && + config.listenerCertPaths?.publicKeyPath && config.listenerCertPaths?.certPath ) { - this.listenerKey = fs.readFileSync(config.listenerCertPaths.keyPath); + this.listenerPublicKey = fs.readFileSync( + config.listenerCertPaths.publicKeyPath + ); + this.listenerPrivateKey = fs.readFileSync( + config.listenerCertPaths.privateKeyPath + ); this.listenerCert = fs.readFileSync(config.listenerCertPaths.certPath); } @@ -75,7 +81,7 @@ export class gRPCWrapper { config.centralAddress, config.clientCerts.caCertPath, config.clientCerts.certPath, - config.clientCerts.keyPath + config.clientCerts.publicKeyPath ); this.ConnectionManager.registerCommandHandlers([ { @@ -119,11 +125,15 @@ export class gRPCWrapper { listenerCertPaths?: { keyPath: string; certPath: string } ): Promise { if (listenerCertPaths?.keyPath && listenerCertPaths?.certPath) { - this.listenerKey = fs.readFileSync(listenerCertPaths.keyPath); + this.listenerPublicKey = fs.readFileSync(listenerCertPaths.keyPath); this.listenerCert = fs.readFileSync(listenerCertPaths.certPath); } - if (!this.listenerKey || !this.listenerCert) { + if ( + !this.listenerPublicKey || + !this.listenerPrivateKey || + !this.listenerCert + ) { this.logger.errorMessage( "Listener certificates are required to start P2P listener. Please provide valid paths." ); @@ -132,8 +142,9 @@ export class gRPCWrapper { return this.ConnectionManager.listenForPeers( port, - this.listenerKey, + this.listenerPublicKey, this.listenerCert, + this.listenerPrivateKey, baseAPIPath ); } diff --git a/Tokenization/backend/wrapper/src/models/config.model.ts b/Tokenization/backend/wrapper/src/models/config.model.ts index 6d6b18555..2e4e9e9ac 100644 --- a/Tokenization/backend/wrapper/src/models/config.model.ts +++ b/Tokenization/backend/wrapper/src/models/config.model.ts @@ -37,10 +37,14 @@ export interface gRPCWrapperConfig { /** Client TLS certificates paths. */ clientCerts: { caCertPath: string; - keyPath: string; + publicKeyPath: string; certPath: string; }; /** Optional listener TLS certificates paths. If provided, the gRPCWrapper will be able to accept incoming connections. */ - listenerCertPaths?: { keyPath: string; certPath: string }; + listenerCertPaths?: { + publicKeyPath: string; + privateKeyPath: string; + certPath: string; + }; } diff --git a/Tokenization/backend/wrapper/src/models/connection.model.ts b/Tokenization/backend/wrapper/src/models/connection.model.ts index f18c24fa4..f624cb312 100644 --- a/Tokenization/backend/wrapper/src/models/connection.model.ts +++ b/Tokenization/backend/wrapper/src/models/connection.model.ts @@ -27,6 +27,8 @@ export enum ConnectionStatus { RECONNECTING = "RECONNECTING", // The connection is refreshing its authentication token TOKEN_REFRESH = "TOKEN_REFRESH", + // The connection has been blocked + BLOCKED = "BLOCKED", } export type ConnectionHeaders = Record; From e0da42496d71bad9912185b7dbcb7399fc8481b0 Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Sun, 28 Sep 2025 13:23:39 +0200 Subject: [PATCH 66/81] feat: update connection for tokens, fix decryption in auth interceptor, fix tests --- .../src/client/Connection/Connection.ts | 153 ++++++++++++++---- .../ConnectionManager/ConnectionManager.ts | 86 +++++----- .../Interceptors/grpc.auth.interceptor.ts | 140 +++++++++------- .../wrapper/src/models/connection.model.ts | 5 + .../src/test/client/Commands/newToken.test.ts | 35 ++-- .../test/client/Commands/revokeToken.test.ts | 28 +++- .../ConnectionManager.test.ts | 21 ++- .../wrapper/src/test/testCerts/testCerts.ts | 6 +- 8 files changed, 314 insertions(+), 160 deletions(-) diff --git a/Tokenization/backend/wrapper/src/client/Connection/Connection.ts b/Tokenization/backend/wrapper/src/client/Connection/Connection.ts index 7e4a75e40..4ba96f9b2 100644 --- a/Tokenization/backend/wrapper/src/client/Connection/Connection.ts +++ b/Tokenization/backend/wrapper/src/client/Connection/Connection.ts @@ -18,90 +18,151 @@ import { ConnectionStatus, FetchOptions, FetchResponse, + TokenPayload, } from "../../models/connection.model"; import * as grpc from "@grpc/grpc-js"; +import { LogManager } from "@aliceo2/web-ui"; + +type ConnectionCerts = { + caCert: NonSharedBuffer; + clientCert: NonSharedBuffer; + clientKey: NonSharedBuffer; +}; /** * @description This class represents a connection to a target client and manages sending messages to it. */ export class Connection { - private token: string; + private jweToken: string; private status: ConnectionStatus; private peerClient?: any; // a client grpc connection instance + // security management variables + private clientSerialNumber?: string; // The certificate SN used to uniquely identify the peer. + private lastActiveTimestamp: number; // Timestamp of the last successful request (for garbage collection). + private authFailures: number; // Counter for consecutive authentication failures (for anti-DDoS/throttling). + private cachedTokenPayload?: TokenPayload; // Cache of the successfully verified token payload. + public targetAddress: string; public direction: ConnectionDirection; + // utils + private logger; + /** * @description Creates a new Connection instance with the given token, target address, and connection direction. * - * @param token - The authentication token for the connection. + * @param jweToken - The encrypted JWE token for the connection. * @param targetAddress - The unique address of the target client. * @param direction - The direction of the connection (e.g., sending or receiving). - * @param peerCtor - The constructor for the gRPC client to be used for communication. - * @param caCertPath - Path to the CA certificate file. - * @param clientCertPath - Path to the client certificate file. - * @param clientKeyPath - Path to the client key file. + * @param clientSN - Optional serial number of the peer's certificate (used for lookups). */ constructor( - token: string, + jweToken: string, targetAddress: string, direction: ConnectionDirection, - peerCtor: any, - private readonly connectionCerts: { - caCert: NonSharedBuffer; - clientCert: NonSharedBuffer; - clientKey: NonSharedBuffer; - } + clientSN?: string ) { - this.token = token; + this.jweToken = jweToken; this.targetAddress = targetAddress; this.direction = direction; + // Initialize state fields + this.clientSerialNumber = clientSN; + this.lastActiveTimestamp = Date.now(); + this.authFailures = 0; + this.status = ConnectionStatus.CONNECTED; + + this.logger = LogManager.getLogger(`Connection ${targetAddress}`); + } + + /** + * @description Creates the mTLS gRPC client and attaches it to the connection. + * This method is REQUIRED ONLY for outbound (SENDING) connections. + * * @param peerCtor - The constructor for the gRPC client to be used for communication. + * @param connectionCerts - Required sending client certificates for mTLS. + */ + public createSslTunnel( + peerCtor: any, + connectionCerts: ConnectionCerts + ): void { + if (this.direction !== ConnectionDirection.SENDING) { + this.logger.warnMessage( + "Attempted to create SSL tunnel on a RECEIVING connection. This is usually unnecessary." + ); + } + if ( !connectionCerts.caCert || !connectionCerts.clientCert || !connectionCerts.clientKey ) { throw new Error( - "Connection certificates are required to create a Connection." + "Connection certificates are required to create an mTLS tunnel." ); } // create grpc credentials const sslCreds = grpc.credentials.createSsl( - this.connectionCerts.caCert, - this.connectionCerts.clientKey, - this.connectionCerts.clientCert + connectionCerts.caCert, + connectionCerts.clientKey, + connectionCerts.clientCert ); - this.peerClient = new peerCtor(targetAddress, sslCreds); - - this.status = ConnectionStatus.CONNECTED; + this.peerClient = new peerCtor(this.targetAddress, sslCreds); + this.updateStatus(ConnectionStatus.CONNECTED); } /** * @description Replace newly generated token - * @param token New token to be replaced + * @param jweToken New token to be replaced */ - public handleNewToken(token: string): void { - this.token = token; + public handleNewToken(jweToken: string): void { + this.jweToken = jweToken; } /** * @description Revoke current token and set status of unauthorized connection */ public handleRevokeToken(): void { - this.token = ""; + this.jweToken = ""; this.status = ConnectionStatus.UNAUTHORIZED; } + /** + * @description Handles a successful authentication event. Updates the active timestamp, + * resets the failure counter, and caches the new token payload. + * This is crucial for high-performance applications to avoid re-validating the same token. + * @param payload The decoded and verified token payload. + */ + public handleSuccessfulAuth(payload: TokenPayload): void { + this.lastActiveTimestamp = Date.now(); + this.authFailures = 0; + this.cachedTokenPayload = payload; + this.updateStatus(ConnectionStatus.CONNECTED); + } + + /** + * @description Handles an authentication failure. Increments the failure counter. + * If the failure count exceeds a local threshold, the connection is locally marked as BLOCKED. + * @returns The new count of consecutive failures. + */ + public handleFailedAuth(): number { + this.authFailures += 1; + + // Local throttling mechanism + if (this.authFailures >= 5) { + this.updateStatus(ConnectionStatus.BLOCKED); + } + return this.authFailures; + } + /** * @description Returns token for this Connection object * @returns Connection token */ public getToken(): string { - return this.token; + return this.jweToken; } /** @@ -128,6 +189,39 @@ export class Connection { return this.targetAddress; } + /** + * @description Returns the client's Serial Number (SN). + * @returns The client's serial number or undefined. + */ + public getSerialNumber(): string | undefined { + return this.clientSerialNumber; + } + + /** + * @description Sets the client's Serial Number. Primarily used for RECEIVING connections + * where the SN is extracted during the first mTLS handshake in the interceptor. + * @param serialNumber The serial number string. + */ + public setSerialNumber(serialNumber: string): void { + this.clientSerialNumber = serialNumber; + } + + /** + * @description Returns the timestamp of the last successful interaction. + * @returns UNIX timestamp in milliseconds. + */ + public getLastActiveTimestamp(): number { + return this.lastActiveTimestamp; + } + + /** + * @description Returns the cached token payload. + * @returns The cached payload or undefined. + */ + public getCachedTokenPayload(): TokenPayload | undefined { + return this.cachedTokenPayload; + } + /** * @description Attaches gRPC client to that connection */ @@ -151,6 +245,11 @@ export class Connection { const path = options.path || "/"; const headers: ConnectionHeaders = { ...(options.headers || {}) }; + // set mandatory grpc metadata + const metadata = new grpc.Metadata(); + metadata.set("jweToken", this.jweToken); + + // build body buffer let bodyBuf: Buffer = Buffer.alloc(0); const b = options.body; if (b != null) { @@ -167,7 +266,7 @@ export class Connection { // return promise with response return new Promise((resolve, reject) => { - this.peerClient.Fetch(req, (err: any, resp: any) => { + this.peerClient.Fetch(req, metadata, (err: any, resp: any) => { if (err) return reject(err); const resBody = resp?.body ? Buffer.from(resp.body) : Buffer.alloc(0); diff --git a/Tokenization/backend/wrapper/src/client/ConnectionManager/ConnectionManager.ts b/Tokenization/backend/wrapper/src/client/ConnectionManager/ConnectionManager.ts index 27f4a8842..efe4c0953 100644 --- a/Tokenization/backend/wrapper/src/client/ConnectionManager/ConnectionManager.ts +++ b/Tokenization/backend/wrapper/src/client/ConnectionManager/ConnectionManager.ts @@ -114,23 +114,6 @@ export class ConnectionManager { centralClient, this.centralDispatcher ); - - this.sendingConnections.set( - "a", - new Connection("1", "a", ConnectionDirection.SENDING, this.peerCtor, { - caCert: this.caCert, - clientCert: this.clientCert, - clientKey: this.clientKey, - }) - ); - this.sendingConnections.set( - "b", - new Connection("2", "b", ConnectionDirection.SENDING, this.peerCtor, { - caCert: this.caCert, - clientCert: this.clientCert, - clientKey: this.clientKey, - }) - ); } /** @@ -166,12 +149,12 @@ export class ConnectionManager { * Creates new connection * @param address Target (external) address of the connection * @param direction Direction of connection - * @param token Optional token for connection + * @param jweToken Optional encrypted JWE token for connection */ public async createNewConnection( address: string, direction: ConnectionDirection, - token?: string + jweToken?: string ) { let conn: Connection | undefined; @@ -183,14 +166,15 @@ export class ConnectionManager { // Return existing connection if found if (conn) { - if (token) { - conn.handleNewToken(token); + if (jweToken) { + conn.handleNewToken(jweToken); } return conn; } // Create new connection - conn = new Connection(token || "", address, direction, this.peerCtor, { + conn = new Connection(jweToken || "", address, direction); + conn.createSslTunnel(this.peerCtor, { caCert: this.caCert, clientCert: this.clientCert, clientKey: this.clientKey, @@ -229,6 +213,27 @@ export class ConnectionManager { } } + /** + * @description Searches through all receiving and sending connections to find a connection by its client Serial Number (SN). + * @param serialNumber The unique serial number of the peer's certificate. + * @returns The matching Connection object or undefined. + */ + getConnectionBySerialNumber(serialNumber: string): Connection | undefined { + // Check receiving connections first + for (const conn of this.receivingConnections.values()) { + if (conn.getSerialNumber() === serialNumber) { + return conn; + } + } + // Check sending connections + for (const conn of this.sendingConnections.values()) { + if (conn.getSerialNumber() === serialNumber) { + return conn; + } + } + return undefined; + } + /** * Returns object with all connections * @returns Object of all connections @@ -246,7 +251,6 @@ export class ConnectionManager { /** Starts a listener server for p2p connections */ public async listenForPeers( port: number, - listenerPublicKey: NonSharedBuffer, listenerCert: NonSharedBuffer, listenerPrivateKey: NonSharedBuffer, baseAPIPath?: string @@ -265,39 +269,25 @@ export class ConnectionManager { callback: grpc.sendUnaryData ) => { // run auth interceptor - gRPCAuthInterceptor( + const { isAuthenticated, conn } = await gRPCAuthInterceptor( call, callback, this.receivingConnections, listenerPrivateKey, - listenerPublicKey + this.peerCtor ); + if (!isAuthenticated || !conn) { + // Authentication failed - response already sent in interceptor + return; + } + try { const clientAddress = call.getPeer(); this.logger.infoMessage(`Incoming request from ${clientAddress}`); - let conn: Connection | undefined = - this.receivingConnections.get(clientAddress); - - if (!conn) { - conn = new Connection( - "", - clientAddress, - ConnectionDirection.RECEIVING, - this.peerCtor, - { - caCert: this.caCert, - clientCert: this.clientCert, - clientKey: this.clientKey, - } - ); - conn.updateStatus(ConnectionStatus.CONNECTED); - this.receivingConnections.set(clientAddress, conn); - this.logger.infoMessage( - `New incoming connection registered for: ${clientAddress}` - ); - } + conn.updateStatus(ConnectionStatus.CONNECTED); + this.receivingConnections.set(clientAddress, conn); // create request to forward to local API endpoint const method = String(call.request?.method || "POST").toUpperCase(); @@ -345,7 +335,7 @@ export class ConnectionManager { this.caCert, [ { - private_key: listenerPublicKey, + private_key: listenerPrivateKey, cert_chain: listenerCert, }, ], @@ -360,6 +350,4 @@ export class ConnectionManager { this.logger.infoMessage(`Peer server listening on localhost:${port}`); } - - private createPeerAuthInterceptor() {} } diff --git a/Tokenization/backend/wrapper/src/client/ConnectionManager/Interceptors/grpc.auth.interceptor.ts b/Tokenization/backend/wrapper/src/client/ConnectionManager/Interceptors/grpc.auth.interceptor.ts index 938097a62..c1c913045 100644 --- a/Tokenization/backend/wrapper/src/client/ConnectionManager/Interceptors/grpc.auth.interceptor.ts +++ b/Tokenization/backend/wrapper/src/client/ConnectionManager/Interceptors/grpc.auth.interceptor.ts @@ -13,128 +13,152 @@ */ import * as grpc from "@grpc/grpc-js"; -import { Connection } from "client/Connection/Connection"; +import { Connection } from "../../../client/Connection/Connection"; import { importPKCS8, importJWK, compactDecrypt, compactVerify } from "jose"; +import { TokenPayload } from "../../../models/connection.model"; +import { ConnectionDirection } from "../../../models/message.model"; -interface TokenPayload { - serialNumber: string; - allowedRequests: ("POST" | "GET" | "PUT" | "DELETE" | "PATCH")[]; -} +// IMPORTANT: This key must be securely provided to the interceptor. +const RAW_ED25519_B64_KEY = "VqkcxlpJYVZI/SxgWH/VqVNeKhMGIbUfHn0okzdGs2E="; +/** + * @description gRPC interceptor function responsible for JWE decryption, JWS verification, + * certificate serial number matching (mTLS binding), and basic authorization. + */ export const gRPCAuthInterceptor = async ( call: grpc.ServerUnaryCall, callback: grpc.sendUnaryData, clientConnections: Map, - privateKeyBuffer: NonSharedBuffer, - publicKeyBuffer: NonSharedBuffer -): Promise => { + privateKeyBuffer: NonSharedBuffer, // RSA Private Key (PKCS8) for JWE decryption + peerCtor: any +): Promise<{ isAuthenticated: Boolean; conn: Connection | null }> => { const metadata = call.metadata.getMap(); - const jweToken = metadata.token as string; + const jweToken = metadata.jweToken as string; + const clientAddress = call.getPeer(); + let conn = clientConnections.get(clientAddress); - // check if token exists + // Check if token exists if (!jweToken) { const error = { name: "AuthenticationError", message: "No token provided", code: grpc.status.UNAUTHENTICATED, }; - callback(error, null); - return false; + return { isAuthenticated: false, conn: null }; + } + + // Connection must exist + if (!conn) { + conn = new Connection( + jweToken, + clientAddress, + ConnectionDirection.RECEIVING, + peerCtor + ); } - // validate JWE (encrypted JWS) - decode JWE -> JWS + // JWE decryption (RSA-OAEP-256) -> JWS (Plaintext) let privateKey: any; - let jwsToken: any; + let jwsToken: string; try { + // Importing RSA private key for decryption privateKey = await importPKCS8( privateKeyBuffer.toString("utf-8"), "RSA-OAEP-256" ); - const { plaintext } = await compactDecrypt(jweToken, privateKey); // decrypt JWE token - jwsToken = plaintext.toString(); + + const { plaintext } = await compactDecrypt(jweToken, privateKey); + jwsToken = new TextDecoder().decode(plaintext).trim(); } catch (_e) { const error = { name: "AuthenticationError", - message: "Incorrect token provided", + message: "Incorrect token provided (JWE Decryption failed)", code: grpc.status.UNAUTHENTICATED, }; - - // TODO?: inform central system about incorrect token coming for peer - // or create counter with incorrect tries and then inform central system - // it potentially might be an attack here. - + // TODO: Consider logging or informing a central security system about potential attack/misconfiguration. callback(error, null); - return false; + return { isAuthenticated: false, conn }; } - // check if connection is blocked - const conn = clientConnections.get(); - - // validate JWS signature - let publicKey: any; + // Verify JWS (With signature) and payload extraction + let pub: any; let payload: TokenPayload; + try { - publicKey = await importJWK(JSON.parse(publicKeyBuffer.toString())); - const { payload: jwtPayload } = await compactVerify(jwsToken, publicKey); + // Convert a raw Base64 Ed25519 public key to JWK format + const jwk = { + kty: "OKP", + crv: "Ed25519", + x: Buffer.from(RAW_ED25519_B64_KEY, "base64").toString("base64url"), + }; + + // Importing the Ed25519 public key for verification - using "EdDSA" algorithm + pub = await importJWK(jwk, "EdDSA"); + + // Compact verify - verify with key and decode the JWS token in one step + const { payload: jwtPayload, protectedHeader } = await compactVerify( + jwsToken, + pub + ); + + // Optional: Additional check to ensure correct signing algorithm was used + // if (protectedHeader.alg !== "EdDSA" && protectedHeader.alg !== "Ed25519") { + // throw new Error("JWS signed with an unexpected algorithm."); + // } + // Decode and parse the JWT payload const payloadString = new TextDecoder().decode(jwtPayload); payload = JSON.parse(payloadString); } catch (e: any) { + const isExpired = e.message?.includes("expired"); const error = { name: "AuthenticationError", - message: `JWS ${ - e.message.includes("expired") ? "Expiration" : "Verification" - } error`, - code: e.message.includes("expired") + message: `JWS Verification error: ${ + isExpired ? "Token expired" : "Invalid signature" + }`, + code: isExpired ? grpc.status.UNAUTHENTICATED : grpc.status.PERMISSION_DENIED, }; - - // TODO?: inform central system about incorrect token coming for peer - // or create counter with incorrect tries and then inform central system - // it potentially might be an attack here. - + // TODO: Consider logging or informing a central security system about failed verification. callback(error, null); - return false; + return { isAuthenticated: false, conn }; } - // Connection tunnel verification with SN - const peerCert = (call as any).getPeerCertificate(); // its not publicly exposed + // mTLS binding check and authorization + // Connection tunnel verification with serialNumber (mTLS SN vs Token SN) + const peerCert = (call as any).getPeerCertificate(); // Retrieves the mTLS client certificate details const clientSerialNumber = peerCert ? peerCert.serialNumber : null; - const tokenSerialNumber = payload.serialNumber; // Serial number is inside payload + const tokenSerialNumber = payload.serialNumber; // Serial number is inside the signed payload if (!clientSerialNumber || tokenSerialNumber !== clientSerialNumber) { + // Critical security failure!!!: The token holder does not match the mTLS certificate holder. const error = { name: "AuthenticationError", code: grpc.status.PERMISSION_DENIED, - message: "Serial number mismatch.", + message: "Serial number mismatch (mTLS binding failure).", } as any; - - // TODO?: inform central system about incorrect token coming for peer - // or create counter with incorrect tries and then inform central system - // it potentially might be an attack here. - + // TODO: This should trigger a high-priority security alert. callback(error, null); - return false; + return { isAuthenticated: false, conn }; } - // Validate permission for request method + // Validate permission for request method (Authorization check) const method = String(call.request?.method || "POST").toUpperCase(); if (!payload.allowedRequests.includes(method as any)) { const error = { name: "AuthorizationError", code: grpc.status.PERMISSION_DENIED, - message: `Request of type ${method} is not allowed.`, + message: `Request of type ${method} is not allowed by the token policy.`, } as any; - // TODO?: inform central system about incorrect token coming for peer - // or create counter with incorrect tries and then inform central system - // it potentially might be an attack here. - callback(error, null); - return false; + return { isAuthenticated: false, conn }; } - return true; + // Authentication and Authorization successful + // Update Connection state with SN and status + conn.handleSuccessfulAuth(payload as any); + return { isAuthenticated: true, conn }; }; diff --git a/Tokenization/backend/wrapper/src/models/connection.model.ts b/Tokenization/backend/wrapper/src/models/connection.model.ts index f624cb312..6f7b34183 100644 --- a/Tokenization/backend/wrapper/src/models/connection.model.ts +++ b/Tokenization/backend/wrapper/src/models/connection.model.ts @@ -62,3 +62,8 @@ export type HttpLikeResponse = { headers: Headers; body: Buffer; }; + +export type TokenPayload = { + serialNumber: string; + allowedRequests: ("POST" | "GET" | "PUT" | "DELETE" | "PATCH")[]; +}; diff --git a/Tokenization/backend/wrapper/src/test/client/Commands/newToken.test.ts b/Tokenization/backend/wrapper/src/test/client/Commands/newToken.test.ts index 37e948cf5..7e9b70b7e 100644 --- a/Tokenization/backend/wrapper/src/test/client/Commands/newToken.test.ts +++ b/Tokenization/backend/wrapper/src/test/client/Commands/newToken.test.ts @@ -26,13 +26,28 @@ import * as protoLoader from "@grpc/proto-loader"; import path from "path"; import { getTestCerts } from "../../testCerts/testCerts"; +// Mock logger +jest.mock( + "@aliceo2/web-ui", + () => ({ + LogManager: { + getLogger: () => ({ + infoMessage: jest.fn(), + debugMessage: jest.fn(), + errorMessage: jest.fn(), + }), + }, + }), + { virtual: true } +); + /** * Helper to create a new token command with given address, direction, and token. */ -function createEventMessage( +const createEventMessage = ( targetAddress: string, connectionDirection: ConnectionDirection -): Command { +): Command => { return { event: DuplexMessageEvent.MESSAGE_EVENT_NEW_TOKEN, payload: { @@ -41,7 +56,7 @@ function createEventMessage( token: "test-token", }, } as Command; -} +}; describe("NewTokenHandler", () => { let manager: ConnectionManager; @@ -88,15 +103,10 @@ describe("NewTokenHandler", () => { dir: ConnectionDirection, token: string ) { - const conn = new Connection( - token, - address, - dir, - peerCtor, - getTestCerts() - ); + const conn = new Connection(token, address, dir); if (dir === ConnectionDirection.SENDING) { this.sendingConnections.set(address, conn); + conn.createSslTunnel(peerCtor, getTestCerts()); } else { this.receivingConnections.set(address, conn); } @@ -110,10 +120,9 @@ describe("NewTokenHandler", () => { const conn = new Connection( "old-token", targetAddress, - ConnectionDirection.SENDING, - peerCtor, - getTestCerts() + ConnectionDirection.SENDING ); + conn.createSslTunnel(peerCtor, getTestCerts()); (manager as any).sendingConnections.set(targetAddress, conn); diff --git a/Tokenization/backend/wrapper/src/test/client/Commands/revokeToken.test.ts b/Tokenization/backend/wrapper/src/test/client/Commands/revokeToken.test.ts index 85eddcc8a..33815675a 100644 --- a/Tokenization/backend/wrapper/src/test/client/Commands/revokeToken.test.ts +++ b/Tokenization/backend/wrapper/src/test/client/Commands/revokeToken.test.ts @@ -27,6 +27,21 @@ import * as protoLoader from "@grpc/proto-loader"; import path from "path"; import { getTestCerts } from "../../testCerts/testCerts"; +// Mock logger +jest.mock( + "@aliceo2/web-ui", + () => ({ + LogManager: { + getLogger: () => ({ + infoMessage: jest.fn(), + debugMessage: jest.fn(), + errorMessage: jest.fn(), + }), + }, + }), + { virtual: true } +); + describe("RevokeToken", () => { const protoPath = path.join( __dirname, @@ -48,7 +63,7 @@ describe("RevokeToken", () => { const wrapper = proto.webui.tokenization; const peerCtor = wrapper.Peer2Peer; - function createEventMessage(targetAddress: string) { + const createEventMessage = (targetAddress: string) => { return { event: DuplexMessageEvent.MESSAGE_EVENT_REVOKE_TOKEN, payload: { @@ -56,7 +71,7 @@ describe("RevokeToken", () => { token: "test-token", }, } as Command; - } + }; let manager: ConnectionManager; @@ -78,10 +93,9 @@ describe("RevokeToken", () => { const conn = new Connection( "valid-token", targetAddress, - ConnectionDirection.SENDING, - peerCtor, - getTestCerts() + ConnectionDirection.SENDING ); + conn.createSslTunnel(peerCtor, getTestCerts()); (manager as any).sendingConnections!.set(targetAddress, conn); const handler = new RevokeTokenHandler(manager); @@ -100,9 +114,7 @@ describe("RevokeToken", () => { const conn = new Connection( "valid-token", targetAddress, - ConnectionDirection.RECEIVING, - peerCtor, - getTestCerts() + ConnectionDirection.RECEIVING ); (manager as any).receivingConnections.set(targetAddress, conn); diff --git a/Tokenization/backend/wrapper/src/test/client/ConnectionManager/ConnectionManager.test.ts b/Tokenization/backend/wrapper/src/test/client/ConnectionManager/ConnectionManager.test.ts index 503b22a50..00d743b90 100644 --- a/Tokenization/backend/wrapper/src/test/client/ConnectionManager/ConnectionManager.test.ts +++ b/Tokenization/backend/wrapper/src/test/client/ConnectionManager/ConnectionManager.test.ts @@ -37,6 +37,24 @@ const mockClient = { // Mock CentralSystem constructor const CentralSystemMock = jest.fn(() => mockClient); +// Mock gRPC auth interceptor +jest.mock( + "../../../client/ConnectionManager/Interceptors/grpc.auth.interceptor", + () => ({ + gRPCAuthInterceptor: jest.fn((call, callback) => { + return Promise.resolve({ + isAuthenticated: true, + conn: { + updateStatus: jest.fn(), + handleSuccessfulAuth: jest.fn(), + getSerialNumber: jest.fn(), + setSerialNumber: jest.fn(), + }, + }); + }), + }) +); + // Mock dispatcher const mockDispatch = jest.fn(); jest.mock( @@ -242,7 +260,6 @@ describe("ConnectionManager", () => { expect.any(Function) ); - // implementacja metody Fetch została przechwycona expect(capturedServerImpl).toBeTruthy(); expect(typeof capturedServerImpl.Fetch).toBe("function"); }); @@ -255,7 +272,7 @@ describe("ConnectionManager", () => { "http://localhost:40041/api/" ); - // przygotuj dane wywołania + // prepare data to call const call = { getPeer: () => "client-42", request: { diff --git a/Tokenization/backend/wrapper/src/test/testCerts/testCerts.ts b/Tokenization/backend/wrapper/src/test/testCerts/testCerts.ts index dcb90aa19..d47f86cd7 100644 --- a/Tokenization/backend/wrapper/src/test/testCerts/testCerts.ts +++ b/Tokenization/backend/wrapper/src/test/testCerts/testCerts.ts @@ -36,7 +36,7 @@ export const getTestClientListenerCertPaths = return { caCertPath: CA_CERT_PATH, certPath: CLIENT_CERT_PATH, - keyPath: CLIENT_KEY_PATH, + publicKeyPath: CLIENT_KEY_PATH, }; }; @@ -55,7 +55,7 @@ export const getTestClientListenerServerCertPaths = return { caCertPath: CA_CERT_PATH, certPath: CLIENT_CERT_PATH, - keyPath: CLIENT_KEY_PATH, + publicKeyPath: CLIENT_KEY_PATH, }; }; @@ -71,7 +71,7 @@ export const getTestClientSenderCertPaths = return { caCertPath: CA_CERT_PATH, certPath: CLIENT_CERT_PATH, - keyPath: CLIENT_KEY_PATH, + publicKeyPath: CLIENT_KEY_PATH, }; }; From 20d62445ffe75cefce34ffd2f990ce2678c964d9 Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Sun, 28 Sep 2025 14:14:02 +0200 Subject: [PATCH 67/81] feat: add handling failed auths in the auth interceptor --- .../ConnectionManager/ConnectionManager.ts | 3 +- .../Interceptors/grpc.auth.interceptor.ts | 129 ++++++++++++++---- 2 files changed, 103 insertions(+), 29 deletions(-) diff --git a/Tokenization/backend/wrapper/src/client/ConnectionManager/ConnectionManager.ts b/Tokenization/backend/wrapper/src/client/ConnectionManager/ConnectionManager.ts index efe4c0953..ef8d1e0cf 100644 --- a/Tokenization/backend/wrapper/src/client/ConnectionManager/ConnectionManager.ts +++ b/Tokenization/backend/wrapper/src/client/ConnectionManager/ConnectionManager.ts @@ -273,8 +273,7 @@ export class ConnectionManager { call, callback, this.receivingConnections, - listenerPrivateKey, - this.peerCtor + listenerPrivateKey ); if (!isAuthenticated || !conn) { diff --git a/Tokenization/backend/wrapper/src/client/ConnectionManager/Interceptors/grpc.auth.interceptor.ts b/Tokenization/backend/wrapper/src/client/ConnectionManager/Interceptors/grpc.auth.interceptor.ts index c1c913045..60f93a48d 100644 --- a/Tokenization/backend/wrapper/src/client/ConnectionManager/Interceptors/grpc.auth.interceptor.ts +++ b/Tokenization/backend/wrapper/src/client/ConnectionManager/Interceptors/grpc.auth.interceptor.ts @@ -15,7 +15,10 @@ import * as grpc from "@grpc/grpc-js"; import { Connection } from "../../../client/Connection/Connection"; import { importPKCS8, importJWK, compactDecrypt, compactVerify } from "jose"; -import { TokenPayload } from "../../../models/connection.model"; +import { + ConnectionStatus, + TokenPayload, +} from "../../../models/connection.model"; import { ConnectionDirection } from "../../../models/message.model"; // IMPORTANT: This key must be securely provided to the interceptor. @@ -29,8 +32,7 @@ export const gRPCAuthInterceptor = async ( call: grpc.ServerUnaryCall, callback: grpc.sendUnaryData, clientConnections: Map, - privateKeyBuffer: NonSharedBuffer, // RSA Private Key (PKCS8) for JWE decryption - peerCtor: any + privateKeyBuffer: NonSharedBuffer // RSA Private Key (PKCS8) for JWE decryption ): Promise<{ isAuthenticated: Boolean; conn: Connection | null }> => { const metadata = call.metadata.getMap(); const jweToken = metadata.jweToken as string; @@ -48,16 +50,50 @@ export const gRPCAuthInterceptor = async ( return { isAuthenticated: false, conn: null }; } - // Connection must exist - if (!conn) { + // Check if connection exists + if (conn) { + // Check if connection is blocked + if (conn.getStatus() === ConnectionStatus.BLOCKED) { + const error = { + name: "AuthenticationError", + message: "Connection is blocked. Contact administrator.", + code: grpc.status.UNAUTHENTICATED, + }; + callback(error, null); + return { isAuthenticated: false, conn }; + } + + if (conn.getToken() === jweToken) { + // check for allowed requests and serial number match if token is the same + if ( + !isRequestAllowed(conn.getCachedTokenPayload(), call.request, callback) + ) { + return { isAuthenticated: false, conn }; + } + + if ( + !isSerialNumberMatching( + conn.getCachedTokenPayload(), + (call as any).getPeerCertificate(), + callback + ) + ) { + conn.handleFailedAuth(); + return { isAuthenticated: false, conn }; + } + + return { isAuthenticated: true, conn }; + } + } else { conn = new Connection( jweToken, clientAddress, - ConnectionDirection.RECEIVING, - peerCtor + ConnectionDirection.RECEIVING ); + clientConnections.set(clientAddress, conn); } + // New connection - need to authenticate // JWE decryption (RSA-OAEP-256) -> JWS (Plaintext) let privateKey: any; let jwsToken: string; @@ -78,6 +114,7 @@ export const gRPCAuthInterceptor = async ( }; // TODO: Consider logging or informing a central security system about potential attack/misconfiguration. callback(error, null); + conn.handleFailedAuth(); return { isAuthenticated: false, conn }; } @@ -123,30 +160,48 @@ export const gRPCAuthInterceptor = async ( }; // TODO: Consider logging or informing a central security system about failed verification. callback(error, null); + + if (!isExpired) { + conn.handleFailedAuth(); + } + return { isAuthenticated: false, conn }; } // mTLS binding check and authorization // Connection tunnel verification with serialNumber (mTLS SN vs Token SN) - const peerCert = (call as any).getPeerCertificate(); // Retrieves the mTLS client certificate details - const clientSerialNumber = peerCert ? peerCert.serialNumber : null; - const tokenSerialNumber = payload.serialNumber; // Serial number is inside the signed payload - - if (!clientSerialNumber || tokenSerialNumber !== clientSerialNumber) { - // Critical security failure!!!: The token holder does not match the mTLS certificate holder. - const error = { - name: "AuthenticationError", - code: grpc.status.PERMISSION_DENIED, - message: "Serial number mismatch (mTLS binding failure).", - } as any; - // TODO: This should trigger a high-priority security alert. - callback(error, null); + if ( + !isSerialNumberMatching( + payload, + (call as any).getPeerCertificate(), + callback + ) + ) { + conn.handleFailedAuth(); return { isAuthenticated: false, conn }; } // Validate permission for request method (Authorization check) - const method = String(call.request?.method || "POST").toUpperCase(); - if (!payload.allowedRequests.includes(method as any)) { + if (!isRequestAllowed(payload, call.request, callback)) { + return { isAuthenticated: false, conn }; + } + + // Authentication and Authorization successful + // Update Connection state with SN and status + conn.handleSuccessfulAuth(payload as any); + return { isAuthenticated: true, conn }; +}; + +const isRequestAllowed = ( + tokenPayload: TokenPayload | undefined, + request: any, + callback: grpc.sendUnaryData +): Boolean => { + const method = String(request?.method || "POST").toUpperCase(); + if ( + !tokenPayload?.allowedRequests || + !tokenPayload.allowedRequests.includes(method as any) + ) { const error = { name: "AuthorizationError", code: grpc.status.PERMISSION_DENIED, @@ -154,11 +209,31 @@ export const gRPCAuthInterceptor = async ( } as any; callback(error, null); - return { isAuthenticated: false, conn }; + return false; } - // Authentication and Authorization successful - // Update Connection state with SN and status - conn.handleSuccessfulAuth(payload as any); - return { isAuthenticated: true, conn }; + return true; +}; + +const isSerialNumberMatching = ( + tokenPayload: TokenPayload | undefined, + peerCert: any, + callback: grpc.sendUnaryData +): Boolean => { + const clientSerialNumber = peerCert ? peerCert.serialNumber : null; + const tokenSerialNumber = tokenPayload?.serialNumber; // Serial number is inside the signed payload + + if (!clientSerialNumber || tokenSerialNumber !== clientSerialNumber) { + // Critical security failure!!!: The token holder does not match the mTLS certificate holder. + const error = { + name: "AuthenticationError", + code: grpc.status.PERMISSION_DENIED, + message: "Serial number mismatch (mTLS binding failure).", + } as any; + // TODO: This should trigger a high-priority security alert. + callback(error, null); + return false; + } + + return true; }; From 37e43ece2626eb563f4bcae374c8a400a0bc682f Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Sun, 28 Sep 2025 14:56:05 +0200 Subject: [PATCH 68/81] feat: create SecurityContext class and add signing alg verification to the auth interceptor --- .../Interceptors/grpc.auth.interceptor.ts | 17 ++++--- .../src/utils/security/SecurityContext.ts | 44 +++++++++++++++++++ 2 files changed, 56 insertions(+), 5 deletions(-) create mode 100644 Tokenization/backend/wrapper/src/utils/security/SecurityContext.ts diff --git a/Tokenization/backend/wrapper/src/client/ConnectionManager/Interceptors/grpc.auth.interceptor.ts b/Tokenization/backend/wrapper/src/client/ConnectionManager/Interceptors/grpc.auth.interceptor.ts index 60f93a48d..d48cf977e 100644 --- a/Tokenization/backend/wrapper/src/client/ConnectionManager/Interceptors/grpc.auth.interceptor.ts +++ b/Tokenization/backend/wrapper/src/client/ConnectionManager/Interceptors/grpc.auth.interceptor.ts @@ -139,10 +139,17 @@ export const gRPCAuthInterceptor = async ( pub ); - // Optional: Additional check to ensure correct signing algorithm was used - // if (protectedHeader.alg !== "EdDSA" && protectedHeader.alg !== "Ed25519") { - // throw new Error("JWS signed with an unexpected algorithm."); - // } + // Additional check to ensure correct signing algorithm was used + if (protectedHeader.alg !== "EdDSA" && protectedHeader.alg !== "Ed25519") { + const error = { + name: "AuthenticationError", + message: "Incorrect signing algorithm for JWS.", + code: grpc.status.UNAUTHENTICATED, + }; + + callback(error, null); + return { isAuthenticated: false, conn }; + } // Decode and parse the JWT payload const payloadString = new TextDecoder().decode(jwtPayload); @@ -188,7 +195,7 @@ export const gRPCAuthInterceptor = async ( // Authentication and Authorization successful // Update Connection state with SN and status - conn.handleSuccessfulAuth(payload as any); + conn.handleSuccessfulAuth(payload); return { isAuthenticated: true, conn }; }; diff --git a/Tokenization/backend/wrapper/src/utils/security/SecurityContext.ts b/Tokenization/backend/wrapper/src/utils/security/SecurityContext.ts new file mode 100644 index 000000000..0a4cb1644 --- /dev/null +++ b/Tokenization/backend/wrapper/src/utils/security/SecurityContext.ts @@ -0,0 +1,44 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +/** + * @description Stores every keys and certificates needed for gRPC mTLS communication and token verifications (JWE/JWS) + */ +export class SecurityContext { + // mTLS keys (RSA) + public readonly caCert: Buffer; + public readonly clientSenderCert: Buffer; + public readonly clientListenerCert: Buffer; + public readonly clientPublicKey: Buffer; + // RSA Private Key (PKCS8) for JWE decryption + public readonly clientPrivateKey: Buffer; + + // Public Ed25519 key for JWS verification + public static readonly JWS_PUBLIC_KEY = + "VqkcxlpJYVZI/SxgWH/VqVNeKhMGIbUfHn0okzdGs2E="; + + constructor( + caCert: Buffer, + clientSenderCert: Buffer, + clientListenerCert: Buffer, + clientPrivateKey: Buffer, + clientPublicKey: Buffer + ) { + this.caCert = caCert; + this.clientSenderCert = clientSenderCert; + this.clientListenerCert = clientListenerCert; + this.clientPrivateKey = clientPrivateKey; + this.clientPublicKey = clientPublicKey; + } +} From fe15cd3ae1b5b51e12938e055e29c2532949aaf5 Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Mon, 29 Sep 2025 19:00:13 +0200 Subject: [PATCH 69/81] fix: fix interceptor and write tests --- .../src/client/Connection/Connection.ts | 8 +- .../ConnectionManager/ConnectionManager.ts | 53 +-- .../Interceptors/grpc.auth.interceptor.ts | 78 ++-- .../backend/wrapper/src/client/gRPCWrapper.ts | 82 ++-- .../wrapper/src/models/config.model.ts | 9 +- .../wrapper/src/models/connection.model.ts | 11 +- .../ConnectionManager.test.ts | 39 +- .../grpc.auth.interceptor.test.ts | 402 ++++++++++++++++++ .../client-b-server.crt | 0 .../clientListenerServer/client-b.key | 28 -- .../wrapper/src/test/testCerts/testCerts.ts | 32 +- .../src/utils/security/SecurityContext.ts | 15 +- 12 files changed, 572 insertions(+), 185 deletions(-) create mode 100644 Tokenization/backend/wrapper/src/test/client/ConnectionManager/Interceptors/grpc.auth.interceptor.test.ts rename Tokenization/backend/wrapper/src/test/testCerts/{clientListenerServer => clientListener}/client-b-server.crt (100%) delete mode 100644 Tokenization/backend/wrapper/src/test/testCerts/clientListenerServer/client-b.key diff --git a/Tokenization/backend/wrapper/src/client/Connection/Connection.ts b/Tokenization/backend/wrapper/src/client/Connection/Connection.ts index 4ba96f9b2..b81e0a813 100644 --- a/Tokenization/backend/wrapper/src/client/Connection/Connection.ts +++ b/Tokenization/backend/wrapper/src/client/Connection/Connection.ts @@ -24,9 +24,9 @@ import * as grpc from "@grpc/grpc-js"; import { LogManager } from "@aliceo2/web-ui"; type ConnectionCerts = { - caCert: NonSharedBuffer; - clientCert: NonSharedBuffer; - clientKey: NonSharedBuffer; + caCert: Buffer; + clientCert: Buffer; + clientKey: Buffer; }; /** @@ -247,7 +247,7 @@ export class Connection { // set mandatory grpc metadata const metadata = new grpc.Metadata(); - metadata.set("jweToken", this.jweToken); + metadata.set("jwetoken", this.jweToken); // build body buffer let bodyBuf: Buffer = Buffer.alloc(0); diff --git a/Tokenization/backend/wrapper/src/client/ConnectionManager/ConnectionManager.ts b/Tokenization/backend/wrapper/src/client/ConnectionManager/ConnectionManager.ts index ef8d1e0cf..3fe1d46aa 100644 --- a/Tokenization/backend/wrapper/src/client/ConnectionManager/ConnectionManager.ts +++ b/Tokenization/backend/wrapper/src/client/ConnectionManager/ConnectionManager.ts @@ -26,6 +26,7 @@ import { import { ConnectionStatus } from "../../models/connection.model"; import * as fs from "fs"; import { gRPCAuthInterceptor } from "./Interceptors/grpc.auth.interceptor"; +import { SecurityContext } from "../../utils/security/SecurityContext"; /** * @description Manages all the connection between clients and central system. @@ -57,11 +58,6 @@ export class ConnectionManager { private peerServer?: grpc.Server; private baseAPIPath: string = "localhost:40041/api/"; - // client certificates - private caCert: NonSharedBuffer; - private clientCert: NonSharedBuffer; - private clientKey: NonSharedBuffer; - /** * @description Initializes a new instance of the ConnectionManager class. * @@ -69,16 +65,12 @@ export class ConnectionManager { * * @param protoPath - The file path to the gRPC proto definition. * @param centralAddress - The address of the central gRPC server (default: "localhost:50051"). - * @param caCertPath - Path to the CA certificate file. - * @param clientCertPath - Path to the client certificate file. - * @param clientKeyPath - Path to the client key file. + * @param securityContext - The security context containing certificates and keys for secure communication. */ constructor( protoPath: string, centralAddress: string = "localhost:50051", - caCertPath: string, - clientCertPath: string, - clientKeyPath: string + private readonly securityContext: SecurityContext ) { const packageDef = protoLoader.loadSync(protoPath, { keepCase: true, @@ -92,16 +84,11 @@ export class ConnectionManager { this.wrapper = proto.webui.tokenization; this.peerCtor = this.wrapper.Peer2Peer; - // read certs - this.caCert = fs.readFileSync(caCertPath); - this.clientCert = fs.readFileSync(clientCertPath); - this.clientKey = fs.readFileSync(clientKeyPath); - // create grpc credentials const sslCreds = grpc.credentials.createSsl( - this.caCert, - this.clientKey, - this.clientCert + this.securityContext.caCert, + this.securityContext.clientPrivateKey, + this.securityContext.clientSenderCert ); const centralClient = new this.wrapper.CentralSystem( centralAddress, @@ -174,16 +161,17 @@ export class ConnectionManager { // Create new connection conn = new Connection(jweToken || "", address, direction); - conn.createSslTunnel(this.peerCtor, { - caCert: this.caCert, - clientCert: this.clientCert, - clientKey: this.clientKey, - }); conn.updateStatus(ConnectionStatus.CONNECTING); if (direction === ConnectionDirection.RECEIVING) { this.receivingConnections.set(address, conn); } else { + // open tunnel only on sending connections + conn.createSslTunnel(this.peerCtor, { + caCert: this.securityContext.caCert, + clientCert: this.securityContext.clientSenderCert, + clientKey: this.securityContext.clientPrivateKey, + }); this.sendingConnections.set(address, conn); } conn.updateStatus(ConnectionStatus.CONNECTED); @@ -251,12 +239,17 @@ export class ConnectionManager { /** Starts a listener server for p2p connections */ public async listenForPeers( port: number, - listenerCert: NonSharedBuffer, - listenerPrivateKey: NonSharedBuffer, baseAPIPath?: string ): Promise { if (baseAPIPath) this.baseAPIPath = baseAPIPath; + if (!this.securityContext.clientListenerCert) { + this.logger.errorMessage( + "Listener certificate not provided in gRPCWrapper. Cannot start peer listener." + ); + return; + } + if (this.peerServer) { this.peerServer.forceShutdown(); this.peerServer = undefined; @@ -273,7 +266,7 @@ export class ConnectionManager { call, callback, this.receivingConnections, - listenerPrivateKey + this.securityContext ); if (!isAuthenticated || !conn) { @@ -331,11 +324,11 @@ export class ConnectionManager { }); const sslCreds = grpc.ServerCredentials.createSsl( - this.caCert, + this.securityContext.caCert, [ { - private_key: listenerPrivateKey, - cert_chain: listenerCert, + private_key: this.securityContext.clientPrivateKey, + cert_chain: this.securityContext.clientListenerCert, }, ], true diff --git a/Tokenization/backend/wrapper/src/client/ConnectionManager/Interceptors/grpc.auth.interceptor.ts b/Tokenization/backend/wrapper/src/client/ConnectionManager/Interceptors/grpc.auth.interceptor.ts index d48cf977e..c9a271d56 100644 --- a/Tokenization/backend/wrapper/src/client/ConnectionManager/Interceptors/grpc.auth.interceptor.ts +++ b/Tokenization/backend/wrapper/src/client/ConnectionManager/Interceptors/grpc.auth.interceptor.ts @@ -20,9 +20,7 @@ import { TokenPayload, } from "../../../models/connection.model"; import { ConnectionDirection } from "../../../models/message.model"; - -// IMPORTANT: This key must be securely provided to the interceptor. -const RAW_ED25519_B64_KEY = "VqkcxlpJYVZI/SxgWH/VqVNeKhMGIbUfHn0okzdGs2E="; +import { SecurityContext } from "../../../utils/security/SecurityContext"; /** * @description gRPC interceptor function responsible for JWE decryption, JWS verification, @@ -32,12 +30,13 @@ export const gRPCAuthInterceptor = async ( call: grpc.ServerUnaryCall, callback: grpc.sendUnaryData, clientConnections: Map, - privateKeyBuffer: NonSharedBuffer // RSA Private Key (PKCS8) for JWE decryption + securityContext: SecurityContext ): Promise<{ isAuthenticated: Boolean; conn: Connection | null }> => { const metadata = call.metadata.getMap(); - const jweToken = metadata.jweToken as string; + const jweToken = metadata.jwetoken as string; const clientAddress = call.getPeer(); let conn = clientConnections.get(clientAddress); + const peerCert = getPeerCertFromCall(call); // Check if token exists if (!jweToken) { @@ -74,7 +73,7 @@ export const gRPCAuthInterceptor = async ( if ( !isSerialNumberMatching( conn.getCachedTokenPayload(), - (call as any).getPeerCertificate(), + peerCert, callback ) ) { @@ -100,7 +99,7 @@ export const gRPCAuthInterceptor = async ( try { // Importing RSA private key for decryption privateKey = await importPKCS8( - privateKeyBuffer.toString("utf-8"), + securityContext.clientPrivateKey.toString("utf-8"), "RSA-OAEP-256" ); @@ -127,7 +126,9 @@ export const gRPCAuthInterceptor = async ( const jwk = { kty: "OKP", crv: "Ed25519", - x: Buffer.from(RAW_ED25519_B64_KEY, "base64").toString("base64url"), + x: Buffer.from(securityContext.JWS_PUBLIC_KEY, "base64").toString( + "base64url" + ), }; // Importing the Ed25519 public key for verification - using "EdDSA" algorithm @@ -177,13 +178,7 @@ export const gRPCAuthInterceptor = async ( // mTLS binding check and authorization // Connection tunnel verification with serialNumber (mTLS SN vs Token SN) - if ( - !isSerialNumberMatching( - payload, - (call as any).getPeerCertificate(), - callback - ) - ) { + if (!isSerialNumberMatching(payload, peerCert, callback)) { conn.handleFailedAuth(); return { isAuthenticated: false, conn }; } @@ -199,16 +194,20 @@ export const gRPCAuthInterceptor = async ( return { isAuthenticated: true, conn }; }; -const isRequestAllowed = ( +/** + * @description Checks if the request method is allowed based on the token permissions. + * @param tokenPayload payload extracted from the token + * @param request gRPC request object containing method information + * @param callback callback to return gRPC error if needed + * @returns true if request method is allowed, false otherwise + */ +export const isRequestAllowed = ( tokenPayload: TokenPayload | undefined, request: any, callback: grpc.sendUnaryData ): Boolean => { const method = String(request?.method || "POST").toUpperCase(); - if ( - !tokenPayload?.allowedRequests || - !tokenPayload.allowedRequests.includes(method as any) - ) { + if (!tokenPayload?.perm || !Object.keys(tokenPayload.perm).includes(method)) { const error = { name: "AuthorizationError", code: grpc.status.PERMISSION_DENIED, @@ -222,25 +221,50 @@ const isRequestAllowed = ( return true; }; -const isSerialNumberMatching = ( +/** + * @description Checks if the serial number from the peer certificate matches the one in the token payload. + * @param tokenPayload payload extracted from the token + * @param peerCert certificate object retrieved from the gRPC call + * @param callback callback to return gRPC error if needed + * @returns true if serial numbers match, false otherwise + */ +export const isSerialNumberMatching = ( tokenPayload: TokenPayload | undefined, peerCert: any, callback: grpc.sendUnaryData ): Boolean => { - const clientSerialNumber = peerCert ? peerCert.serialNumber : null; - const tokenSerialNumber = tokenPayload?.serialNumber; // Serial number is inside the signed payload + const clientSN = normalizeSerial(peerCert?.serialNumber); + const tokenSN = normalizeSerial(tokenPayload?.subSerialNumber); - if (!clientSerialNumber || tokenSerialNumber !== clientSerialNumber) { - // Critical security failure!!!: The token holder does not match the mTLS certificate holder. + if (!clientSN || clientSN !== tokenSN) { const error = { name: "AuthenticationError", code: grpc.status.PERMISSION_DENIED, message: "Serial number mismatch (mTLS binding failure).", } as any; - // TODO: This should trigger a high-priority security alert. callback(error, null); return false; } - return true; }; + +/** + * @description Normalizes a certificate serial number by removing colons and converting to uppercase. + * @param sn serial number string possibly containing colons or being null/undefined + * @returns normalized serial number string + */ +const normalizeSerial = (sn?: string | null): string => { + // Node retrieves serial number as hex string, without leading 0x and with possible colons so we need to normalize it + return (sn || "").replace(/[^0-9a-f]/gi, "").toUpperCase(); +}; + +/** + * @description Retrieves the peer certificate from the gRPC call object. + * @param call gRPC call object + * @returns peer certificate object from the gRPC call + */ +export const getPeerCertFromCall = (call: any) => { + const session = call?.call?.stream?.session; + const sock = session?.socket as any; + return sock?.getPeerCertificate(true); // whole certificate info from TLS socket +}; diff --git a/Tokenization/backend/wrapper/src/client/gRPCWrapper.ts b/Tokenization/backend/wrapper/src/client/gRPCWrapper.ts index b8d617abc..74e11b1d7 100644 --- a/Tokenization/backend/wrapper/src/client/gRPCWrapper.ts +++ b/Tokenization/backend/wrapper/src/client/gRPCWrapper.ts @@ -21,8 +21,8 @@ import { import { Connection } from "./Connection/Connection"; import { NewTokenHandler } from "./Commands/newToken/newToken.handler"; import { gRPCWrapperConfig } from "../models/config.model"; +import { SecurityContext } from "../utils/security/SecurityContext"; import * as fs from "fs"; -import { LogManager } from "@aliceo2/web-ui"; /** * @description Wrapper class for managing secure gRPC wrapper. @@ -35,21 +35,17 @@ import { LogManager } from "@aliceo2/web-ui"; * @example * ```typescript * const grpcWrapper = new gRPCWrapper(PROTO_PATH, CENTRAL_SYSTEM_ADDRESS); - * // Use grpcWrapper to interact with gRPC services + * Use grpcWrapper to interact with gRPC services * ``` */ export class gRPCWrapper { private ConnectionManager: ConnectionManager; - private listenerPublicKey?: NonSharedBuffer; - private listenerPrivateKey?: NonSharedBuffer; - private listenerCert?: NonSharedBuffer; - private logger = LogManager.getLogger("gRPCWrapper"); + private securityContext: SecurityContext; /** * @description Initializes an instance of gRPCWrapper class. * - * @param protoPath - The file path to the gRPC proto definition. - * @param centralAddress - The address of the central gRPC server (default: "localhost:50051"). + * @param config - External configuration object containing necessary paths and addresses. */ constructor(config: gRPCWrapperConfig) { if ( @@ -58,31 +54,42 @@ export class gRPCWrapper { !config.clientCerts || !config.clientCerts.caCertPath || !config.clientCerts.certPath || - !config.clientCerts.publicKeyPath + !config.clientCerts.publicKeyPath || + !config.clientCerts.privateKeyPath ) { - throw new Error("Invalid gRPCWrapper configuration provided."); + throw new Error( + "Invalid gRPCWrapper configuration provided. Missing required paths." + ); } - if ( - config.listenerCertPaths?.publicKeyPath && - config.listenerCertPaths?.certPath - ) { - this.listenerPublicKey = fs.readFileSync( - config.listenerCertPaths.publicKeyPath - ); - this.listenerPrivateKey = fs.readFileSync( - config.listenerCertPaths.privateKeyPath - ); - this.listenerCert = fs.readFileSync(config.listenerCertPaths.certPath); + let clientListenerCert: Buffer = Buffer.alloc(0); + + // Klucze do wysyłania (Sender) są obowiązkowe + const caCert = fs.readFileSync(config.clientCerts.caCertPath); + const clientSenderCert = fs.readFileSync(config.clientCerts.certPath); + const clientPublicKey = fs.readFileSync(config.clientCerts.publicKeyPath); + const clientPrivateKey = fs.readFileSync(config.clientCerts.privateKeyPath); + + if (config.listenerCertPath) { + // If we have dedicated listener cert, use it + clientListenerCert = fs.readFileSync(config.listenerCertPath); } + this.securityContext = new SecurityContext( + caCert, + clientSenderCert, + clientPrivateKey, + clientPublicKey, + clientListenerCert + ); + this.ConnectionManager = new ConnectionManager( config.protoPath, config.centralAddress, - config.clientCerts.caCertPath, - config.clientCerts.certPath, - config.clientCerts.publicKeyPath + this.securityContext ); + + // Register all command handlers this.ConnectionManager.registerCommandHandlers([ { event: DuplexMessageEvent.MESSAGE_EVENT_REVOKE_TOKEN, @@ -121,32 +128,9 @@ export class gRPCWrapper { */ public async listenForPeers( port: number, - baseAPIPath?: string, - listenerCertPaths?: { keyPath: string; certPath: string } + baseAPIPath?: string ): Promise { - if (listenerCertPaths?.keyPath && listenerCertPaths?.certPath) { - this.listenerPublicKey = fs.readFileSync(listenerCertPaths.keyPath); - this.listenerCert = fs.readFileSync(listenerCertPaths.certPath); - } - - if ( - !this.listenerPublicKey || - !this.listenerPrivateKey || - !this.listenerCert - ) { - this.logger.errorMessage( - "Listener certificates are required to start P2P listener. Please provide valid paths." - ); - return; - } - - return this.ConnectionManager.listenForPeers( - port, - this.listenerPublicKey, - this.listenerCert, - this.listenerPrivateKey, - baseAPIPath - ); + return this.ConnectionManager.listenForPeers(port, baseAPIPath); } /** diff --git a/Tokenization/backend/wrapper/src/models/config.model.ts b/Tokenization/backend/wrapper/src/models/config.model.ts index 2e4e9e9ac..3167065d1 100644 --- a/Tokenization/backend/wrapper/src/models/config.model.ts +++ b/Tokenization/backend/wrapper/src/models/config.model.ts @@ -37,14 +37,11 @@ export interface gRPCWrapperConfig { /** Client TLS certificates paths. */ clientCerts: { caCertPath: string; - publicKeyPath: string; - certPath: string; - }; - - /** Optional listener TLS certificates paths. If provided, the gRPCWrapper will be able to accept incoming connections. */ - listenerCertPaths?: { publicKeyPath: string; privateKeyPath: string; certPath: string; }; + + /** Optional listener TLS certificate path. If provided, the gRPCWrapper will be able to accept incoming connections. */ + listenerCertPath?: string; } diff --git a/Tokenization/backend/wrapper/src/models/connection.model.ts b/Tokenization/backend/wrapper/src/models/connection.model.ts index 6f7b34183..dc9c97c61 100644 --- a/Tokenization/backend/wrapper/src/models/connection.model.ts +++ b/Tokenization/backend/wrapper/src/models/connection.model.ts @@ -33,6 +33,7 @@ export enum ConnectionStatus { export type ConnectionHeaders = Record; +// Options for making fetch-like requests over a connection export type FetchOptions = { method?: string; path?: string; @@ -40,6 +41,7 @@ export type FetchOptions = { body?: string | Buffer | Uint8Array | null; }; +// A more specific type for fetch responses, including status, headers, and body export type FetchResponse = { status: number; headers: ConnectionHeaders; @@ -63,7 +65,12 @@ export type HttpLikeResponse = { body: Buffer; }; +// Payload structure for authentication tokens export type TokenPayload = { - serialNumber: string; - allowedRequests: ("POST" | "GET" | "PUT" | "DELETE" | "PATCH")[]; + subSerialNumber: string; + aud: string; + perm: Object; + iat: number; + exp: number; + jti: string; }; diff --git a/Tokenization/backend/wrapper/src/test/client/ConnectionManager/ConnectionManager.test.ts b/Tokenization/backend/wrapper/src/test/client/ConnectionManager/ConnectionManager.test.ts index 00d743b90..0ed5b11e1 100644 --- a/Tokenization/backend/wrapper/src/test/client/ConnectionManager/ConnectionManager.test.ts +++ b/Tokenization/backend/wrapper/src/test/client/ConnectionManager/ConnectionManager.test.ts @@ -18,10 +18,7 @@ import { ConnectionDirection, DuplexMessageEvent, } from "../../../models/message.model"; -import { - getTestCentralCertPaths, - getTestCerts, -} from "../../testCerts/testCerts"; +import { SecurityContext } from "../../../utils/security/SecurityContext"; // Mock duplex stream const mockStream = { @@ -146,7 +143,14 @@ jest.mock("@grpc/grpc-js", () => { describe("ConnectionManager", () => { let conn: ConnectionManager; - const { caCertPath, certPath, keyPath } = getTestCentralCertPaths(); + const MOCK_CERT = Buffer.from("MOCK_CERT"); + const securityContext = new SecurityContext( + MOCK_CERT, + MOCK_CERT, + MOCK_CERT, + MOCK_CERT, + MOCK_CERT + ); beforeEach(() => { jest.clearAllMocks(); @@ -155,9 +159,7 @@ describe("ConnectionManager", () => { conn = new ConnectionManager( "dummy.proto", "localhost:12345", - caCertPath, - certPath, - keyPath + securityContext ); }); @@ -241,12 +243,7 @@ describe("ConnectionManager", () => { }); test("listenForPeers() should start server and register service", async () => { - await conn.listenForPeers( - 50055, - getTestCerts().clientCert, - getTestCerts().caCert, - "http://localhost:40041/api/" - ); + await conn.listenForPeers(50055, "http://localhost:40041/api/"); const serverCtor = (grpc.Server as any).mock; expect(serverCtor).toBeDefined(); @@ -265,12 +262,7 @@ describe("ConnectionManager", () => { }); test("p2p Fetch should register incoming receiving connection and forward request", async () => { - await conn.listenForPeers( - 50056, - getTestCerts().clientCert, - getTestCerts().caCert, - "http://localhost:40041/api/" - ); + await conn.listenForPeers(50056, "http://localhost:40041/api/"); // prepare data to call const call = { @@ -328,12 +320,7 @@ describe("ConnectionManager", () => { }); test("p2p Fetch should return INTERNAL on forward error", async () => { - await conn.listenForPeers( - 50057, - getTestCerts().clientCert, - getTestCerts().caCert, - "http://localhost:40041/api/" - ); + await conn.listenForPeers(50057, "http://localhost:40041/api/"); const call = { getPeer: () => "client-error", diff --git a/Tokenization/backend/wrapper/src/test/client/ConnectionManager/Interceptors/grpc.auth.interceptor.test.ts b/Tokenization/backend/wrapper/src/test/client/ConnectionManager/Interceptors/grpc.auth.interceptor.test.ts new file mode 100644 index 000000000..791e9a4a8 --- /dev/null +++ b/Tokenization/backend/wrapper/src/test/client/ConnectionManager/Interceptors/grpc.auth.interceptor.test.ts @@ -0,0 +1,402 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +import * as grpc from "@grpc/grpc-js"; +import * as jose from "jose"; +import * as interceptor from "../../../../client/ConnectionManager/Interceptors/grpc.auth.interceptor"; + +// Connection class mock +jest.mock( + "../../../../client/Connection/Connection", + () => { + return { + Connection: jest + .fn() + .mockImplementation( + (jweToken: string, address: string, direction: any) => { + return { + jweToken, + address, + direction, + status: 1, + payload: { subSerialNumber: "AABBCC", perm: { POST: true } }, + getStatus: jest.fn(function () { + return this.status; + }), + getToken: jest.fn(function () { + return this.jweToken; + }), + getCachedTokenPayload: jest.fn(function () { + return this.payload; + }), + handleFailedAuth: jest.fn(), + handleSuccessfulAuth: jest.fn(function (p: any) { + this.payload = p; + this.status = 1; + }), + }; + } + ), + }; + }, + { virtual: true } +); + +import { Connection } from "../../../../client/Connection/Connection"; + +jest.mock("jose", () => ({ + importPKCS8: jest.fn(), + importJWK: jest.fn(), + compactDecrypt: jest.fn(), + compactVerify: jest.fn(), +})); + +import { + ConnectionStatus, + TokenPayload, +} from "../../../../models/connection.model"; +import { SecurityContext } from "../../../../utils/security/SecurityContext"; +import { ConnectionDirection } from "../../../../models/message.model"; + +const mockSecurityContext = { + clientPrivateKey: Buffer.from("mock_private_key_rsa"), + JWS_PUBLIC_KEY: "mock_public_key_ed25519", +} as unknown as SecurityContext; + +let isRequestAllowedSpy: jest.SpyInstance; +let isSerialNumberMatchingSpy: jest.SpyInstance; +let getPeerCertFromCallSpy: jest.SpyInstance; + +const mockCall = { + metadata: { getMap: jest.fn(() => ({})) }, + getPeer: jest.fn(() => "ipv4:127.0.0.1:12345"), + request: { method: "POST" }, +} as unknown as grpc.ServerUnaryCall; + +const mockCallback = jest.fn(); +const mockClientConnections = new Map(); + +describe("gRPCAuthInterceptor", () => { + const MOCK_ADDRESS = "ipv4:127.0.0.1:12345"; + const VALID_JWE = "valid.jwe.token"; + const VALID_JWS = "valid.jws.token"; + const DECRYPTED_PAYLOAD: TokenPayload = { + subSerialNumber: "DDEEFF", + perm: { POST: true, GET: false }, + } as any; + + beforeEach(() => { + jest.clearAllMocks(); + mockClientConnections.clear(); + + (mockCall.metadata.getMap as unknown as jest.Mock).mockReturnValue({ + jwetoken: VALID_JWE, + }); + (mockCall.getPeer as unknown as jest.Mock).mockReturnValue(MOCK_ADDRESS); + (mockCall as any).request = { method: "POST" }; + + (jose.importPKCS8 as jest.Mock).mockResolvedValue("mock_priv_key"); + (jose.compactDecrypt as jest.Mock).mockResolvedValue({ + plaintext: Buffer.from(VALID_JWS), + }); + (jose.importJWK as jest.Mock).mockResolvedValue("mock_pub_key"); + (jose.compactVerify as jest.Mock).mockResolvedValue({ + payload: Buffer.from(JSON.stringify(DECRYPTED_PAYLOAD)), + protectedHeader: { alg: "EdDSA" }, + }); + + // mocks of internal functions + isRequestAllowedSpy = jest + .spyOn(interceptor, "isRequestAllowed") + .mockImplementation((_p, _r, _cb) => true); + + isSerialNumberMatchingSpy = jest + .spyOn(interceptor, "isSerialNumberMatching") + .mockImplementation((_p, _pc, _cb) => true); + + getPeerCertFromCallSpy = jest + .spyOn(interceptor, "getPeerCertFromCall") + .mockReturnValue({ serialNumber: "DDEEFF" }); + }); + + const getCreatedConn = () => { + const instances = (Connection as jest.Mock).mock?.instances ?? []; + return ( + instances.find((i: any) => i.address === MOCK_ADDRESS) ?? + mockClientConnections.get(MOCK_ADDRESS) + ); + }; + + it("should fail if no JWE token is provided in the metadata", async () => { + (mockCall.metadata.getMap as unknown as jest.Mock).mockReturnValue({}); + + const result = await interceptor.gRPCAuthInterceptor( + mockCall, + mockCallback, + mockClientConnections as any, + mockSecurityContext + ); + + expect(result.isAuthenticated).toBe(false); + expect(result.conn).toBe(null); + expect(mockCallback).toHaveBeenCalledWith( + expect.objectContaining({ + code: grpc.status.UNAUTHENTICATED, + message: "No token provided", + }), + null + ); + }); + + it("should authenticate instantly if connection exists and token hasn't changed", async () => { + const existingConn = new (Connection as jest.Mock)( + VALID_JWE, + MOCK_ADDRESS, + ConnectionDirection.RECEIVING + ); + existingConn.getToken.mockReturnValue(VALID_JWE); + mockClientConnections.set(MOCK_ADDRESS, existingConn); + + const result = await interceptor.gRPCAuthInterceptor( + mockCall, + mockCallback, + mockClientConnections as any, + mockSecurityContext + ); + + expect(result.isAuthenticated).toBe(true); + expect(result.conn).toBe(existingConn); + expect(isRequestAllowedSpy).toHaveBeenCalledTimes(1); + expect(isSerialNumberMatchingSpy).toHaveBeenCalledTimes(1); + expect(jose.compactDecrypt as jest.Mock).not.toHaveBeenCalled(); + }); + + it("should reject if connection exists but is BLOCKED", async () => { + const existingConn = new (Connection as jest.Mock)( + VALID_JWE, + MOCK_ADDRESS, + ConnectionDirection.RECEIVING + ); + existingConn.status = ConnectionStatus.BLOCKED; + mockClientConnections.set(MOCK_ADDRESS, existingConn); + + const result = await interceptor.gRPCAuthInterceptor( + mockCall, + mockCallback, + mockClientConnections as any, + mockSecurityContext + ); + + expect(result.isAuthenticated).toBe(false); + expect(mockCallback).toHaveBeenCalledWith( + expect.objectContaining({ + code: grpc.status.UNAUTHENTICATED, + message: "Connection is blocked. Contact administrator.", + }), + null + ); + }); + + it("should reject existing connection on serial number mismatch", async () => { + const existingConn = new (Connection as jest.Mock)( + VALID_JWE, + MOCK_ADDRESS, + ConnectionDirection.RECEIVING + ); + existingConn.getToken.mockReturnValue(VALID_JWE); + mockClientConnections.set(MOCK_ADDRESS, existingConn); + + // mock serial number mismatch + isSerialNumberMatchingSpy.mockImplementation((_p, _pc, cb) => { + cb( + { + name: "AuthenticationError", + code: grpc.status.PERMISSION_DENIED, + message: "Serial number mismatch (mTLS binding failure).", + } as any, + null + ); + return false; + }); + + const result = await interceptor.gRPCAuthInterceptor( + mockCall, + mockCallback, + mockClientConnections as any, + mockSecurityContext + ); + + expect(result.isAuthenticated).toBe(false); + expect(existingConn.handleFailedAuth).toHaveBeenCalledTimes(1); + expect(mockCallback).toHaveBeenCalledWith( + expect.objectContaining({ + message: "Serial number mismatch (mTLS binding failure).", + }), + null + ); + }); + + it("should successfully authenticate a NEW connection", async () => { + (mockCall.metadata.getMap as unknown as jest.Mock).mockReturnValue({ + jwetoken: "NEW.JWE.TOKEN", + }); + + const result = await interceptor.gRPCAuthInterceptor( + mockCall, + mockCallback, + mockClientConnections as any, + mockSecurityContext + ); + + const created = getCreatedConn(); + expect(result.isAuthenticated).toBe(true); + expect(created).toBeDefined(); + expect(created!.handleSuccessfulAuth).toHaveBeenCalledWith( + DECRYPTED_PAYLOAD + ); + expect(jose.compactDecrypt as jest.Mock).toHaveBeenCalledTimes(1); + expect(jose.compactVerify as jest.Mock).toHaveBeenCalledTimes(1); + expect(isSerialNumberMatchingSpy).toHaveBeenCalledTimes(1); + expect(isRequestAllowedSpy).toHaveBeenCalledTimes(1); + }); + + it("should fail if JWE decryption fails", async () => { + (jose.compactDecrypt as jest.Mock).mockRejectedValue( + new Error("Decryption failed") + ); + + const result = await interceptor.gRPCAuthInterceptor( + mockCall, + mockCallback, + mockClientConnections as any, + mockSecurityContext + ); + + const created = getCreatedConn(); + expect(result.isAuthenticated).toBe(false); + expect(created!.handleFailedAuth).toHaveBeenCalledTimes(1); + expect(mockCallback).toHaveBeenCalledWith( + expect.objectContaining({ + message: "Incorrect token provided (JWE Decryption failed)", + }), + null + ); + }); + + it("should fail if JWS verification fails", async () => { + (jose.compactVerify as jest.Mock).mockRejectedValue( + new Error("Invalid signature") + ); + + const result = await interceptor.gRPCAuthInterceptor( + mockCall, + mockCallback, + mockClientConnections as any, + mockSecurityContext + ); + + const created = getCreatedConn(); + expect(result.isAuthenticated).toBe(false); + expect(created!.handleFailedAuth).toHaveBeenCalledTimes(1); + expect(mockCallback).toHaveBeenCalledWith( + expect.objectContaining({ + message: "JWS Verification error: Invalid signature", + }), + null + ); + }); + + it("should fail if JWS is expired", async () => { + (jose.compactVerify as jest.Mock).mockRejectedValue({ + message: "JWT is expired", + }); + + const result = await interceptor.gRPCAuthInterceptor( + mockCall, + mockCallback, + mockClientConnections as any, + mockSecurityContext + ); + + const created = getCreatedConn(); + expect(result.isAuthenticated).toBe(false); + expect(created!.handleFailedAuth).not.toHaveBeenCalled(); // for expired token, we do not block the connection + expect(mockCallback).toHaveBeenCalledWith( + expect.objectContaining({ + message: "JWS Verification error: Token expired", + code: grpc.status.UNAUTHENTICATED, + }), + null + ); + }); + + it("should fail if mTLS serial number mismatch occurs after decryption", async () => { + isSerialNumberMatchingSpy.mockImplementation((_p, _pc, cb) => { + cb( + { + name: "AuthenticationError", + code: grpc.status.PERMISSION_DENIED, + message: "Serial number mismatch (mTLS binding failure).", + } as any, + null + ); + return false; + }); + + const result = await interceptor.gRPCAuthInterceptor( + mockCall, + mockCallback, + mockClientConnections as any, + mockSecurityContext + ); + + const created = getCreatedConn(); + expect(result.isAuthenticated).toBe(false); + expect(created!.handleFailedAuth).toHaveBeenCalledTimes(1); + expect(mockCallback).toHaveBeenCalledWith( + expect.objectContaining({ + message: "Serial number mismatch (mTLS binding failure).", + }), + null + ); + }); + + it("should fail if request authorization check fails", async () => { + isRequestAllowedSpy.mockImplementation((_p, _r, cb) => { + cb( + { + name: "AuthorizationError", + code: grpc.status.PERMISSION_DENIED, + message: "Request of type POST is not allowed by the token policy.", + } as any, + null + ); + return false; + }); + + const result = await interceptor.gRPCAuthInterceptor( + mockCall, + mockCallback, + mockClientConnections as any, + mockSecurityContext + ); + + expect(result.isAuthenticated).toBe(false); + expect(mockCallback).toHaveBeenCalledWith( + expect.objectContaining({ code: grpc.status.PERMISSION_DENIED }), + null + ); + expect(isSerialNumberMatchingSpy).toHaveBeenCalledTimes(1); + }); +}); diff --git a/Tokenization/backend/wrapper/src/test/testCerts/clientListenerServer/client-b-server.crt b/Tokenization/backend/wrapper/src/test/testCerts/clientListener/client-b-server.crt similarity index 100% rename from Tokenization/backend/wrapper/src/test/testCerts/clientListenerServer/client-b-server.crt rename to Tokenization/backend/wrapper/src/test/testCerts/clientListener/client-b-server.crt diff --git a/Tokenization/backend/wrapper/src/test/testCerts/clientListenerServer/client-b.key b/Tokenization/backend/wrapper/src/test/testCerts/clientListenerServer/client-b.key deleted file mode 100644 index b2fc830da..000000000 --- a/Tokenization/backend/wrapper/src/test/testCerts/clientListenerServer/client-b.key +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDHGqAsEkV5CAtx -G+Q+PpLXqC5Imwk38wnEYCeZZAXe62v2d5Rd4gPYpQQw1RPZvvcQ78sPfP7wm8OF -8bgn1jX+fL8K5oBuYcpcf+i3ScjBaN3XXJeOFbJdC42oYeK6eJfYpoMfn6sg9dhT -p5SPH5XdVfSKJgQVSLM9FSHOYnwA41K7jc64x2FmjsWtvipRzj+KhWqGIBvdJ2b+ -coNYj05BHpTHwxzY5o9jfBADivdkHP2glTcbsEvzltyWaTU8NmFHpPT+ijT0WJOP -zNNny+ihqskf5MAPYQGxPB8h0FPXK4QIC57O3FqizarZNSwnPd/qFBoSc6qLPqdn -wVx5e52BAgMBAAECggEAA0i8JZ3ziWiJj8cO/7vWfjom8UmlYEfg/F09qfkNY7zs -Xfdg+h91QsiOBiQtnKTavGvIJKxCJEPdeMMg739ICreSCyL8MVXpmZb+hq9v4UjS -h+/eDBjthT1gi8t5iuvcTVWJyia/Et8bP13/RFEYDruROgogfR1i33oOwbG8K+OM -iDSIRo9swBaVNqFuWZKqZr1pjY9KUfh4jnSA+x1hueVHREJYt6qvX2KNhHFhyfyo -RRuLSKmz7Xx3sTu7qQD1NRKsD/8lM4oZWGPTit0F6qx+FfzbI0nBn9In8czmrPcm -VqwdgT5aR5I6lfzAj6kMHlmY2n0f4J0Hk6Ha6MgkJQKBgQD6AnjeToUw6PcIbTrJ -18mDfzOCMhQjQHDXipHrJ06K65B+4XX8/oTkRahyeqn4sbKKHP11e6bsDPVNRK0P -9G3wYw7nPNcGtF1pojWvh0HxBTJ/iugDgZT5ngtwbydvAT7+m6De3DicEJo5ZUPP -IAweL+qf9Nh30PtKiOSgJ2GrRwKBgQDL3+eX0sQg+dfYb3zAJmsLwXoY4fAOGc1o -KI4UI26Bq4TPrJv9guaDjSYNE62M7+H7vWudlG+KPxt6jNaxxpIo5J1c7qVl+gx6 -kTDZ053peVLtWJzLrRT9/e238bCYnKfFpKwRzUGT9kEkMvmFqb56lnhKBXS/OPuP -3dAOWHvE9wKBgAJM4YXSHSGdEyDNuHvA84a1NekdwtesMR2alcsfGnbmwfaY5ngE -c36SMYGUJVo3cFga+i4JjDihyeQDHMCH1DchAjMYeTYDlNRy/KF30iCAlr1brtTR -bWh6jspjC27XCRhYoDtMtWyiLnkWuHAAcHwansMIArHfh2BhMBFVK23jAoGAGwrf -GFdfppQdWlsnbAFsj4mhXW2SvvwTL+65MdilTtPmcPmPU2gqlWaClpd2nMww6Ihu -nt9SkD7gsTe/PqN9PaldajdJfyZUw2lA1pPoTVDHfC4V1jpmH26wOob3ira01lWK -cW4NdcfjSh7s1Br45h/RYtgobTjsvV+Jum1oNW8CgYAeY82AEydYzqjZRH66xAjQ -pSxaT6B2YS5JqJFbRAtHA/ndApHbW7ALX5vqoJ6EkJ+aORQQMZUkX32n3Y0WFNE2 -FR9JEcnBjimwmG+JTBRCk1luamsZZpH/sHZohH1bsq9+dGo9xIeeBdi5DPxpdsKM -6NuWljVhDbuJeRiamlo5tg== ------END PRIVATE KEY----- diff --git a/Tokenization/backend/wrapper/src/test/testCerts/testCerts.ts b/Tokenization/backend/wrapper/src/test/testCerts/testCerts.ts index d47f86cd7..3e2f3ef93 100644 --- a/Tokenization/backend/wrapper/src/test/testCerts/testCerts.ts +++ b/Tokenization/backend/wrapper/src/test/testCerts/testCerts.ts @@ -28,15 +28,20 @@ export const getTestClientListenerCertPaths = __dirname, "./clientListener/client-b-client.crt" ); - const CLIENT_KEY_PATH = path.join( + const CLIENT_PRIVATE_KEY_PATH = path.join( __dirname, "./clientListener/client-b.key" ); + const CLIENT_PUBLIC_KEY_PATH = path.join( + __dirname, + "./clientListener/client-b.pub.pem" + ); return { caCertPath: CA_CERT_PATH, certPath: CLIENT_CERT_PATH, - publicKeyPath: CLIENT_KEY_PATH, + privateKeyPath: CLIENT_PRIVATE_KEY_PATH, + publicKeyPath: CLIENT_PUBLIC_KEY_PATH, }; }; @@ -47,15 +52,20 @@ export const getTestClientListenerServerCertPaths = __dirname, "./clientListenerServer/client-b-server.crt" ); - const CLIENT_KEY_PATH = path.join( + const CLIENT_PRIVATE_KEY_PATH = path.join( + __dirname, + "./clientListener/client-b.key" + ); + const CLIENT_PUBLIC_KEY_PATH = path.join( __dirname, - "./clientListenerServer/client-b.key" + "./clientListener/client-b.pub.pem" ); return { caCertPath: CA_CERT_PATH, certPath: CLIENT_CERT_PATH, - publicKeyPath: CLIENT_KEY_PATH, + publicKeyPath: CLIENT_PUBLIC_KEY_PATH, + privateKeyPath: CLIENT_PRIVATE_KEY_PATH, }; }; @@ -66,12 +76,20 @@ export const getTestClientSenderCertPaths = __dirname, "./clientSender/client-a-client.crt" ); - const CLIENT_KEY_PATH = path.join(__dirname, "./clientSender/client-a.key"); + const CLIENT_PRIVATE_KEY_PATH = path.join( + __dirname, + "./clientSender/client-a.key" + ); + const CLIENT_PUBLIC_KEY_PATH = path.join( + __dirname, + "./clientSender/client-a.pub.pem" + ); return { caCertPath: CA_CERT_PATH, certPath: CLIENT_CERT_PATH, - publicKeyPath: CLIENT_KEY_PATH, + privateKeyPath: CLIENT_PRIVATE_KEY_PATH, + publicKeyPath: CLIENT_PUBLIC_KEY_PATH, }; }; diff --git a/Tokenization/backend/wrapper/src/utils/security/SecurityContext.ts b/Tokenization/backend/wrapper/src/utils/security/SecurityContext.ts index 0a4cb1644..a60315036 100644 --- a/Tokenization/backend/wrapper/src/utils/security/SecurityContext.ts +++ b/Tokenization/backend/wrapper/src/utils/security/SecurityContext.ts @@ -19,26 +19,29 @@ export class SecurityContext { // mTLS keys (RSA) public readonly caCert: Buffer; public readonly clientSenderCert: Buffer; - public readonly clientListenerCert: Buffer; + public readonly clientListenerCert?: Buffer; public readonly clientPublicKey: Buffer; // RSA Private Key (PKCS8) for JWE decryption public readonly clientPrivateKey: Buffer; // Public Ed25519 key for JWS verification - public static readonly JWS_PUBLIC_KEY = - "VqkcxlpJYVZI/SxgWH/VqVNeKhMGIbUfHn0okzdGs2E="; + public readonly JWS_PUBLIC_KEY = + "hTb3l5gwoIWISOLi6cQMwcultawKyA6vxnimXWtE6JI="; constructor( caCert: Buffer, clientSenderCert: Buffer, - clientListenerCert: Buffer, clientPrivateKey: Buffer, - clientPublicKey: Buffer + clientPublicKey: Buffer, + clientListenerCert?: Buffer ) { this.caCert = caCert; this.clientSenderCert = clientSenderCert; - this.clientListenerCert = clientListenerCert; this.clientPrivateKey = clientPrivateKey; this.clientPublicKey = clientPublicKey; + + if (clientListenerCert) { + this.clientListenerCert = clientListenerCert; + } } } From cbc3ede7b1c48b2ea50ecd57f6479322b55aa720 Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Wed, 1 Oct 2025 21:34:25 +0200 Subject: [PATCH 70/81] fix: unit tests and update token interface --- .../Interceptors/grpc.auth.interceptor.ts | 86 +++++++++++++++---- .../wrapper/src/models/connection.model.ts | 18 ++-- .../grpc.auth.interceptor.test.ts | 24 ------ .../src/utils/security/SecurityContext.ts | 12 ++- 4 files changed, 93 insertions(+), 47 deletions(-) diff --git a/Tokenization/backend/wrapper/src/client/ConnectionManager/Interceptors/grpc.auth.interceptor.ts b/Tokenization/backend/wrapper/src/client/ConnectionManager/Interceptors/grpc.auth.interceptor.ts index c9a271d56..5e959ed2b 100644 --- a/Tokenization/backend/wrapper/src/client/ConnectionManager/Interceptors/grpc.auth.interceptor.ts +++ b/Tokenization/backend/wrapper/src/client/ConnectionManager/Interceptors/grpc.auth.interceptor.ts @@ -156,23 +156,15 @@ export const gRPCAuthInterceptor = async ( const payloadString = new TextDecoder().decode(jwtPayload); payload = JSON.parse(payloadString); } catch (e: any) { - const isExpired = e.message?.includes("expired"); const error = { name: "AuthenticationError", - message: `JWS Verification error: ${ - isExpired ? "Token expired" : "Invalid signature" - }`, - code: isExpired - ? grpc.status.UNAUTHENTICATED - : grpc.status.PERMISSION_DENIED, + message: `JWS Verification error: Invalid signature`, + code: grpc.status.PERMISSION_DENIED, }; // TODO: Consider logging or informing a central security system about failed verification. callback(error, null); - if (!isExpired) { - conn.handleFailedAuth(); - } - + conn.handleFailedAuth(); return { isAuthenticated: false, conn }; } @@ -207,11 +199,25 @@ export const isRequestAllowed = ( callback: grpc.sendUnaryData ): Boolean => { const method = String(request?.method || "POST").toUpperCase(); - if (!tokenPayload?.perm || !Object.keys(tokenPayload.perm).includes(method)) { + const isValidPayload = validateTokenPayload(tokenPayload, request.method); + let isUnexpired; + + if (isValidPayload) { + isUnexpired = isPermissionUnexpired( + tokenPayload.iat[method], + tokenPayload.exp[method] + ); + } + + if (!isValidPayload || !isUnexpired) { const error = { name: "AuthorizationError", - code: grpc.status.PERMISSION_DENIED, - message: `Request of type ${method} is not allowed by the token policy.`, + code: isUnexpired + ? grpc.status.PERMISSION_DENIED + : grpc.status.UNAUTHENTICATED, + message: isUnexpired + ? `Request of type ${method} is not allowed by the token policy.` + : `Request of type ${method}, permission has expired.`, } as any; callback(error, null); @@ -221,6 +227,56 @@ export const isRequestAllowed = ( return true; }; +/** + * @description Validates the structure and types of the token payload. + * @returns true if token payload is valid, false otherwise + */ +const validateTokenPayload = ( + tokenPayload: TokenPayload | undefined, + method: string +): tokenPayload is TokenPayload => { + if (!tokenPayload) { + return false; + } + + if ( + typeof tokenPayload.iat !== "object" || + typeof tokenPayload.exp !== "object" || + typeof tokenPayload.sub !== "string" || + typeof tokenPayload.aud !== "string" || + typeof tokenPayload.iss !== "string" || + typeof tokenPayload.jti !== "string" || + Object.keys(tokenPayload.iat).length === 0 || + Object.keys(tokenPayload.exp).length === 0 || + !tokenPayload.iat.hasOwnProperty(method) || + !tokenPayload.exp.hasOwnProperty(method) + ) { + return false; + } + + return true; +}; + +/** + * @description Checks if the permissions granted in the token have expired. + * @param iat issued-at timestamp for the specific method + * @param exp expiration timestamp for the specific method + * @returns true if permission is still valid, false if expired + */ +export const isPermissionUnexpired = (iat: number, exp: number): Boolean => { + const nowInSeconds = Math.floor(Date.now() / 1000); + + if (nowInSeconds >= exp) { + return false; + } + + if (iat > nowInSeconds) { + return false; + } + + return true; +}; + /** * @description Checks if the serial number from the peer certificate matches the one in the token payload. * @param tokenPayload payload extracted from the token @@ -234,7 +290,7 @@ export const isSerialNumberMatching = ( callback: grpc.sendUnaryData ): Boolean => { const clientSN = normalizeSerial(peerCert?.serialNumber); - const tokenSN = normalizeSerial(tokenPayload?.subSerialNumber); + const tokenSN = normalizeSerial(tokenPayload?.sub); if (!clientSN || clientSN !== tokenSN) { const error = { diff --git a/Tokenization/backend/wrapper/src/models/connection.model.ts b/Tokenization/backend/wrapper/src/models/connection.model.ts index dc9c97c61..165dee5b6 100644 --- a/Tokenization/backend/wrapper/src/models/connection.model.ts +++ b/Tokenization/backend/wrapper/src/models/connection.model.ts @@ -65,12 +65,20 @@ export type HttpLikeResponse = { body: Buffer; }; -// Payload structure for authentication tokens +/** + * @description Payload structure for authentication tokens + * @sub {string} sub - Subject: Client's certificate serial number + * @aud {string} aud - Audience: Listener's certificate serial number + * @iss {string} iss - Issuer: Central system's certificate serial number + * @iat {Object} iat - Issued At: Permissions granted to the client (e.g., allowed HTTP methods with timestamps) + * @exp {number} exp - Expiration: Expiry timestamps for the granted permissions + * @jti {string} jti - JWT ID: Unique identifier for the token + */ export type TokenPayload = { - subSerialNumber: string; + sub: string; aud: string; - perm: Object; - iat: number; - exp: number; + iss: string; + iat: { [method: string]: number }; + exp: { [method: string]: number }; jti: string; }; diff --git a/Tokenization/backend/wrapper/src/test/client/ConnectionManager/Interceptors/grpc.auth.interceptor.test.ts b/Tokenization/backend/wrapper/src/test/client/ConnectionManager/Interceptors/grpc.auth.interceptor.test.ts index 791e9a4a8..ac9a170ec 100644 --- a/Tokenization/backend/wrapper/src/test/client/ConnectionManager/Interceptors/grpc.auth.interceptor.test.ts +++ b/Tokenization/backend/wrapper/src/test/client/ConnectionManager/Interceptors/grpc.auth.interceptor.test.ts @@ -317,30 +317,6 @@ describe("gRPCAuthInterceptor", () => { ); }); - it("should fail if JWS is expired", async () => { - (jose.compactVerify as jest.Mock).mockRejectedValue({ - message: "JWT is expired", - }); - - const result = await interceptor.gRPCAuthInterceptor( - mockCall, - mockCallback, - mockClientConnections as any, - mockSecurityContext - ); - - const created = getCreatedConn(); - expect(result.isAuthenticated).toBe(false); - expect(created!.handleFailedAuth).not.toHaveBeenCalled(); // for expired token, we do not block the connection - expect(mockCallback).toHaveBeenCalledWith( - expect.objectContaining({ - message: "JWS Verification error: Token expired", - code: grpc.status.UNAUTHENTICATED, - }), - null - ); - }); - it("should fail if mTLS serial number mismatch occurs after decryption", async () => { isSerialNumberMatchingSpy.mockImplementation((_p, _pc, cb) => { cb( diff --git a/Tokenization/backend/wrapper/src/utils/security/SecurityContext.ts b/Tokenization/backend/wrapper/src/utils/security/SecurityContext.ts index a60315036..973390045 100644 --- a/Tokenization/backend/wrapper/src/utils/security/SecurityContext.ts +++ b/Tokenization/backend/wrapper/src/utils/security/SecurityContext.ts @@ -25,15 +25,15 @@ export class SecurityContext { public readonly clientPrivateKey: Buffer; // Public Ed25519 key for JWS verification - public readonly JWS_PUBLIC_KEY = - "hTb3l5gwoIWISOLi6cQMwcultawKyA6vxnimXWtE6JI="; + public readonly JWS_PUBLIC_KEY: string; constructor( caCert: Buffer, clientSenderCert: Buffer, clientPrivateKey: Buffer, clientPublicKey: Buffer, - clientListenerCert?: Buffer + clientListenerCert?: Buffer, + JWS_PUBLIC_KEY?: string ) { this.caCert = caCert; this.clientSenderCert = clientSenderCert; @@ -43,5 +43,11 @@ export class SecurityContext { if (clientListenerCert) { this.clientListenerCert = clientListenerCert; } + + if (JWS_PUBLIC_KEY) { + this.JWS_PUBLIC_KEY = JWS_PUBLIC_KEY; + } else { + this.JWS_PUBLIC_KEY = "hTb3l5gwoIWISOLi6cQMwcultawKyA6vxnimXWtE6JI="; + } } } From 8104655956438e67c91ad8f37c0ee09401604e66 Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Wed, 1 Oct 2025 22:13:46 +0200 Subject: [PATCH 71/81] feat: implement broadcasting on central system --- .../wrapper/src/central/CentralSystemWrapper.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Tokenization/backend/wrapper/src/central/CentralSystemWrapper.ts b/Tokenization/backend/wrapper/src/central/CentralSystemWrapper.ts index a7a8b5eb4..31f839200 100644 --- a/Tokenization/backend/wrapper/src/central/CentralSystemWrapper.ts +++ b/Tokenization/backend/wrapper/src/central/CentralSystemWrapper.ts @@ -175,6 +175,17 @@ export class CentralSystemWrapper { } } + public broadcastEvent(data: DuplexMessageModel): void { + this.clients.forEach((client, ip) => { + try { + client.write(data); + this.logger.infoMessage(`Broadcasted event to ${ip}:`, data); + } catch (err) { + this.logger.errorMessage(`Error broadcasting to ${ip}:`, err); + } + }); + } + /** * @description Gets all connected client IPs * @returns Array of connected client IPs From 20e3a72dace7aecec5b18b2943b65177c978de97 Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Thu, 2 Oct 2025 18:16:16 +0200 Subject: [PATCH 72/81] feat: add token commands to central system --- .../src/central/CentralSystemWrapper.ts | 11 ++++++- .../getAllTokens/getAllTokens.command.ts | 24 ++++++++++++++ .../Commands/renewToken/renewToken.command.ts | 27 ++++++++++++++++ .../Commands/newToken/newToken.command.ts | 4 +-- .../Commands/newToken/newToken.handler.ts | 3 +- .../revokeToken/revokeToken.command.ts | 4 +-- .../revokeToken/revokeToken.handler.ts | 5 +-- .../ConnectionManager/CentralConnection.ts | 28 +++++++++++++++- .../wrapper/src/models/commands.model.ts | 2 +- .../wrapper/src/models/config.model.ts | 8 +++++ .../wrapper/src/models/message.model.ts | 20 +++++++++++- .../backend/wrapper/src/proto/wrapper.proto | 32 +++++++++++++------ 12 files changed, 148 insertions(+), 20 deletions(-) create mode 100644 Tokenization/backend/wrapper/src/central/Commands/getAllTokens/getAllTokens.command.ts create mode 100644 Tokenization/backend/wrapper/src/central/Commands/renewToken/renewToken.command.ts diff --git a/Tokenization/backend/wrapper/src/central/CentralSystemWrapper.ts b/Tokenization/backend/wrapper/src/central/CentralSystemWrapper.ts index 31f839200..c40f7f78c 100644 --- a/Tokenization/backend/wrapper/src/central/CentralSystemWrapper.ts +++ b/Tokenization/backend/wrapper/src/central/CentralSystemWrapper.ts @@ -18,6 +18,7 @@ import { LogManager } from "@aliceo2/web-ui"; import { DuplexMessageModel } from "../models/message.model"; import * as fs from "fs"; import { CentralSystemConfig } from "models/config.model"; +import { CentralCommandDispatcher } from "client/ConnectionManager/EventManagement/CentralCommandDispatcher"; /** * @description Central System gRPC wrapper that manages client connections and handles gRPC streams with them. @@ -25,6 +26,7 @@ import { CentralSystemConfig } from "models/config.model"; export class CentralSystemWrapper { // utilities private logger = LogManager.getLogger("CentralSystemWrapper"); + private dispatcher = new CentralCommandDispatcher(); // class properties private server: grpc.Server; @@ -57,6 +59,13 @@ export class CentralSystemWrapper { this.serverCerts = config.serverCerts; this.port = config.port || 50051; + // Register command handlers if provided + if (config.commandHandlers) { + config.commandHandlers.forEach(({ command, handler }) => { + this.dispatcher.register(command, handler); + }); + } + this.server = new grpc.Server(); this.setupService(); } @@ -158,7 +167,7 @@ export class CentralSystemWrapper { * @param data Data to send * @returns Whether the data was successfully sent */ - public sendEvent(ip: string, data: DuplexMessageModel): boolean { + public sendEvent(ip: string, data: DuplexMessageModel): Boolean { const client = this.clients.get(ip); if (!client) { this.logger.warnMessage(`Client ${ip} not found for sending event`); diff --git a/Tokenization/backend/wrapper/src/central/Commands/getAllTokens/getAllTokens.command.ts b/Tokenization/backend/wrapper/src/central/Commands/getAllTokens/getAllTokens.command.ts new file mode 100644 index 000000000..3c3bbd3dc --- /dev/null +++ b/Tokenization/backend/wrapper/src/central/Commands/getAllTokens/getAllTokens.command.ts @@ -0,0 +1,24 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +import { Command } from "../../../models/commands.model"; +import { DuplexMessageEvent } from "../../../models/message.model"; + +/** + * @description Command used to retrieve all tokens for a client. Handles structure logic. + */ +export class GetAllTokensCommand implements Command { + readonly event = DuplexMessageEvent.MESSAGE_EVENT_NEW_TOKEN; + constructor() {} +} diff --git a/Tokenization/backend/wrapper/src/central/Commands/renewToken/renewToken.command.ts b/Tokenization/backend/wrapper/src/central/Commands/renewToken/renewToken.command.ts new file mode 100644 index 000000000..3faff0b9b --- /dev/null +++ b/Tokenization/backend/wrapper/src/central/Commands/renewToken/renewToken.command.ts @@ -0,0 +1,27 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +import { Command } from "../../../models/commands.model"; +import { + DuplexMessageEvent, + SingleTokenPayload, +} from "../../../models/message.model"; + +/** + * @description Command used to renew token for a client after its expiration. Handles structure logic. + */ +export class RenewTokenCommand implements Command { + readonly event = DuplexMessageEvent.MESSAGE_EVENT_NEW_TOKEN; + constructor(payload: SingleTokenPayload) {} +} diff --git a/Tokenization/backend/wrapper/src/client/Commands/newToken/newToken.command.ts b/Tokenization/backend/wrapper/src/client/Commands/newToken/newToken.command.ts index df8889d0d..cd8c36482 100644 --- a/Tokenization/backend/wrapper/src/client/Commands/newToken/newToken.command.ts +++ b/Tokenization/backend/wrapper/src/client/Commands/newToken/newToken.command.ts @@ -15,7 +15,7 @@ import { Command } from "../../../models/commands.model"; import { DuplexMessageEvent, - TokenMessage, + SingleTokenPayload, } from "../../../models/message.model"; /** @@ -23,5 +23,5 @@ import { */ export class NewTokenCommand implements Command { readonly event = DuplexMessageEvent.MESSAGE_EVENT_NEW_TOKEN; - constructor(public payload: TokenMessage) {} + constructor(public payload: SingleTokenPayload) {} } diff --git a/Tokenization/backend/wrapper/src/client/Commands/newToken/newToken.handler.ts b/Tokenization/backend/wrapper/src/client/Commands/newToken/newToken.handler.ts index feade4424..84bb09abe 100644 --- a/Tokenization/backend/wrapper/src/client/Commands/newToken/newToken.handler.ts +++ b/Tokenization/backend/wrapper/src/client/Commands/newToken/newToken.handler.ts @@ -34,7 +34,8 @@ export class NewTokenHandler implements CommandHandler { * @throws Will throw an error if any of the required payload fields are missing. */ async handle(command: NewTokenCommand): Promise { - const { targetAddress, connectionDirection, token } = command.payload || {}; + const { targetAddress, connectionDirection, token } = + command.payload.singleToken || {}; if (!targetAddress || !token || !connectionDirection) { throw new Error( "Insufficient arguments. Expected: targetAddress, connectionDirection, token." diff --git a/Tokenization/backend/wrapper/src/client/Commands/revokeToken/revokeToken.command.ts b/Tokenization/backend/wrapper/src/client/Commands/revokeToken/revokeToken.command.ts index 656bd6496..7a5205565 100644 --- a/Tokenization/backend/wrapper/src/client/Commands/revokeToken/revokeToken.command.ts +++ b/Tokenization/backend/wrapper/src/client/Commands/revokeToken/revokeToken.command.ts @@ -15,7 +15,7 @@ import { Command } from "../../../models/commands.model"; import { DuplexMessageEvent, - TokenMessage, + SingleTokenPayload, } from "../../../models/message.model"; /** @@ -23,5 +23,5 @@ import { */ export class RevokeTokenCommand implements Command { readonly event = DuplexMessageEvent.MESSAGE_EVENT_REVOKE_TOKEN; - constructor(public payload: TokenMessage) {} + constructor(public payload: SingleTokenPayload) {} } diff --git a/Tokenization/backend/wrapper/src/client/Commands/revokeToken/revokeToken.handler.ts b/Tokenization/backend/wrapper/src/client/Commands/revokeToken/revokeToken.handler.ts index af923fc36..075d1d8c8 100644 --- a/Tokenization/backend/wrapper/src/client/Commands/revokeToken/revokeToken.handler.ts +++ b/Tokenization/backend/wrapper/src/client/Commands/revokeToken/revokeToken.handler.ts @@ -36,14 +36,15 @@ export class RevokeTokenHandler implements CommandHandler { * @throws Will throw an error if the target address is missing in the command payload. */ async handle(command: RevokeTokenCommand): Promise { - const { targetAddress } = command.payload || {}; + const { targetAddress, connectionDirection } = + command.payload.singleToken || {}; if (!targetAddress) { throw new Error("Target address is required to revoke token."); } const conn = this.manager.getConnectionByAddress( targetAddress, - command.payload.connectionDirection + connectionDirection ); conn?.handleRevokeToken(); diff --git a/Tokenization/backend/wrapper/src/client/ConnectionManager/CentralConnection.ts b/Tokenization/backend/wrapper/src/client/ConnectionManager/CentralConnection.ts index 3aefaf9fb..24cf71601 100644 --- a/Tokenization/backend/wrapper/src/client/ConnectionManager/CentralConnection.ts +++ b/Tokenization/backend/wrapper/src/client/ConnectionManager/CentralConnection.ts @@ -15,7 +15,10 @@ import * as grpc from "@grpc/grpc-js"; import { LogManager } from "@aliceo2/web-ui"; import { CentralCommandDispatcher } from "./EventManagement/CentralCommandDispatcher"; -import { DuplexMessageModel } from "../../models/message.model"; +import { + DuplexMessageEvent, + DuplexMessageModel, +} from "../../models/message.model"; import { ReconnectionScheduler } from "../../utils/reconnectionScheduler"; /** @@ -68,6 +71,29 @@ export class CentralConnection { }); } + /** + * + * @param data Message with event type and corresponding payload + * @returns + */ + public sendEvent(data: DuplexMessageModel) { + if (!this.stream) { + this.logger.warnMessage( + `Stream is not defined. Connect to Central System first.` + ); + return false; + } + + try { + this.stream.write(data); + this.logger.infoMessage(`Sent event to Central System:`, data); + return true; + } catch (err) { + this.logger.errorMessage(`Error sending to Central System:`, err); + return false; + } + } + /** * @description Starts the connection to the central system. */ diff --git a/Tokenization/backend/wrapper/src/models/commands.model.ts b/Tokenization/backend/wrapper/src/models/commands.model.ts index 1bba14108..f2a8fe4cd 100644 --- a/Tokenization/backend/wrapper/src/models/commands.model.ts +++ b/Tokenization/backend/wrapper/src/models/commands.model.ts @@ -19,7 +19,7 @@ import { DuplexMessageEvent } from "./message.model"; */ export interface Command { event: DuplexMessageEvent; - payload: any; + payload?: any; } /** diff --git a/Tokenization/backend/wrapper/src/models/config.model.ts b/Tokenization/backend/wrapper/src/models/config.model.ts index 3167065d1..df5256986 100644 --- a/Tokenization/backend/wrapper/src/models/config.model.ts +++ b/Tokenization/backend/wrapper/src/models/config.model.ts @@ -12,6 +12,9 @@ * or submit itself to any jurisdiction. */ +import { CommandHandler } from "./commands.model"; +import { DuplexMessageEvent } from "./message.model"; + export interface CentralSystemConfig { /** Path to the proto file defining the services. */ protoPath: string; @@ -26,6 +29,11 @@ export interface CentralSystemConfig { certPath: string; keyPath: string; }; + + commandHandlers?: { + command: DuplexMessageEvent; + handler: CommandHandler; + }[]; } export interface gRPCWrapperConfig { diff --git a/Tokenization/backend/wrapper/src/models/message.model.ts b/Tokenization/backend/wrapper/src/models/message.model.ts index fb64a4002..bc002cb8a 100644 --- a/Tokenization/backend/wrapper/src/models/message.model.ts +++ b/Tokenization/backend/wrapper/src/models/message.model.ts @@ -21,11 +21,18 @@ * @property MESSAGE_EVENT_EMPTY: No event, used for initialization or no response. * @property MESSAGE_EVENT_NEW_TOKEN: Event for replacing with newly generated token. * @property MESSAGE_EVENT_REVOKE_TOKEN: Event for revoking an existing token. + * @property MESSAGE_EVENT_GET_ALL_TOKENS: Event for getting all tokens for this client. + * @property MESSAGE_EVENT_RENEW_TOKEN: Event for renewing a token after expiration. */ export enum DuplexMessageEvent { + // Central system commands MESSAGE_EVENT_EMPTY = "MESSAGE_EVENT_EMPTY", MESSAGE_EVENT_NEW_TOKEN = "MESSAGE_EVENT_NEW_TOKEN", MESSAGE_EVENT_REVOKE_TOKEN = "MESSAGE_EVENT_REVOKE_TOKEN", + + // Client commands + MESSAGE_EVENT_GET_ALL_TOKENS = "MESSAGE_EVENT_GET_LAST_TOKEN", + MESSAGE_EVENT_RENEW_TOKEN = "MESSAGE_EVENT_RENEW_TOKEN", } /** @@ -47,6 +54,7 @@ export enum ConnectionDirection { /** * @description Model for token generation and revocation messages. * @property {string} token - The token to be replaced or revoked. + * @property {ConnectionDirection} connectionDirection - Direction of a connection token * @property {string} targetAddress - The address of connection binded to this token. */ export interface TokenMessage { @@ -55,6 +63,16 @@ export interface TokenMessage { targetAddress: string; } +export interface TokenListPayload { + tokensList: TokenMessage[]; +} + +export interface SingleTokenPayload { + singleToken: TokenMessage; +} + +export type TokenPayloadVariant = TokenListPayload | SingleTokenPayload; + /** * @description Model for duplex stream messages between client and central system. * @property {DuplexMessageEvent} event - The event type of the message. @@ -69,5 +87,5 @@ export interface TokenMessage { */ export interface DuplexMessageModel { event: DuplexMessageEvent; - payload: TokenMessage; + payload?: TokenPayloadVariant; } diff --git a/Tokenization/backend/wrapper/src/proto/wrapper.proto b/Tokenization/backend/wrapper/src/proto/wrapper.proto index abe296b59..0d3f2733a 100644 --- a/Tokenization/backend/wrapper/src/proto/wrapper.proto +++ b/Tokenization/backend/wrapper/src/proto/wrapper.proto @@ -45,6 +45,11 @@ message Token { ConnectionDirection connectionDirection = 3; } +// List of multiple tokens +message TokenList { + repeated Token tokens = 1; +} + // Stream message that can contain one of specific messages message Payload { // Message event type @@ -53,7 +58,8 @@ message Payload { // Data related to specific event type oneof data { EmptyMessage emptyMessage = 2; - Token payload = 3; + Token singleToken = 3; + TokenList tokensList = 4; } } @@ -88,14 +94,22 @@ message HttpLikeResponse { // ====================================== enum MessageEvent { - // Default value, represents an empty event - MESSAGE_EVENT_EMPTY = 0; - - // New token message type, contains a new token and target address - MESSAGE_EVENT_NEW_TOKEN = 1; - - // Revoke token message type, contains a token to be revoked - MESSAGE_EVENT_REVOKE_TOKEN = 2; + // Central system commands + // Default value, represents an empty event + MESSAGE_EVENT_EMPTY = 0; + + // New token message type, contains a new token and target address + MESSAGE_EVENT_NEW_TOKEN = 1; + + // Revoke token message type, contains a token to be revoked + MESSAGE_EVENT_REVOKE_TOKEN = 2; + + // Client commands + // Get all tokens assigned to this client + MESSAGE_EVENT_GET_ALL_TOKENS = 3; + + // Renew a token after expiration + MESSAGE_EVENT_RENEW_TOKEN = 4; } enum ConnectionDirection { From 706107a0d75579765e424154c438ae5b67c0c52a Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Fri, 10 Oct 2025 17:48:42 +0200 Subject: [PATCH 73/81] feat: create renewing and getting all tokens. Implement custom queue logic for token renewal retries --- .../revokeToken/revokeToken.handler.ts | 8 +- .../sendAllTokens/sendAllTokens.command.ts | 27 +++ .../sendAllTokens/sendAllTokens.handler.ts | 47 ++++ .../src/client/Connection/Connection.ts | 225 ++++++++++++++++-- .../ConnectionManager/CentralConnection.ts | 5 +- .../ConnectionManager/ConnectionManager.ts | 67 +++++- .../Interceptors/grpc.auth.interceptor.ts | 160 ++++++++----- .../backend/wrapper/src/client/gRPCWrapper.ts | 6 + .../wrapper/src/models/message.model.ts | 37 ++- .../src/test/utils/queues/RetryQueue.test.ts | 102 ++++++++ .../wrapper/src/utils/custom.identifier.ts | 28 +++ .../wrapper/src/utils/queues/RetryQueue.ts | 131 ++++++++++ 12 files changed, 748 insertions(+), 95 deletions(-) create mode 100644 Tokenization/backend/wrapper/src/client/Commands/sendAllTokens/sendAllTokens.command.ts create mode 100644 Tokenization/backend/wrapper/src/client/Commands/sendAllTokens/sendAllTokens.handler.ts create mode 100644 Tokenization/backend/wrapper/src/test/utils/queues/RetryQueue.test.ts create mode 100644 Tokenization/backend/wrapper/src/utils/custom.identifier.ts create mode 100644 Tokenization/backend/wrapper/src/utils/queues/RetryQueue.ts diff --git a/Tokenization/backend/wrapper/src/client/Commands/revokeToken/revokeToken.handler.ts b/Tokenization/backend/wrapper/src/client/Commands/revokeToken/revokeToken.handler.ts index 075d1d8c8..7ce080649 100644 --- a/Tokenization/backend/wrapper/src/client/Commands/revokeToken/revokeToken.handler.ts +++ b/Tokenization/backend/wrapper/src/client/Commands/revokeToken/revokeToken.handler.ts @@ -33,13 +33,15 @@ export class RevokeTokenHandler implements CommandHandler { * Handles the RevokeTokenCommand by revoking the token on the target connection. * * @param command - The RevokeTokenCommand containing the target address and direction. - * @throws Will throw an error if the target address is missing in the command payload. + * @throws Will throw an error if the target address or direction is missing in the command payload. */ async handle(command: RevokeTokenCommand): Promise { const { targetAddress, connectionDirection } = command.payload.singleToken || {}; - if (!targetAddress) { - throw new Error("Target address is required to revoke token."); + if (!targetAddress || !connectionDirection) { + throw new Error( + "Target address and connection direction are required to revoke token." + ); } const conn = this.manager.getConnectionByAddress( diff --git a/Tokenization/backend/wrapper/src/client/Commands/sendAllTokens/sendAllTokens.command.ts b/Tokenization/backend/wrapper/src/client/Commands/sendAllTokens/sendAllTokens.command.ts new file mode 100644 index 000000000..4f362bde9 --- /dev/null +++ b/Tokenization/backend/wrapper/src/client/Commands/sendAllTokens/sendAllTokens.command.ts @@ -0,0 +1,27 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +import { Command } from "../../../models/commands.model"; +import { + DuplexMessageEvent, + TokenListPayload, +} from "../../../models/message.model"; + +/** + * @description Command used to handle all tokens from a central system. Handles structure logic. + */ +export class SendAllTokensCommand implements Command { + readonly event = DuplexMessageEvent.MESSAGE_EVENT_SEND_ALL_TOKENS; + constructor(public payload: TokenListPayload) {} +} diff --git a/Tokenization/backend/wrapper/src/client/Commands/sendAllTokens/sendAllTokens.handler.ts b/Tokenization/backend/wrapper/src/client/Commands/sendAllTokens/sendAllTokens.handler.ts new file mode 100644 index 000000000..0e1165bf7 --- /dev/null +++ b/Tokenization/backend/wrapper/src/client/Commands/sendAllTokens/sendAllTokens.handler.ts @@ -0,0 +1,47 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +import { CommandHandler } from "../../../models/commands.model"; +import { SendAllTokensCommand } from "./sendAllTokens.command"; +import { ConnectionManager } from "../../ConnectionManager/ConnectionManager"; +import { ConnectionDirection } from "../../../models/message.model"; + +export class SendAllTokensHandler + implements CommandHandler +{ + /** + * Creates a new instance of RevokeTokenHandler. + * + * @param manager - The ConnectionManager used to retrieve active connections. + */ + constructor(private manager: ConnectionManager) {} + + /** + * Handles the RevokeTokenCommand by revoking the token on the target connection. + * + * @param command - The RevokeTokenCommand containing the target address and direction. + * @throws Will throw an error if the target address or direction is missing in the command payload. + */ + async handle(command: SendAllTokensCommand): Promise { + const { tokensList } = command.payload; + + for (const token of tokensList) { + this.manager.createNewConnection( + token.targetAddress, + token.connectionDirection || ConnectionDirection.SENDING, + token.token || "" + ); + } + } +} diff --git a/Tokenization/backend/wrapper/src/client/Connection/Connection.ts b/Tokenization/backend/wrapper/src/client/Connection/Connection.ts index b81e0a813..49aa4506e 100644 --- a/Tokenization/backend/wrapper/src/client/Connection/Connection.ts +++ b/Tokenization/backend/wrapper/src/client/Connection/Connection.ts @@ -12,7 +12,11 @@ * or submit itself to any jurisdiction. */ -import { ConnectionDirection } from "../../models/message.model"; +import { + ConnectionDirection, + TOKEN_REASON_HEADER, + TokenAuthReason, +} from "../../models/message.model"; import { ConnectionHeaders, ConnectionStatus, @@ -22,6 +26,8 @@ import { } from "../../models/connection.model"; import * as grpc from "@grpc/grpc-js"; import { LogManager } from "@aliceo2/web-ui"; +import { RetryQueue, RetryTask } from "../../utils/queues/RetryQueue"; +import { genId } from "../../utils/custom.identifier"; type ConnectionCerts = { caCert: Buffer; @@ -48,6 +54,24 @@ export class Connection { // utils private logger; + private retryQueue = new RetryQueue({ + maxRetries: 5, + baseDelayMs: 300, + maxDelayMs: 8000, + jitter: true, + }); + private pendingTokenRefresh?: Promise; + private isRefreshing = false; + + // for debug purposes + private failedRequestsLog: Array<{ + id: string; + method: string; + path: string; + reason: string; + at: number; + tryNo: number; + }> = []; /** * @description Creates a new Connection instance with the given token, target address, and connection direction. @@ -61,6 +85,7 @@ export class Connection { jweToken: string, targetAddress: string, direction: ConnectionDirection, + private renewToken: (token: string, targetAddress: string) => void, clientSN?: string ) { this.jweToken = jweToken; @@ -119,6 +144,22 @@ export class Connection { */ public handleNewToken(jweToken: string): void { this.jweToken = jweToken; + + // reset + this.authFailures = 0; + this.updateStatus(ConnectionStatus.CONNECTED); + + // drain retry queue on all after hanling new token + this.retryQueue.drainNow(); + + // end of refreshing + this.isRefreshing = false; + const p = this.pendingTokenRefresh; + this.pendingTokenRefresh = undefined; + // if someone awaited pendingTokenRefresh then we resolve it: + if (p) { + (p as any)._resolve?.(); // createTokenRefreshPromise + } } /** @@ -229,57 +270,193 @@ export class Connection { this.peerClient = client; } + // ----------------------------------------------------------------------------------------------------------------------------- + // FETCH HANDLING SECTION + // ----------------------------------------------------------------------------------------------------------------------------- + /** + * @description Waits for the token to be refreshed + * @returns Promise + */ + private async awaitTokenRefresh(): Promise { + if (!this.pendingTokenRefresh) return; + return this.pendingTokenRefresh; + } + + /** + * @description Creates a promise that resolves when a new token is refreshed + * It is used internally to handle the token refresh process. + * If the token is currently being refreshed, it returns the existing promise. + * If not, it creates a new promise and stores it in the connection object. + * When the new token is received, it resolves the promise. + * @returns A promise that resolves when the token is refreshed. + */ + private createTokenRefreshPromise(): Promise { + if (this.pendingTokenRefresh) return this.pendingTokenRefresh; + + let _resolve!: () => void; + let _reject!: (e: any) => void; + const newPromise = new Promise((resolve, reject) => { + _resolve = resolve; + _reject = reject; + }) as any; + // add reference to be resolved by handleNewToken + newPromise._resolve = _resolve; + newPromise._reject = _reject; + this.pendingTokenRefresh = newPromise; + return newPromise; + } + + /** + * @description Triggers token renewal if not already in progress. + * Updates connection status to TOKEN_REFRESH and creates a new promise to be resolved when the new token is received. + * Logs a warning message with the reason for the token renewal. + * @param reason The reason for the token renewal. + */ + private triggerTokenRenewIfNeeded(reason: TokenAuthReason) { + if (this.isRefreshing) return; + this.isRefreshing = true; + this.updateStatus(ConnectionStatus.TOKEN_REFRESH); + this.createTokenRefreshPromise(); // sets pendingTokenRefresh + this.logger.warnMessage( + `Trigger token renew due to: ${TokenAuthReason[reason]}` + ); + this.renewToken(this.jweToken, this.targetAddress); + } + + /** + * @description Performs a fetch-like request over a gRPC connection. + * Returns a promise that resolves with a FetchResponse object, containing the response status, headers, and body. + * The body can be accessed as a Buffer, or as a string or JSON object using the text() and json() methods respectively. + * @param req The request object to be sent over the gRPC connection. + * @param metadata The metadata object to be sent with the request. + * @returns A promise that resolves with a FetchResponse object. + */ + private grpcFetch(req: any, metadata: grpc.Metadata): Promise { + return new Promise((resolve, reject) => { + this.peerClient!.Fetch(req, metadata, (err: any, resp: any) => { + if (err) return reject(err); + + const resBody = resp?.body ? Buffer.from(resp.body) : Buffer.alloc(0); + resolve({ + status: Number(resp?.status ?? 200), + headers: resp?.headers || {}, + body: resBody, + text: async () => resBody.toString("utf8"), + json: async () => JSON.parse(resBody.toString("utf8")), + }); + }); + }); + } + + /** + * Checks if the given TokenAuthReason is eligible for token renewal. + * @param reason The reason for the authentication failure, or undefined if the authentication succeeded. + * @returns True if the reason is eligible for token renewal, false otherwise. + */ + private isAuthRenewable(reason: TokenAuthReason | undefined): boolean { + return ( + reason === TokenAuthReason.NO_TOKEN || + reason === TokenAuthReason.PERMISSION_EXPIRED || + reason === TokenAuthReason.JWE_DECRYPT_FAIL || + reason === TokenAuthReason.JWS_INVALID || + reason === TokenAuthReason.SERIAL_MISMATCH + ); + } + /** * @description "HTTP-like" fetch via gRPC protocol * @returns Promise with peer's response */ - public fetch(options: FetchOptions = {}): Promise { + public async fetch(options: FetchOptions = {}): Promise { if (!this.peerClient) { - return Promise.reject( - new Error(`Peer client not attached for ${this.getTargetAddress()}`) + throw new Error( + `Peer client not attached for ${this.getTargetAddress()}` + ); + } + if (this.status === ConnectionStatus.BLOCKED) { + throw new Error( + "Connection is blocked. Contact your admin for further details." ); } - // build a request object const method = (options.method || "POST").toUpperCase(); const path = options.path || "/"; const headers: ConnectionHeaders = { ...(options.headers || {}) }; - // set mandatory grpc metadata const metadata = new grpc.Metadata(); metadata.set("jwetoken", this.jweToken); - // build body buffer let bodyBuf: Buffer = Buffer.alloc(0); const b = options.body; if (b != null) { if (Buffer.isBuffer(b)) bodyBuf = b; else if (b instanceof Uint8Array) bodyBuf = Buffer.from(b); else if (typeof b === "string") bodyBuf = Buffer.from(b, "utf8"); - else - return Promise.reject( - new Error("Body must be a string/Buffer/Uint8Array") - ); + else throw new Error("Body must be a string/Buffer/Uint8Array"); } const req = { method, path, headers, body: bodyBuf }; - // return promise with response - return new Promise((resolve, reject) => { - this.peerClient.Fetch(req, metadata, (err: any, resp: any) => { - if (err) return reject(err); + // If someone is already refreshing token then wait and retry + if (this.status === ConnectionStatus.TOKEN_REFRESH) { + await this.awaitTokenRefresh(); + // after refresh we have new token to setup in metadata + const meta = new grpc.Metadata(); + meta.set("jwetoken", this.jweToken); + return this.grpcFetch(req, meta); + } - const resBody = resp?.body ? Buffer.from(resp.body) : Buffer.alloc(0); - const fetchResponse: FetchResponse = { - status: Number(resp?.status ?? 200), - headers: resp?.headers || {}, - body: resBody, - text: async () => resBody.toString("utf8"), - json: async () => JSON.parse(resBody.toString("utf8")), + // First try of request fetching + try { + return await this.grpcFetch(req, metadata); + } catch (err: any) { + const reason: TokenAuthReason | undefined = + err?.metadataMap?.get?.(TOKEN_REASON_HEADER); + + if (!this.isAuthRenewable(reason)) { + // errors that are not eligible for token renewal (e.g. FORBIDDEN, BLOCKED, INTERNAL, etc.) + this.logger.errorMessage( + `Error fetching for ${this.targetAddress}: (${method}):`, + err + ); + throw err; + } + + // Queue and refresh section + const id = genId(); // id of the request + this.failedRequestsLog.push({ + id, + method, + path, + reason: TokenAuthReason[reason!], + at: Date.now(), + tryNo: 1, + }); + + // trigger renewal + this.triggerTokenRenewIfNeeded(reason!); + + // create retry fetch object that will resolve when the token is refreshed + return new Promise((resolve, reject) => { + const task: RetryTask = { + id, + tryNo: 1, + createdAt: Date.now(), + reason: TokenAuthReason[reason!], + // retry function – will be executed after a drainNow() or after a backoff + exec: async () => { + // if still refreshing token then wait + await this.awaitTokenRefresh(); + const meta = new grpc.Metadata(); + meta.set("jwetoken", this.jweToken); + return this.grpcFetch(req, meta); + }, + resolve, + reject, }; - resolve(fetchResponse); + this.retryQueue.enqueue(task); }); - }); + } } } diff --git a/Tokenization/backend/wrapper/src/client/ConnectionManager/CentralConnection.ts b/Tokenization/backend/wrapper/src/client/ConnectionManager/CentralConnection.ts index 24cf71601..9e1ee89b3 100644 --- a/Tokenization/backend/wrapper/src/client/ConnectionManager/CentralConnection.ts +++ b/Tokenization/backend/wrapper/src/client/ConnectionManager/CentralConnection.ts @@ -15,10 +15,7 @@ import * as grpc from "@grpc/grpc-js"; import { LogManager } from "@aliceo2/web-ui"; import { CentralCommandDispatcher } from "./EventManagement/CentralCommandDispatcher"; -import { - DuplexMessageEvent, - DuplexMessageModel, -} from "../../models/message.model"; +import { DuplexMessageModel } from "../../models/message.model"; import { ReconnectionScheduler } from "../../utils/reconnectionScheduler"; /** diff --git a/Tokenization/backend/wrapper/src/client/ConnectionManager/ConnectionManager.ts b/Tokenization/backend/wrapper/src/client/ConnectionManager/ConnectionManager.ts index 3fe1d46aa..b15322cbd 100644 --- a/Tokenization/backend/wrapper/src/client/ConnectionManager/ConnectionManager.ts +++ b/Tokenization/backend/wrapper/src/client/ConnectionManager/ConnectionManager.ts @@ -18,13 +18,12 @@ import { CentralConnection } from "./CentralConnection"; import { CentralCommandDispatcher } from "./EventManagement/CentralCommandDispatcher"; import { Connection } from "../Connection/Connection"; import { LogManager } from "@aliceo2/web-ui"; -import { Command, CommandHandler } from "models/commands.model"; +import { Command, CommandHandler } from "../../models/commands.model"; import { ConnectionDirection, DuplexMessageEvent, } from "../../models/message.model"; import { ConnectionStatus } from "../../models/connection.model"; -import * as fs from "fs"; import { gRPCAuthInterceptor } from "./Interceptors/grpc.auth.interceptor"; import { SecurityContext } from "../../utils/security/SecurityContext"; @@ -51,9 +50,10 @@ export class ConnectionManager { private centralDispatcher: CentralCommandDispatcher; private centralConnection: CentralConnection; - private sendingConnections = new Map(); + private sendingConnections = new Map(); private receivingConnections = new Map(); + private peerCtor: any; // p2p gRPC constructor private peerServer?: grpc.Server; private baseAPIPath: string = "localhost:40041/api/"; @@ -160,7 +160,12 @@ export class ConnectionManager { } // Create new connection - conn = new Connection(jweToken || "", address, direction); + conn = new Connection( + jweToken || "", + address, + direction, + this.renewToken.bind(this) + ); conn.updateStatus(ConnectionStatus.CONNECTING); if (direction === ConnectionDirection.RECEIVING) { @@ -201,6 +206,25 @@ export class ConnectionManager { } } + /** + * @description Retrieves a connection instance by its token. + * @param {string} token - The token to search for. + * @returns {Connection | undefined} The connection instance with the matching token, or undefined if not found. + */ + getConnectionByToken(token: string): Connection | undefined { + for (const conn of this.sendingConnections.values()) { + if (conn.getToken() === token) { + return conn; + } + } + for (const conn of this.receivingConnections.values()) { + if (conn.getToken() === token) { + return conn; + } + } + return undefined; + } + /** * @description Searches through all receiving and sending connections to find a connection by its client Serial Number (SN). * @param serialNumber The unique serial number of the peer's certificate. @@ -265,7 +289,7 @@ export class ConnectionManager { const { isAuthenticated, conn } = await gRPCAuthInterceptor( call, callback, - this.receivingConnections, + this, this.securityContext ); @@ -342,4 +366,37 @@ export class ConnectionManager { this.logger.infoMessage(`Peer server listening on localhost:${port}`); } + + /** + * @description Sends command to central system to get all tokens + * @returns Promise - central system will send asynchronously MESSAGE_EVENT_SEND_ALL_TOKENS with TokenListPayload interface + */ + public getAllTokens(): void { + this.centralConnection.sendEvent({ + event: DuplexMessageEvent.MESSAGE_EVENT_GET_ALL_TOKENS, + }); + } + + /** + * @description Sends command to central system to renew token for a specific target connection + * @param expiredToken token that needs to be renew + * @param targetAddress target address we want to send to + * @returns Promise - central system will send asynchronously MESSAGE_EVENT_NEW_TOKEN with SingleTokenPayload interface + */ + private renewToken(expiredToken: string, targetAddress: string): void { + const conn = this.sendingConnections.get(targetAddress); + if (!conn) { + return; + } + + this.centralConnection.sendEvent({ + event: DuplexMessageEvent.MESSAGE_EVENT_RENEW_TOKEN, + payload: { + singleToken: { + token: expiredToken, + targetAddress: targetAddress, + }, + }, + }); + } } diff --git a/Tokenization/backend/wrapper/src/client/ConnectionManager/Interceptors/grpc.auth.interceptor.ts b/Tokenization/backend/wrapper/src/client/ConnectionManager/Interceptors/grpc.auth.interceptor.ts index 5e959ed2b..293ac1753 100644 --- a/Tokenization/backend/wrapper/src/client/ConnectionManager/Interceptors/grpc.auth.interceptor.ts +++ b/Tokenization/backend/wrapper/src/client/ConnectionManager/Interceptors/grpc.auth.interceptor.ts @@ -19,8 +19,14 @@ import { ConnectionStatus, TokenPayload, } from "../../../models/connection.model"; -import { ConnectionDirection } from "../../../models/message.model"; +import { + ConnectionDirection, + TOKEN_REASON_HEADER, + TOKEN_TARGET_HEADER, + TokenAuthReason, +} from "../../../models/message.model"; import { SecurityContext } from "../../../utils/security/SecurityContext"; +import { ConnectionManager } from "../ConnectionManager"; /** * @description gRPC interceptor function responsible for JWE decryption, JWS verification, @@ -29,37 +35,42 @@ import { SecurityContext } from "../../../utils/security/SecurityContext"; export const gRPCAuthInterceptor = async ( call: grpc.ServerUnaryCall, callback: grpc.sendUnaryData, - clientConnections: Map, + connectionManager: ConnectionManager, securityContext: SecurityContext -): Promise<{ isAuthenticated: Boolean; conn: Connection | null }> => { +): Promise<{ isAuthenticated: Boolean; conn: Connection | undefined }> => { const metadata = call.metadata.getMap(); const jweToken = metadata.jwetoken as string; const clientAddress = call.getPeer(); - let conn = clientConnections.get(clientAddress); + let conn = connectionManager.getConnectionByAddress( + clientAddress, + ConnectionDirection.RECEIVING + ); const peerCert = getPeerCertFromCall(call); // Check if token exists if (!jweToken) { - const error = { - name: "AuthenticationError", - message: "No token provided", - code: grpc.status.UNAUTHENTICATED, - }; - callback(error, null); - return { isAuthenticated: false, conn: null }; + return createFailAuthResponse( + call, + callback, + conn, + grpc.status.UNAUTHENTICATED, + "No token provided", + TokenAuthReason.NO_TOKEN + ); } // Check if connection exists if (conn) { // Check if connection is blocked if (conn.getStatus() === ConnectionStatus.BLOCKED) { - const error = { - name: "AuthenticationError", - message: "Connection is blocked. Contact administrator.", - code: grpc.status.UNAUTHENTICATED, - }; - callback(error, null); - return { isAuthenticated: false, conn }; + return createFailAuthResponse( + call, + callback, + conn, + grpc.status.UNAUTHENTICATED, + "No token provided", + TokenAuthReason.NO_TOKEN + ); } if (conn.getToken() === jweToken) { @@ -67,7 +78,14 @@ export const gRPCAuthInterceptor = async ( if ( !isRequestAllowed(conn.getCachedTokenPayload(), call.request, callback) ) { - return { isAuthenticated: false, conn }; + return createFailAuthResponse( + call, + callback, + conn, + grpc.status.PERMISSION_DENIED, + "Method not allowed", + TokenAuthReason.PERMISSION_FORBIDDEN + ); } if ( @@ -78,18 +96,24 @@ export const gRPCAuthInterceptor = async ( ) ) { conn.handleFailedAuth(); - return { isAuthenticated: false, conn }; + return createFailAuthResponse( + call, + callback, + conn, + grpc.status.UNAUTHENTICATED, + "Serial number mismatch", + TokenAuthReason.SERIAL_MISMATCH + ); } return { isAuthenticated: true, conn }; } } else { - conn = new Connection( - jweToken, + conn = await connectionManager.createNewConnection( clientAddress, - ConnectionDirection.RECEIVING + ConnectionDirection.RECEIVING, + jweToken ); - clientConnections.set(clientAddress, conn); } // New connection - need to authenticate @@ -106,15 +130,14 @@ export const gRPCAuthInterceptor = async ( const { plaintext } = await compactDecrypt(jweToken, privateKey); jwsToken = new TextDecoder().decode(plaintext).trim(); } catch (_e) { - const error = { - name: "AuthenticationError", - message: "Incorrect token provided (JWE Decryption failed)", - code: grpc.status.UNAUTHENTICATED, - }; - // TODO: Consider logging or informing a central security system about potential attack/misconfiguration. - callback(error, null); - conn.handleFailedAuth(); - return { isAuthenticated: false, conn }; + return createFailAuthResponse( + call, + callback, + conn, + grpc.status.UNAUTHENTICATED, + "Incorrect token provided (JWE Decryption failed)", + TokenAuthReason.JWE_DECRYPT_FAIL + ); } // Verify JWS (With signature) and payload extraction @@ -142,42 +165,54 @@ export const gRPCAuthInterceptor = async ( // Additional check to ensure correct signing algorithm was used if (protectedHeader.alg !== "EdDSA" && protectedHeader.alg !== "Ed25519") { - const error = { - name: "AuthenticationError", - message: "Incorrect signing algorithm for JWS.", - code: grpc.status.UNAUTHENTICATED, - }; - - callback(error, null); - return { isAuthenticated: false, conn }; + return createFailAuthResponse( + call, + callback, + conn, + grpc.status.UNAUTHENTICATED, + "Incorrect signing algorithm for JWS.", + TokenAuthReason.JWS_INVALID + ); } // Decode and parse the JWT payload const payloadString = new TextDecoder().decode(jwtPayload); payload = JSON.parse(payloadString); } catch (e: any) { - const error = { - name: "AuthenticationError", - message: `JWS Verification error: Invalid signature`, - code: grpc.status.PERMISSION_DENIED, - }; - // TODO: Consider logging or informing a central security system about failed verification. - callback(error, null); - - conn.handleFailedAuth(); - return { isAuthenticated: false, conn }; + return createFailAuthResponse( + call, + callback, + conn, + grpc.status.UNAUTHENTICATED, + "JWS Verification error: Invalid signature", + TokenAuthReason.JWS_INVALID + ); } // mTLS binding check and authorization // Connection tunnel verification with serialNumber (mTLS SN vs Token SN) if (!isSerialNumberMatching(payload, peerCert, callback)) { conn.handleFailedAuth(); - return { isAuthenticated: false, conn }; + return createFailAuthResponse( + call, + callback, + conn, + grpc.status.UNAUTHENTICATED, + "Serial number mismatch", + TokenAuthReason.SERIAL_MISMATCH + ); } // Validate permission for request method (Authorization check) if (!isRequestAllowed(payload, call.request, callback)) { - return { isAuthenticated: false, conn }; + return createFailAuthResponse( + call, + callback, + conn, + grpc.status.PERMISSION_DENIED, + "Method not allowed", + TokenAuthReason.PERMISSION_FORBIDDEN + ); } // Authentication and Authorization successful @@ -324,3 +359,22 @@ export const getPeerCertFromCall = (call: any) => { const sock = session?.socket as any; return sock?.getPeerCertificate(true); // whole certificate info from TLS socket }; + +const createFailAuthResponse = ( + call: grpc.ServerUnaryCall, + callback: grpc.sendUnaryData, + conn: Connection | undefined, + code: grpc.status, + msg: string, + reason: TokenAuthReason +) => { + const md = new grpc.Metadata(); + md.set(TOKEN_REASON_HEADER, reason); + md.set(TOKEN_TARGET_HEADER, call.getPeer() || ""); + const err = Object.assign(new Error(msg), { + code, + metadata: md, + }); + callback(err, null); + return { isAuthenticated: false, conn }; +}; diff --git a/Tokenization/backend/wrapper/src/client/gRPCWrapper.ts b/Tokenization/backend/wrapper/src/client/gRPCWrapper.ts index 74e11b1d7..f295630e8 100644 --- a/Tokenization/backend/wrapper/src/client/gRPCWrapper.ts +++ b/Tokenization/backend/wrapper/src/client/gRPCWrapper.ts @@ -23,6 +23,7 @@ import { NewTokenHandler } from "./Commands/newToken/newToken.handler"; import { gRPCWrapperConfig } from "../models/config.model"; import { SecurityContext } from "../utils/security/SecurityContext"; import * as fs from "fs"; +import { SendAllTokensHandler } from "./Commands/sendAllTokens/sendAllTokens.handler"; /** * @description Wrapper class for managing secure gRPC wrapper. @@ -99,6 +100,10 @@ export class gRPCWrapper { event: DuplexMessageEvent.MESSAGE_EVENT_NEW_TOKEN, handler: new NewTokenHandler(this.ConnectionManager), }, + { + event: DuplexMessageEvent.MESSAGE_EVENT_SEND_ALL_TOKENS, + handler: new SendAllTokensHandler(this.ConnectionManager), + }, ]); } @@ -107,6 +112,7 @@ export class gRPCWrapper { */ public connectToCentralSystem() { this.ConnectionManager.connectToCentralSystem(); + this.ConnectionManager.getAllTokens(); } /** diff --git a/Tokenization/backend/wrapper/src/models/message.model.ts b/Tokenization/backend/wrapper/src/models/message.model.ts index bc002cb8a..b89b219ce 100644 --- a/Tokenization/backend/wrapper/src/models/message.model.ts +++ b/Tokenization/backend/wrapper/src/models/message.model.ts @@ -29,6 +29,7 @@ export enum DuplexMessageEvent { MESSAGE_EVENT_EMPTY = "MESSAGE_EVENT_EMPTY", MESSAGE_EVENT_NEW_TOKEN = "MESSAGE_EVENT_NEW_TOKEN", MESSAGE_EVENT_REVOKE_TOKEN = "MESSAGE_EVENT_REVOKE_TOKEN", + MESSAGE_EVENT_SEND_ALL_TOKENS = "MESSAGE_EVENT_SEND_ALL_TOKENS", // Client commands MESSAGE_EVENT_GET_ALL_TOKENS = "MESSAGE_EVENT_GET_LAST_TOKEN", @@ -47,6 +48,32 @@ export enum ConnectionDirection { DUPLEX = "DUPLEX", } +/** + * @remarks This enum is used to indicate specific causes when token-based authentication does not succeed. + * + * @enum + * @property NO_TOKEN - No authentication token was provided. + * @property CONNECTION_BLOCKED - The connection was blocked, possibly due to security policies. + * @property JWE_DECRYPT_FAIL - Failed to decrypt the JWE (JSON Web Encryption) token. + * @property JWS_INVALID - The JWS (JSON Web Signature) token is invalid. + * @property SERIAL_MISMATCH - The token's serial number does not match the expected value. + * @property PERMISSION_EXPIRED - The permissions associated with the token have expired. + * @property PERMISSION_FORBIDDEN - The token does not have the required permissions. + */ +export enum TokenAuthReason { + NO_TOKEN = "NO_TOKEN", + CONNECTION_BLOCKED = "CONNECTION_BLOCKED", + JWE_DECRYPT_FAIL = "JWE_DECRYPT_FAIL", + JWS_INVALID = "JWS_INVALID", + SERIAL_MISMATCH = "SERIAL_MISMATCH", + PERMISSION_EXPIRED = "PERMISSION_EXPIRED", + PERMISSION_FORBIDDEN = "PERMISSION_FORBIDDEN", +} + +// Header names for token messages +export const TOKEN_REASON_HEADER = "x-token-reason"; // TokenAuthReason from enum +export const TOKEN_TARGET_HEADER = "x-token-target"; // address/peer + // ====================================== // INTERFACES // ====================================== @@ -54,12 +81,12 @@ export enum ConnectionDirection { /** * @description Model for token generation and revocation messages. * @property {string} token - The token to be replaced or revoked. - * @property {ConnectionDirection} connectionDirection - Direction of a connection token + * @property {ConnectionDirection} connectionDirection - The direction of the connection associated with this token. * @property {string} targetAddress - The address of connection binded to this token. */ export interface TokenMessage { token?: string; - connectionDirection: ConnectionDirection; + connectionDirection?: ConnectionDirection; targetAddress: string; } @@ -76,13 +103,11 @@ export type TokenPayloadVariant = TokenListPayload | SingleTokenPayload; /** * @description Model for duplex stream messages between client and central system. * @property {DuplexMessageEvent} event - The event type of the message. - * @property {ConnectionDirection} connectionDirection - The direction of the connection, optional for some events. - * @property {TokenMessage} data - The data associated with the event, it may be undefined for some events. + * @property {TokenPayloadVariant} payload - The data associated with the event, it may be undefined for some events. * @example * { * event: DuplexMessageEvent.MESSAGE_EVENT_NEW_TOKEN, - * connectionDirection: ConnectionDirection.SENDING, - * payload: {token: 'abc', targetAddress: 'localhost:50051'} + * payload: { singleToken: {token: 'abc', targetAddress: 'localhost:50051'} } * } */ export interface DuplexMessageModel { diff --git a/Tokenization/backend/wrapper/src/test/utils/queues/RetryQueue.test.ts b/Tokenization/backend/wrapper/src/test/utils/queues/RetryQueue.test.ts new file mode 100644 index 000000000..606a1c01e --- /dev/null +++ b/Tokenization/backend/wrapper/src/test/utils/queues/RetryQueue.test.ts @@ -0,0 +1,102 @@ +import { RetryQueue, RetryTask } from "../../../utils/queues/RetryQueue"; + +describe("RetryQueue", () => { + let queue: RetryQueue; + + beforeEach(() => { + jest.useFakeTimers(); + queue = new RetryQueue(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it("should enqueue a task and increase size", () => { + const task = createTask(); + queue.enqueue(task); + expect(queue.size()).toBe(1); + }); + + it("should execute a task after delay and resolve", async () => { + const exec = jest.fn().mockResolvedValue("done"); + const resolve = jest.fn(); + const reject = jest.fn(); + const task = createTask({ exec, resolve, reject }); + + queue.enqueue(task); + jest.runOnlyPendingTimers(); + + // Wait for promise resolution + await Promise.resolve(); + expect(exec).toHaveBeenCalled(); + expect(resolve).toHaveBeenCalledWith("done"); + expect(reject).not.toHaveBeenCalled(); + }); + + it("should retry failed task up to maxRetries and then reject", async () => { + jest.useFakeTimers(); + + const exec = jest.fn().mockRejectedValue(new Error("fail")); + const resolve = jest.fn(); + const reject = jest.fn(); + + const task = createTask({ exec, resolve, reject, tryNo: 0 }); + + const queue = new RetryQueue({ + maxRetries: 2, + baseDelayMs: 1, + maxDelayMs: 10, + jitter: false, + }); + + queue.enqueue(task); + + await jest.runAllTimersAsync(); + + expect(exec).toHaveBeenCalledTimes(3); + expect(reject).toHaveBeenCalledTimes(1); + expect(resolve).not.toHaveBeenCalled(); + }); + + it("should call drainNow and execute all tasks immediately", async () => { + const exec = jest.fn().mockResolvedValue("drained"); + const resolve = jest.fn(); + const reject = jest.fn(); + const task = createTask({ exec, resolve, reject }); + + queue.enqueue(task); + queue.drainNow(); + + await Promise.resolve(); + expect(exec).toHaveBeenCalled(); + expect(resolve).toHaveBeenCalledWith("drained"); + expect(reject).not.toHaveBeenCalled(); + }); + + it("should set lastRunAt when task is executed", async () => { + const exec = jest.fn().mockResolvedValue("ok"); + const resolve = jest.fn(); + const reject = jest.fn(); + const task = createTask({ exec, resolve, reject }); + + queue.enqueue(task); + jest.runOnlyPendingTimers(); + await Promise.resolve(); + + expect(typeof task.lastRunAt).toBe("number"); + expect(task.lastRunAt).toBeLessThanOrEqual(Date.now()); + }); + + function createTask(overrides: Partial = {}): RetryTask { + return { + id: "id1", + tryNo: 0, + createdAt: Date.now(), + exec: jest.fn().mockResolvedValue(undefined), + resolve: jest.fn(), + reject: jest.fn(), + ...overrides, + }; + } +}); diff --git a/Tokenization/backend/wrapper/src/utils/custom.identifier.ts b/Tokenization/backend/wrapper/src/utils/custom.identifier.ts new file mode 100644 index 000000000..870869b79 --- /dev/null +++ b/Tokenization/backend/wrapper/src/utils/custom.identifier.ts @@ -0,0 +1,28 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +/** + * Generates a unique identifier string composed of the current timestamp + * and a random value. The timestamp is represented as a base-36 string + * and the random value is a base-36 string of four random 32-bit integers. + * The two values are separated by a hyphen. + * @returns A unique identifier string. + */ +export const genId = (): string => { + const time = Date.now().toString(36); + const rand = Array.from(crypto.getRandomValues(new Uint32Array(4))) + .map((x) => x.toString(36)) + .join(""); + return `${time}-${rand}`; +}; diff --git a/Tokenization/backend/wrapper/src/utils/queues/RetryQueue.ts b/Tokenization/backend/wrapper/src/utils/queues/RetryQueue.ts new file mode 100644 index 000000000..1c78acbb3 --- /dev/null +++ b/Tokenization/backend/wrapper/src/utils/queues/RetryQueue.ts @@ -0,0 +1,131 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +export type RetryTaskFn = () => Promise; + +export interface RetryTask { + id: string; + tryNo: number; + createdAt: number; + lastRunAt?: number; + reason?: string; + exec: RetryTaskFn; + resolve: (v: T) => void; + reject: (e: any) => void; +} + +export interface RetryQueueOptions { + maxRetries?: number; // default 5 + baseDelayMs?: number; // default 300 + maxDelayMs?: number; // default 8000 + jitter?: boolean; // default true +} + +export class RetryQueue { + private q: RetryTask[] = []; + private running = false; + private opts: Required; + + /** + * @description Creates a new RetryQueue instance. + * @param {RetryQueueOptions} [opts] - Optional configuration options + * @property {number} [opts.maxRetries] - Max number of retries (default 5) + * @property {number} [opts.baseDelayMs] - Base delay in ms (default 300) + * @property {number} [opts.maxDelayMs] - Max delay in ms (default 8000) + * @property {boolean} [opts.jitter] - Whether to use jitter in delay calculation (default true) + */ + constructor(opts?: RetryQueueOptions) { + this.opts = { + maxRetries: opts?.maxRetries ?? 5, + baseDelayMs: opts?.baseDelayMs ?? 300, + maxDelayMs: opts?.maxDelayMs ?? 8000, + jitter: opts?.jitter ?? true, + }; + } + + /** + * @description Returns the number of tasks currently in the retry queue. + * @returns The number of tasks in the retry queue. + */ + public size(): number { + return this.q.length; + } + + /** + * @description Adds a task to the retry queue. The task will be executed after a delay. + * @param {RetryTask} task - The task to be added to the retry queue. + * @template T - The type of the data returned by the task. + */ + public enqueue(task: RetryTask) { + this.q.push(task); + // we don't start the task – exec will be called after delay or drainNow + this.schedule(task); + } + + /** + * Drains the retry queue by executing all the tasks currently in the queue. + * If any task fails, it will be re-enqueued with an incremented tryNo. + * If the task has been retried more than maxRetries times, it will be rejected. + * This function is useful when we want to flush the retry queue, for example, when a new token is received. + */ + public drainNow() { + if (this.running) return; + this.running = true; + const tasks = [...this.q]; + this.q = []; + for (const t of tasks) { + t.exec() + .then(t.resolve) + .catch((e) => { + // if failed, re-enqueue with incremented tryNo + t.tryNo++; + if (t.tryNo > this.opts.maxRetries) { + t.reject(e); + return; + } + this.enqueue(t); + }); + } + this.running = false; + } + + /** + * Schedules a task to be executed after a delay. The delay is calculated using a + * exponential backoff strategy. If jitter is enabled, a random value between 0.5 and 1 + * is added to the calculated delay. If the task fails, it will be re-enqueued with an + * incremented tryNo. If the task has been retried more than maxRetries times, it will be + * rejected. + * @param {RetryTask} task - The task to be scheduled + */ + private schedule(task: RetryTask) { + const { baseDelayMs, maxDelayMs, jitter } = this.opts; + const exp = Math.min(maxDelayMs, baseDelayMs * Math.pow(2, task.tryNo)); + const delay = jitter ? Math.floor(exp * (0.5 + Math.random())) : exp; + + setTimeout(() => { + task.lastRunAt = Date.now(); + task + .exec() + .then(task.resolve) + .catch((e) => { + task.tryNo++; + if (task.tryNo > this.opts.maxRetries) { + task.reject(e); + return; + } + this.enqueue(task); + }); + }, delay); + } +} From 684c000523c9441d82df7f137b8fa9124138a642 Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Fri, 10 Oct 2025 18:32:02 +0200 Subject: [PATCH 74/81] fix: fix tests with new logic --- .../src/central/CentralSystemWrapper.ts | 2 +- .../src/test/central/CentralSystem.test.ts | 143 ------ .../src/test/client/Commands/newToken.test.ts | 48 +- .../test/client/Commands/revokeToken.test.ts | 18 +- .../grpc.auth.interceptor.test.ts | 432 ++++-------------- 5 files changed, 133 insertions(+), 510 deletions(-) delete mode 100644 Tokenization/backend/wrapper/src/test/central/CentralSystem.test.ts diff --git a/Tokenization/backend/wrapper/src/central/CentralSystemWrapper.ts b/Tokenization/backend/wrapper/src/central/CentralSystemWrapper.ts index c40f7f78c..85eb01a72 100644 --- a/Tokenization/backend/wrapper/src/central/CentralSystemWrapper.ts +++ b/Tokenization/backend/wrapper/src/central/CentralSystemWrapper.ts @@ -18,7 +18,7 @@ import { LogManager } from "@aliceo2/web-ui"; import { DuplexMessageModel } from "../models/message.model"; import * as fs from "fs"; import { CentralSystemConfig } from "models/config.model"; -import { CentralCommandDispatcher } from "client/ConnectionManager/EventManagement/CentralCommandDispatcher"; +import { CentralCommandDispatcher } from "../client/ConnectionManager/EventManagement/CentralCommandDispatcher"; /** * @description Central System gRPC wrapper that manages client connections and handles gRPC streams with them. diff --git a/Tokenization/backend/wrapper/src/test/central/CentralSystem.test.ts b/Tokenization/backend/wrapper/src/test/central/CentralSystem.test.ts deleted file mode 100644 index ef54852ae..000000000 --- a/Tokenization/backend/wrapper/src/test/central/CentralSystem.test.ts +++ /dev/null @@ -1,143 +0,0 @@ -/** - * @license - * Copyright 2019-2020 CERN and copyright holders of ALICE O2. - * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. - * All rights not expressly granted are reserved. - * - * This software is distributed under the terms of the GNU General Public - * License v3 (GPL Version 3), copied verbatim in the file "COPYING". - * - * In applying this license CERN does not waive the privileges and immunities - * granted to it by virtue of its status as an Intergovernmental Organization - * or submit itself to any jurisdiction. - */ - -const mockAddService = jest.fn(); -const mockBindAsync = jest.fn(); -const mockServerInstance = { - addService: mockAddService, - bindAsync: mockBindAsync, -}; - -const logger = { - infoMessage: jest.fn(), - errorMessage: jest.fn(), -}; - -jest.mock( - "@aliceo2/web-ui", - () => ({ - LogManager: { - getLogger: () => logger, - }, - }), - { virtual: true } -); - -jest.mock("@grpc/proto-loader", () => ({ - loadSync: jest.fn(() => { - return {}; - }), -})); - -jest.mock("@grpc/grpc-js", () => { - const original = jest.requireActual("@grpc/grpc-js"); - return { - ...original, - Server: jest.fn(() => mockServerInstance), - ServerCredentials: { - createSsl: jest.fn(() => "mock-credentials"), - }, - loadPackageDefinition: jest.fn(() => ({ - webui: { - tokenization: { - CentralSystem: { - service: "mock-service", - }, - }, - }, - })), - }; -}); - -import { getTestCentralCertPaths } from "../testCerts/testCerts"; -import { CentralSystemWrapper } from "../../central/CentralSystemWrapper"; -import * as grpc from "@grpc/grpc-js"; - -describe("CentralSystemWrapper", () => { - let wrapper: CentralSystemWrapper; - const testCentralCertPaths = getTestCentralCertPaths(); - - beforeEach(() => { - jest.clearAllMocks(); - wrapper = new CentralSystemWrapper({ - protoPath: "dummy.proto", - port: 12345, - serverCerts: testCentralCertPaths, - }); - }); - - test("should set up gRPC service and add it to the server", () => { - expect(grpc.Server).toHaveBeenCalled(); - expect(grpc.loadPackageDefinition).toHaveBeenCalled(); - expect(grpc.ServerCredentials.createSsl).not.toHaveBeenCalled(); - expect(wrapper).toBeDefined(); - }); - - test("should call listen and bind the server", () => { - mockBindAsync.mockImplementation((_addr, _creds, cb) => cb(null, 12345)); - - wrapper.listen(); - - expect(mockBindAsync).toHaveBeenCalledWith( - "localhost:12345", - "mock-credentials", - expect.any(Function) - ); - }); - - test("should log error if bind fails", () => { - const error = new Error("bind failed"); - mockBindAsync.mockImplementation((_addr, _creds, cb) => cb(error, null)); - - wrapper.listen(); - - expect(logger.errorMessage).toHaveBeenCalledWith( - "Server bind error:", - error - ); - }); - - test("should handle client stream events", () => { - const logger = require("@aliceo2/web-ui").LogManager.getLogger(); - - const mockCall = { - getPeer: jest.fn(() => "client123"), - on: jest.fn((event, cb) => { - if (event === "end") cb(); - if (event === "error") cb(new Error("stream error")); - }), - end: jest.fn(), - }; - - const handler = (wrapper as any).clientStreamHandler.bind(wrapper); - handler(mockCall); - - expect(mockCall.on).toHaveBeenCalledWith("data", expect.any(Function)); - expect(mockCall.on).toHaveBeenCalledWith("end", expect.any(Function)); - expect(mockCall.on).toHaveBeenCalledWith("error", expect.any(Function)); - - expect(mockCall.end).toHaveBeenCalled(); - expect(logger.infoMessage).toHaveBeenCalledWith( - expect.stringContaining("Client client123") - ); - - expect(logger.infoMessage).toHaveBeenCalledWith( - "Client client123 ended stream." - ); - expect(logger.errorMessage).toHaveBeenCalledWith( - "Stream error from client client123:", - expect.any(Error) - ); - }); -}); diff --git a/Tokenization/backend/wrapper/src/test/client/Commands/newToken.test.ts b/Tokenization/backend/wrapper/src/test/client/Commands/newToken.test.ts index 7e9b70b7e..008878323 100644 --- a/Tokenization/backend/wrapper/src/test/client/Commands/newToken.test.ts +++ b/Tokenization/backend/wrapper/src/test/client/Commands/newToken.test.ts @@ -46,14 +46,17 @@ jest.mock( */ const createEventMessage = ( targetAddress: string, - connectionDirection: ConnectionDirection + connectionDirection: ConnectionDirection, + token: string ): Command => { return { event: DuplexMessageEvent.MESSAGE_EVENT_NEW_TOKEN, payload: { - targetAddress, - connectionDirection, - token: "test-token", + singleToken: { + targetAddress, + connectionDirection, + token, + }, }, } as Command; }; @@ -103,7 +106,7 @@ describe("NewTokenHandler", () => { dir: ConnectionDirection, token: string ) { - const conn = new Connection(token, address, dir); + const conn = new Connection(token, address, dir, null as any); if (dir === ConnectionDirection.SENDING) { this.sendingConnections.set(address, conn); conn.createSslTunnel(peerCtor, getTestCerts()); @@ -120,7 +123,8 @@ describe("NewTokenHandler", () => { const conn = new Connection( "old-token", targetAddress, - ConnectionDirection.SENDING + ConnectionDirection.SENDING, + null as any ); conn.createSslTunnel(peerCtor, getTestCerts()); @@ -128,12 +132,16 @@ describe("NewTokenHandler", () => { const handler = new NewTokenHandler(manager); const command = new NewTokenCommand( - createEventMessage(targetAddress, ConnectionDirection.SENDING).payload + createEventMessage( + targetAddress, + ConnectionDirection.SENDING, + "new-token" + ).payload ); await handler.handle(command); - expect(conn.getToken()).toBe("test-token"); + expect(conn.getToken()).toBe("new-token"); }); it("should create new RECEIVING connection if not found", async () => { @@ -141,7 +149,11 @@ describe("NewTokenHandler", () => { const handler = new NewTokenHandler(manager); const command = new NewTokenCommand( - createEventMessage(targetAddress, ConnectionDirection.RECEIVING).payload + createEventMessage( + targetAddress, + ConnectionDirection.RECEIVING, + "test-token" + ).payload ); await handler.handle(command); @@ -156,7 +168,11 @@ describe("NewTokenHandler", () => { const handler = new NewTokenHandler(manager); const command = new NewTokenCommand( - createEventMessage(targetAddress, ConnectionDirection.DUPLEX).payload + createEventMessage( + targetAddress, + ConnectionDirection.DUPLEX, + "new-token" + ).payload ); await handler.handle(command); @@ -168,8 +184,8 @@ describe("NewTokenHandler", () => { expect(sendingConn).toBeDefined(); expect(receivingConn).toBeDefined(); - expect(sendingConn.getToken()).toBe("test-token"); - expect(receivingConn.getToken()).toBe("test-token"); + expect(sendingConn.getToken()).toBe("new-token"); + expect(receivingConn.getToken()).toBe("new-token"); }); it("should throw error when payload is missing required fields", async () => { @@ -183,9 +199,11 @@ describe("NewTokenHandler", () => { it("should create command with correct event and payload", () => { const payload = { - targetAddress: "peer-000", - connectionDirection: ConnectionDirection.SENDING, - token: "sample-token", + singleToken: { + targetAddress: "peer-000", + connectionDirection: ConnectionDirection.SENDING, + token: "sample-token", + }, }; const command = new NewTokenCommand(payload); diff --git a/Tokenization/backend/wrapper/src/test/client/Commands/revokeToken.test.ts b/Tokenization/backend/wrapper/src/test/client/Commands/revokeToken.test.ts index 33815675a..0889cb9fe 100644 --- a/Tokenization/backend/wrapper/src/test/client/Commands/revokeToken.test.ts +++ b/Tokenization/backend/wrapper/src/test/client/Commands/revokeToken.test.ts @@ -26,6 +26,7 @@ import * as grpc from "@grpc/grpc-js"; import * as protoLoader from "@grpc/proto-loader"; import path from "path"; import { getTestCerts } from "../../testCerts/testCerts"; +import { connect } from "http2"; // Mock logger jest.mock( @@ -67,8 +68,11 @@ describe("RevokeToken", () => { return { event: DuplexMessageEvent.MESSAGE_EVENT_REVOKE_TOKEN, payload: { - targetAddress: targetAddress, - token: "test-token", + singleToken: { + targetAddress: targetAddress, + token: "test-token", + connectionDirection: ConnectionDirection.SENDING, + }, }, } as Command; }; @@ -93,7 +97,8 @@ describe("RevokeToken", () => { const conn = new Connection( "valid-token", targetAddress, - ConnectionDirection.SENDING + ConnectionDirection.SENDING, + null as any ); conn.createSslTunnel(peerCtor, getTestCerts()); (manager as any).sendingConnections!.set(targetAddress, conn); @@ -114,7 +119,8 @@ describe("RevokeToken", () => { const conn = new Connection( "valid-token", targetAddress, - ConnectionDirection.RECEIVING + ConnectionDirection.RECEIVING, + null as any ); (manager as any).receivingConnections.set(targetAddress, conn); @@ -139,7 +145,7 @@ describe("RevokeToken", () => { await expect(handler.handle(command)).resolves.toBeUndefined(); expect(manager.getConnectionByAddress).toHaveBeenCalledWith( targetAddress, - undefined + "SENDING" ); }); @@ -153,7 +159,7 @@ describe("RevokeToken", () => { const command = new RevokeTokenCommand(invalidMessage as any); await expect(handler.handle(command)).rejects.toThrow( - "Target address is required to revoke token." + "Target address and connection direction are required to revoke token." ); }); diff --git a/Tokenization/backend/wrapper/src/test/client/ConnectionManager/Interceptors/grpc.auth.interceptor.test.ts b/Tokenization/backend/wrapper/src/test/client/ConnectionManager/Interceptors/grpc.auth.interceptor.test.ts index ac9a170ec..3ec430087 100644 --- a/Tokenization/backend/wrapper/src/test/client/ConnectionManager/Interceptors/grpc.auth.interceptor.test.ts +++ b/Tokenization/backend/wrapper/src/test/client/ConnectionManager/Interceptors/grpc.auth.interceptor.test.ts @@ -1,59 +1,9 @@ -/** - * @license - * Copyright 2019-2020 CERN and copyright holders of ALICE O2. - * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. - * All rights not expressly granted are reserved. - * - * This software is distributed under the terms of the GNU General Public - * License v3 (GPL Version 3), copied verbatim in the file "COPYING". - * - * In applying this license CERN does not waive the privileges and immunities - * granted to it by virtue of its status as an Intergovernmental Organization - * or submit itself to any jurisdiction. - */ - -import * as grpc from "@grpc/grpc-js"; -import * as jose from "jose"; -import * as interceptor from "../../../../client/ConnectionManager/Interceptors/grpc.auth.interceptor"; - -// Connection class mock -jest.mock( - "../../../../client/Connection/Connection", - () => { - return { - Connection: jest - .fn() - .mockImplementation( - (jweToken: string, address: string, direction: any) => { - return { - jweToken, - address, - direction, - status: 1, - payload: { subSerialNumber: "AABBCC", perm: { POST: true } }, - getStatus: jest.fn(function () { - return this.status; - }), - getToken: jest.fn(function () { - return this.jweToken; - }), - getCachedTokenPayload: jest.fn(function () { - return this.payload; - }), - handleFailedAuth: jest.fn(), - handleSuccessfulAuth: jest.fn(function (p: any) { - this.payload = p; - this.status = 1; - }), - }; - } - ), - }; - }, - { virtual: true } -); - -import { Connection } from "../../../../client/Connection/Connection"; +import { + isRequestAllowed, + isPermissionUnexpired, + isSerialNumberMatching, + getPeerCertFromCall, +} from "../../../../client/ConnectionManager/Interceptors/grpc.auth.interceptor"; jest.mock("jose", () => ({ importPKCS8: jest.fn(), @@ -62,317 +12,109 @@ jest.mock("jose", () => ({ compactVerify: jest.fn(), })); -import { - ConnectionStatus, - TokenPayload, -} from "../../../../models/connection.model"; -import { SecurityContext } from "../../../../utils/security/SecurityContext"; -import { ConnectionDirection } from "../../../../models/message.model"; - -const mockSecurityContext = { - clientPrivateKey: Buffer.from("mock_private_key_rsa"), - JWS_PUBLIC_KEY: "mock_public_key_ed25519", -} as unknown as SecurityContext; - -let isRequestAllowedSpy: jest.SpyInstance; -let isSerialNumberMatchingSpy: jest.SpyInstance; -let getPeerCertFromCallSpy: jest.SpyInstance; - -const mockCall = { - metadata: { getMap: jest.fn(() => ({})) }, - getPeer: jest.fn(() => "ipv4:127.0.0.1:12345"), - request: { method: "POST" }, -} as unknown as grpc.ServerUnaryCall; - -const mockCallback = jest.fn(); -const mockClientConnections = new Map(); - -describe("gRPCAuthInterceptor", () => { - const MOCK_ADDRESS = "ipv4:127.0.0.1:12345"; - const VALID_JWE = "valid.jwe.token"; - const VALID_JWS = "valid.jws.token"; - const DECRYPTED_PAYLOAD: TokenPayload = { - subSerialNumber: "DDEEFF", - perm: { POST: true, GET: false }, - } as any; - - beforeEach(() => { - jest.clearAllMocks(); - mockClientConnections.clear(); - - (mockCall.metadata.getMap as unknown as jest.Mock).mockReturnValue({ - jwetoken: VALID_JWE, +describe("grpc.auth.interceptor", () => { + describe("isPermissionUnexpired", () => { + it("returns true if current time is within iat and exp", () => { + const now = Math.floor(Date.now() / 1000); + expect(isPermissionUnexpired(now - 10, now + 10)).toBe(true); }); - (mockCall.getPeer as unknown as jest.Mock).mockReturnValue(MOCK_ADDRESS); - (mockCall as any).request = { method: "POST" }; - (jose.importPKCS8 as jest.Mock).mockResolvedValue("mock_priv_key"); - (jose.compactDecrypt as jest.Mock).mockResolvedValue({ - plaintext: Buffer.from(VALID_JWS), + it("returns false if current time is after exp", () => { + const now = Math.floor(Date.now() / 1000); + expect(isPermissionUnexpired(now - 20, now - 10)).toBe(false); }); - (jose.importJWK as jest.Mock).mockResolvedValue("mock_pub_key"); - (jose.compactVerify as jest.Mock).mockResolvedValue({ - payload: Buffer.from(JSON.stringify(DECRYPTED_PAYLOAD)), - protectedHeader: { alg: "EdDSA" }, - }); - - // mocks of internal functions - isRequestAllowedSpy = jest - .spyOn(interceptor, "isRequestAllowed") - .mockImplementation((_p, _r, _cb) => true); - - isSerialNumberMatchingSpy = jest - .spyOn(interceptor, "isSerialNumberMatching") - .mockImplementation((_p, _pc, _cb) => true); - - getPeerCertFromCallSpy = jest - .spyOn(interceptor, "getPeerCertFromCall") - .mockReturnValue({ serialNumber: "DDEEFF" }); - }); - - const getCreatedConn = () => { - const instances = (Connection as jest.Mock).mock?.instances ?? []; - return ( - instances.find((i: any) => i.address === MOCK_ADDRESS) ?? - mockClientConnections.get(MOCK_ADDRESS) - ); - }; - - it("should fail if no JWE token is provided in the metadata", async () => { - (mockCall.metadata.getMap as unknown as jest.Mock).mockReturnValue({}); - - const result = await interceptor.gRPCAuthInterceptor( - mockCall, - mockCallback, - mockClientConnections as any, - mockSecurityContext - ); - - expect(result.isAuthenticated).toBe(false); - expect(result.conn).toBe(null); - expect(mockCallback).toHaveBeenCalledWith( - expect.objectContaining({ - code: grpc.status.UNAUTHENTICATED, - message: "No token provided", - }), - null - ); - }); - - it("should authenticate instantly if connection exists and token hasn't changed", async () => { - const existingConn = new (Connection as jest.Mock)( - VALID_JWE, - MOCK_ADDRESS, - ConnectionDirection.RECEIVING - ); - existingConn.getToken.mockReturnValue(VALID_JWE); - mockClientConnections.set(MOCK_ADDRESS, existingConn); - - const result = await interceptor.gRPCAuthInterceptor( - mockCall, - mockCallback, - mockClientConnections as any, - mockSecurityContext - ); - expect(result.isAuthenticated).toBe(true); - expect(result.conn).toBe(existingConn); - expect(isRequestAllowedSpy).toHaveBeenCalledTimes(1); - expect(isSerialNumberMatchingSpy).toHaveBeenCalledTimes(1); - expect(jose.compactDecrypt as jest.Mock).not.toHaveBeenCalled(); + it("returns false if current time is before iat", () => { + const now = Math.floor(Date.now() / 1000); + expect(isPermissionUnexpired(now + 10, now + 20)).toBe(false); + }); }); - it("should reject if connection exists but is BLOCKED", async () => { - const existingConn = new (Connection as jest.Mock)( - VALID_JWE, - MOCK_ADDRESS, - ConnectionDirection.RECEIVING - ); - existingConn.status = ConnectionStatus.BLOCKED; - mockClientConnections.set(MOCK_ADDRESS, existingConn); - - const result = await interceptor.gRPCAuthInterceptor( - mockCall, - mockCallback, - mockClientConnections as any, - mockSecurityContext - ); - - expect(result.isAuthenticated).toBe(false); - expect(mockCallback).toHaveBeenCalledWith( - expect.objectContaining({ - code: grpc.status.UNAUTHENTICATED, - message: "Connection is blocked. Contact administrator.", - }), - null - ); - }); + describe("isRequestAllowed", () => { + const callback = jest.fn(); - it("should reject existing connection on serial number mismatch", async () => { - const existingConn = new (Connection as jest.Mock)( - VALID_JWE, - MOCK_ADDRESS, - ConnectionDirection.RECEIVING - ); - existingConn.getToken.mockReturnValue(VALID_JWE); - mockClientConnections.set(MOCK_ADDRESS, existingConn); + const validPayload = { + iat: { POST: Math.floor(Date.now() / 1000) - 10 }, + exp: { POST: Math.floor(Date.now() / 1000) + 100 }, + sub: "serial", + aud: "aud", + iss: "iss", + jti: "jti", + }; - // mock serial number mismatch - isSerialNumberMatchingSpy.mockImplementation((_p, _pc, cb) => { - cb( - { - name: "AuthenticationError", - code: grpc.status.PERMISSION_DENIED, - message: "Serial number mismatch (mTLS binding failure).", - } as any, - null + it("returns true for valid payload and unexpired permission", () => { + expect(isRequestAllowed(validPayload, { method: "POST" }, callback)).toBe( + true ); - return false; }); - const result = await interceptor.gRPCAuthInterceptor( - mockCall, - mockCallback, - mockClientConnections as any, - mockSecurityContext - ); - - expect(result.isAuthenticated).toBe(false); - expect(existingConn.handleFailedAuth).toHaveBeenCalledTimes(1); - expect(mockCallback).toHaveBeenCalledWith( - expect.objectContaining({ - message: "Serial number mismatch (mTLS binding failure).", - }), - null - ); - }); - - it("should successfully authenticate a NEW connection", async () => { - (mockCall.metadata.getMap as unknown as jest.Mock).mockReturnValue({ - jwetoken: "NEW.JWE.TOKEN", + it("returns false and calls callback for expired permission", () => { + const expiredPayload = { + ...validPayload, + iat: { POST: Math.floor(Date.now() / 1000) - 100 }, + exp: { POST: Math.floor(Date.now() / 1000) - 10 }, + }; + callback.mockClear(); + expect( + isRequestAllowed(expiredPayload, { method: "POST" }, callback) + ).toBe(false); + expect(callback).toHaveBeenCalled(); }); - const result = await interceptor.gRPCAuthInterceptor( - mockCall, - mockCallback, - mockClientConnections as any, - mockSecurityContext - ); - - const created = getCreatedConn(); - expect(result.isAuthenticated).toBe(true); - expect(created).toBeDefined(); - expect(created!.handleSuccessfulAuth).toHaveBeenCalledWith( - DECRYPTED_PAYLOAD - ); - expect(jose.compactDecrypt as jest.Mock).toHaveBeenCalledTimes(1); - expect(jose.compactVerify as jest.Mock).toHaveBeenCalledTimes(1); - expect(isSerialNumberMatchingSpy).toHaveBeenCalledTimes(1); - expect(isRequestAllowedSpy).toHaveBeenCalledTimes(1); - }); - - it("should fail if JWE decryption fails", async () => { - (jose.compactDecrypt as jest.Mock).mockRejectedValue( - new Error("Decryption failed") - ); - - const result = await interceptor.gRPCAuthInterceptor( - mockCall, - mockCallback, - mockClientConnections as any, - mockSecurityContext - ); - - const created = getCreatedConn(); - expect(result.isAuthenticated).toBe(false); - expect(created!.handleFailedAuth).toHaveBeenCalledTimes(1); - expect(mockCallback).toHaveBeenCalledWith( - expect.objectContaining({ - message: "Incorrect token provided (JWE Decryption failed)", - }), - null - ); + it("returns false and calls callback for invalid payload", () => { + callback.mockClear(); + expect(isRequestAllowed(undefined, { method: "POST" }, callback)).toBe( + false + ); + expect(callback).toHaveBeenCalled(); + }); }); - it("should fail if JWS verification fails", async () => { - (jose.compactVerify as jest.Mock).mockRejectedValue( - new Error("Invalid signature") - ); - - const result = await interceptor.gRPCAuthInterceptor( - mockCall, - mockCallback, - mockClientConnections as any, - mockSecurityContext - ); - - const created = getCreatedConn(); - expect(result.isAuthenticated).toBe(false); - expect(created!.handleFailedAuth).toHaveBeenCalledTimes(1); - expect(mockCallback).toHaveBeenCalledWith( - expect.objectContaining({ - message: "JWS Verification error: Invalid signature", - }), - null - ); - }); + describe("isSerialNumberMatching", () => { + const callback = jest.fn(); - it("should fail if mTLS serial number mismatch occurs after decryption", async () => { - isSerialNumberMatchingSpy.mockImplementation((_p, _pc, cb) => { - cb( - { - name: "AuthenticationError", - code: grpc.status.PERMISSION_DENIED, - message: "Serial number mismatch (mTLS binding failure).", - } as any, - null - ); - return false; + it("returns true if serial numbers match", () => { + const payload = { sub: "ABCDEF" } as any; + const peerCert = { serialNumber: "ab:cd:ef" }; + expect(isSerialNumberMatching(payload, peerCert, callback)).toBe(true); }); - const result = await interceptor.gRPCAuthInterceptor( - mockCall, - mockCallback, - mockClientConnections as any, - mockSecurityContext - ); + it("returns false and calls callback if serial numbers do not match", () => { + const payload = { sub: "ABCDEF" } as any; + const peerCert = { serialNumber: "123456" }; + callback.mockClear(); + expect(isSerialNumberMatching(payload, peerCert, callback)).toBe(false); + expect(callback).toHaveBeenCalled(); + }); - const created = getCreatedConn(); - expect(result.isAuthenticated).toBe(false); - expect(created!.handleFailedAuth).toHaveBeenCalledTimes(1); - expect(mockCallback).toHaveBeenCalledWith( - expect.objectContaining({ - message: "Serial number mismatch (mTLS binding failure).", - }), - null - ); + it("returns false and calls callback if serial number is missing", () => { + const payload = { sub: "ABCDEF" } as any; + const peerCert = {}; + callback.mockClear(); + expect(isSerialNumberMatching(payload, peerCert, callback)).toBe(false); + expect(callback).toHaveBeenCalled(); + }); }); - it("should fail if request authorization check fails", async () => { - isRequestAllowedSpy.mockImplementation((_p, _r, cb) => { - cb( - { - name: "AuthorizationError", - code: grpc.status.PERMISSION_DENIED, - message: "Request of type POST is not allowed by the token policy.", - } as any, - null - ); - return false; + describe("getPeerCertFromCall", () => { + it("returns peer certificate from nested call object", () => { + const fakeCert = { serialNumber: "123" }; + const call = { + call: { + stream: { + session: { + socket: { + getPeerCertificate: jest.fn().mockReturnValue(fakeCert), + }, + }, + }, + }, + }; + expect(getPeerCertFromCall(call)).toBe(fakeCert); }); - const result = await interceptor.gRPCAuthInterceptor( - mockCall, - mockCallback, - mockClientConnections as any, - mockSecurityContext - ); - - expect(result.isAuthenticated).toBe(false); - expect(mockCallback).toHaveBeenCalledWith( - expect.objectContaining({ code: grpc.status.PERMISSION_DENIED }), - null - ); - expect(isSerialNumberMatchingSpy).toHaveBeenCalledTimes(1); + it("returns undefined if structure is missing", () => { + expect(getPeerCertFromCall({})).toBeUndefined(); + }); }); }); From a99ecf2249c4ac1d54be464030e9a0e1247d8f3c Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Fri, 10 Oct 2025 18:36:43 +0200 Subject: [PATCH 75/81] feat: add tests for Connection class --- .../test/client/Connection/Connection.test.ts | 209 ++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 Tokenization/backend/wrapper/src/test/client/Connection/Connection.test.ts diff --git a/Tokenization/backend/wrapper/src/test/client/Connection/Connection.test.ts b/Tokenization/backend/wrapper/src/test/client/Connection/Connection.test.ts new file mode 100644 index 000000000..3913f72d7 --- /dev/null +++ b/Tokenization/backend/wrapper/src/test/client/Connection/Connection.test.ts @@ -0,0 +1,209 @@ +import { Connection } from "../../../client/Connection/Connection"; +import { + ConnectionDirection, + TOKEN_REASON_HEADER, + TokenAuthReason, +} from "../../../models/message.model"; +import { + ConnectionStatus, + TokenPayload, +} from "../../../models/connection.model"; + +jest.mock("@aliceo2/web-ui", () => ({ + LogManager: { + getLogger: jest.fn(() => ({ + warnMessage: jest.fn(), + errorMessage: jest.fn(), + })), + }, +})); + +const mockRenewToken = jest.fn(); + +describe("Connection", () => { + const jweToken = "test-token"; + const targetAddress = "localhost:50051"; + const direction = ConnectionDirection.SENDING; + + let connection: Connection; + + beforeEach(() => { + connection = new Connection( + jweToken, + targetAddress, + direction, + mockRenewToken, + "serial-123" + ); + mockRenewToken.mockClear(); + }); + + it("should initialize with correct values", () => { + expect(connection.getToken()).toBe(jweToken); + expect(connection.getTargetAddress()).toBe(targetAddress); + expect(connection.getStatus()).toBe(ConnectionStatus.CONNECTED); + expect(connection.getSerialNumber()).toBe("serial-123"); + expect(typeof connection.getLastActiveTimestamp()).toBe("number"); + }); + + it("should update and get serial number", () => { + connection.setSerialNumber("new-serial"); + expect(connection.getSerialNumber()).toBe("new-serial"); + }); + + it("should handle new token", () => { + connection.handleNewToken("new-token"); + expect(connection.getToken()).toBe("new-token"); + expect(connection.getStatus()).toBe(ConnectionStatus.CONNECTED); + }); + + it("should handle token revocation", () => { + connection.handleRevokeToken(); + expect(connection.getToken()).toBe(""); + expect(connection.getStatus()).toBe(ConnectionStatus.UNAUTHORIZED); + }); + + it("should handle successful authentication", () => { + const payload: TokenPayload = { + sub: "user", + exp: Date.now() + 1000, + } as any; + connection.handleSuccessfulAuth(payload); + expect(connection.getCachedTokenPayload()).toBe(payload); + expect(connection.getStatus()).toBe(ConnectionStatus.CONNECTED); + expect(typeof connection.getLastActiveTimestamp()).toBe("number"); + }); + + it("should increment auth failures and block after 5 failures", () => { + for (let i = 0; i < 4; i++) { + expect(connection.handleFailedAuth()).toBe(i + 1); + expect(connection.getStatus()).toBe(ConnectionStatus.CONNECTED); + } + expect(connection.handleFailedAuth()).toBe(5); + expect(connection.getStatus()).toBe(ConnectionStatus.BLOCKED); + }); + + it("should update status", () => { + connection.updateStatus(ConnectionStatus.BLOCKED); + expect(connection.getStatus()).toBe(ConnectionStatus.BLOCKED); + }); + + it("should attach grpc client", () => { + const grpcClient = { Fetch: jest.fn() }; + connection.attachGrpcClient(grpcClient); + // @ts-ignore + expect(connection.peerClient).toBe(grpcClient); + }); + + describe("fetch", () => { + let grpcClient: any; + + beforeEach(() => { + grpcClient = { + Fetch: jest.fn(), + }; + connection.attachGrpcClient(grpcClient); + connection.updateStatus(ConnectionStatus.CONNECTED); + }); + + it("should throw if peerClient is not attached", async () => { + const c = new Connection( + jweToken, + targetAddress, + direction, + mockRenewToken + ); + await expect(c.fetch()).rejects.toThrow(/Peer client not attached/); + }); + + it("should throw if connection is blocked", async () => { + connection.updateStatus(ConnectionStatus.BLOCKED); + await expect(connection.fetch()).rejects.toThrow(/Connection is blocked/); + }); + + it("should perform a successful fetch", async () => { + grpcClient.Fetch.mockImplementation((_req: any, _meta: any, cb: any) => { + cb(null, { + status: 200, + headers: { foo: "bar" }, + body: Buffer.from("ok"), + }); + }); + const res = await connection.fetch({ method: "GET", path: "/test" }); + expect(res.status).toBe(200); + expect(res.headers.foo).toBe("bar"); + expect(await res.text()).toBe("ok"); + }); + + it("should handle token renewal and retry fetch", async () => { + // Simulate first fetch fails with renewable error, then succeeds + let callCount = 0; + grpcClient.Fetch.mockImplementation((_req: any, meta: any, cb: any) => { + callCount++; + if (callCount === 1) { + const err: any = new Error("Token expired"); + err.metadataMap = new Map([ + [TOKEN_REASON_HEADER, TokenAuthReason.PERMISSION_EXPIRED], + ]); + cb(err); + } else { + cb(null, { status: 200, headers: {}, body: Buffer.from("retry-ok") }); + } + }); + + // Patch handleNewToken to simulate token refresh + setTimeout(() => { + connection.handleNewToken("refreshed-token"); + }, 10); + + const res = await connection.fetch({ method: "POST", path: "/renew" }); + expect(res.status).toBe(200); + expect(await res.text()).toBe("retry-ok"); + expect(mockRenewToken).toHaveBeenCalled(); + }); + + it("should throw on non-renewable error", async () => { + grpcClient.Fetch.mockImplementation((_req: any, _meta: any, cb: any) => { + const err: any = new Error("Forbidden"); + err.metadataMap = new Map([ + [TOKEN_REASON_HEADER, TokenAuthReason.PERMISSION_FORBIDDEN], + ]); + cb(err); + }); + await expect(connection.fetch({ path: "/fail" })).rejects.toThrow( + "Forbidden" + ); + }); + }); + + describe("createSslTunnel", () => { + const peerCtor = jest.fn(); + const certs = { + caCert: Buffer.from("ca"), + clientCert: Buffer.from("cert"), + clientKey: Buffer.from("key"), + }; + + it("should throw if certs are missing", () => { + expect(() => + connection.createSslTunnel(peerCtor, { + caCert: Buffer.from("ca"), + clientCert: undefined as any, + clientKey: Buffer.from("key"), + }) + ).toThrow(/Connection certificates are required/); + }); + + it("should create ssl tunnel and set status", () => { + const grpcCreds = {}; + const oldCreateSsl = jest + .spyOn(require("@grpc/grpc-js").credentials, "createSsl") + .mockReturnValue(grpcCreds as any); + peerCtor.mockImplementation(() => ({})); + connection.createSslTunnel(peerCtor, certs); + expect(peerCtor).toHaveBeenCalledWith(targetAddress, grpcCreds); + expect(connection.getStatus()).toBe(ConnectionStatus.CONNECTED); + oldCreateSsl.mockRestore(); + }); + }); +}); From fe5957d6bb216ebf20e85602b486fdbf9838b13d Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Fri, 10 Oct 2025 19:22:54 +0200 Subject: [PATCH 76/81] fix: fix tests and renew token logic --- .../src/client/Connection/Connection.ts | 8 ++- .../Interceptors/grpc.auth.interceptor.ts | 65 +++++++------------ .../grpc.auth.interceptor.test.ts | 35 +++++----- 3 files changed, 47 insertions(+), 61 deletions(-) diff --git a/Tokenization/backend/wrapper/src/client/Connection/Connection.ts b/Tokenization/backend/wrapper/src/client/Connection/Connection.ts index 49aa4506e..5b124e942 100644 --- a/Tokenization/backend/wrapper/src/client/Connection/Connection.ts +++ b/Tokenization/backend/wrapper/src/client/Connection/Connection.ts @@ -313,7 +313,13 @@ export class Connection { * @param reason The reason for the token renewal. */ private triggerTokenRenewIfNeeded(reason: TokenAuthReason) { - if (this.isRefreshing) return; + if ( + this.isRefreshing || + (reason !== TokenAuthReason.PERMISSION_EXPIRED && + reason !== TokenAuthReason.NO_TOKEN && + reason !== TokenAuthReason.PERMISSION_FORBIDDEN) + ) + return; this.isRefreshing = true; this.updateStatus(ConnectionStatus.TOKEN_REFRESH); this.createTokenRefreshPromise(); // sets pendingTokenRefresh diff --git a/Tokenization/backend/wrapper/src/client/ConnectionManager/Interceptors/grpc.auth.interceptor.ts b/Tokenization/backend/wrapper/src/client/ConnectionManager/Interceptors/grpc.auth.interceptor.ts index 293ac1753..69ebe2dc1 100644 --- a/Tokenization/backend/wrapper/src/client/ConnectionManager/Interceptors/grpc.auth.interceptor.ts +++ b/Tokenization/backend/wrapper/src/client/ConnectionManager/Interceptors/grpc.auth.interceptor.ts @@ -75,26 +75,24 @@ export const gRPCAuthInterceptor = async ( if (conn.getToken() === jweToken) { // check for allowed requests and serial number match if token is the same - if ( - !isRequestAllowed(conn.getCachedTokenPayload(), call.request, callback) - ) { + const isReqAllowed = isRequestAllowed( + conn.getCachedTokenPayload(), + call.request + ); + if (!isReqAllowed.isAllowed) { return createFailAuthResponse( call, callback, conn, grpc.status.PERMISSION_DENIED, "Method not allowed", - TokenAuthReason.PERMISSION_FORBIDDEN + isReqAllowed.isUnexpired + ? TokenAuthReason.PERMISSION_FORBIDDEN + : TokenAuthReason.PERMISSION_EXPIRED ); } - if ( - !isSerialNumberMatching( - conn.getCachedTokenPayload(), - peerCert, - callback - ) - ) { + if (!isSerialNumberMatching(conn.getCachedTokenPayload(), peerCert)) { conn.handleFailedAuth(); return createFailAuthResponse( call, @@ -191,7 +189,7 @@ export const gRPCAuthInterceptor = async ( // mTLS binding check and authorization // Connection tunnel verification with serialNumber (mTLS SN vs Token SN) - if (!isSerialNumberMatching(payload, peerCert, callback)) { + if (!isSerialNumberMatching(payload, peerCert)) { conn.handleFailedAuth(); return createFailAuthResponse( call, @@ -204,14 +202,20 @@ export const gRPCAuthInterceptor = async ( } // Validate permission for request method (Authorization check) - if (!isRequestAllowed(payload, call.request, callback)) { + const isReqAllowed = isRequestAllowed( + conn.getCachedTokenPayload(), + call.request + ); + if (!isReqAllowed.isAllowed) { return createFailAuthResponse( call, callback, conn, grpc.status.PERMISSION_DENIED, "Method not allowed", - TokenAuthReason.PERMISSION_FORBIDDEN + isReqAllowed.isUnexpired + ? TokenAuthReason.PERMISSION_FORBIDDEN + : TokenAuthReason.PERMISSION_EXPIRED ); } @@ -230,12 +234,11 @@ export const gRPCAuthInterceptor = async ( */ export const isRequestAllowed = ( tokenPayload: TokenPayload | undefined, - request: any, - callback: grpc.sendUnaryData -): Boolean => { + request: any +): { isAllowed: boolean; isUnexpired: boolean } => { const method = String(request?.method || "POST").toUpperCase(); const isValidPayload = validateTokenPayload(tokenPayload, request.method); - let isUnexpired; + let isUnexpired = true; if (isValidPayload) { isUnexpired = isPermissionUnexpired( @@ -245,21 +248,10 @@ export const isRequestAllowed = ( } if (!isValidPayload || !isUnexpired) { - const error = { - name: "AuthorizationError", - code: isUnexpired - ? grpc.status.PERMISSION_DENIED - : grpc.status.UNAUTHENTICATED, - message: isUnexpired - ? `Request of type ${method} is not allowed by the token policy.` - : `Request of type ${method}, permission has expired.`, - } as any; - - callback(error, null); - return false; + return { isAllowed: false, isUnexpired: isUnexpired }; } - return true; + return { isAllowed: true, isUnexpired: isUnexpired }; }; /** @@ -298,7 +290,7 @@ const validateTokenPayload = ( * @param exp expiration timestamp for the specific method * @returns true if permission is still valid, false if expired */ -export const isPermissionUnexpired = (iat: number, exp: number): Boolean => { +export const isPermissionUnexpired = (iat: number, exp: number): boolean => { const nowInSeconds = Math.floor(Date.now() / 1000); if (nowInSeconds >= exp) { @@ -321,19 +313,12 @@ export const isPermissionUnexpired = (iat: number, exp: number): Boolean => { */ export const isSerialNumberMatching = ( tokenPayload: TokenPayload | undefined, - peerCert: any, - callback: grpc.sendUnaryData + peerCert: any ): Boolean => { const clientSN = normalizeSerial(peerCert?.serialNumber); const tokenSN = normalizeSerial(tokenPayload?.sub); if (!clientSN || clientSN !== tokenSN) { - const error = { - name: "AuthenticationError", - code: grpc.status.PERMISSION_DENIED, - message: "Serial number mismatch (mTLS binding failure).", - } as any; - callback(error, null); return false; } return true; diff --git a/Tokenization/backend/wrapper/src/test/client/ConnectionManager/Interceptors/grpc.auth.interceptor.test.ts b/Tokenization/backend/wrapper/src/test/client/ConnectionManager/Interceptors/grpc.auth.interceptor.test.ts index 3ec430087..65ef5919b 100644 --- a/Tokenization/backend/wrapper/src/test/client/ConnectionManager/Interceptors/grpc.auth.interceptor.test.ts +++ b/Tokenization/backend/wrapper/src/test/client/ConnectionManager/Interceptors/grpc.auth.interceptor.test.ts @@ -31,8 +31,6 @@ describe("grpc.auth.interceptor", () => { }); describe("isRequestAllowed", () => { - const callback = jest.fn(); - const validPayload = { iat: { POST: Math.floor(Date.now() / 1000) - 10 }, exp: { POST: Math.floor(Date.now() / 1000) + 100 }, @@ -43,9 +41,10 @@ describe("grpc.auth.interceptor", () => { }; it("returns true for valid payload and unexpired permission", () => { - expect(isRequestAllowed(validPayload, { method: "POST" }, callback)).toBe( - true - ); + expect(isRequestAllowed(validPayload, { method: "POST" })).toEqual({ + isAllowed: true, + isUnexpired: true, + }); }); it("returns false and calls callback for expired permission", () => { @@ -54,19 +53,17 @@ describe("grpc.auth.interceptor", () => { iat: { POST: Math.floor(Date.now() / 1000) - 100 }, exp: { POST: Math.floor(Date.now() / 1000) - 10 }, }; - callback.mockClear(); - expect( - isRequestAllowed(expiredPayload, { method: "POST" }, callback) - ).toBe(false); - expect(callback).toHaveBeenCalled(); + expect(isRequestAllowed(expiredPayload, { method: "POST" })).toEqual({ + isAllowed: false, + isUnexpired: false, + }); }); it("returns false and calls callback for invalid payload", () => { - callback.mockClear(); - expect(isRequestAllowed(undefined, { method: "POST" }, callback)).toBe( - false - ); - expect(callback).toHaveBeenCalled(); + expect(isRequestAllowed(undefined, { method: "POST" })).toEqual({ + isAllowed: false, + isUnexpired: true, + }); }); }); @@ -76,23 +73,21 @@ describe("grpc.auth.interceptor", () => { it("returns true if serial numbers match", () => { const payload = { sub: "ABCDEF" } as any; const peerCert = { serialNumber: "ab:cd:ef" }; - expect(isSerialNumberMatching(payload, peerCert, callback)).toBe(true); + expect(isSerialNumberMatching(payload, peerCert)).toBe(true); }); it("returns false and calls callback if serial numbers do not match", () => { const payload = { sub: "ABCDEF" } as any; const peerCert = { serialNumber: "123456" }; callback.mockClear(); - expect(isSerialNumberMatching(payload, peerCert, callback)).toBe(false); - expect(callback).toHaveBeenCalled(); + expect(isSerialNumberMatching(payload, peerCert)).toBe(false); }); it("returns false and calls callback if serial number is missing", () => { const payload = { sub: "ABCDEF" } as any; const peerCert = {}; callback.mockClear(); - expect(isSerialNumberMatching(payload, peerCert, callback)).toBe(false); - expect(callback).toHaveBeenCalled(); + expect(isSerialNumberMatching(payload, peerCert)).toBe(false); }); }); From 224fdb7cb4e81b93cbbf8dfe42a4670cff7fec22 Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Fri, 10 Oct 2025 19:25:36 +0200 Subject: [PATCH 77/81] fix: add banners --- .../src/test/client/Connection/Connection.test.ts | 14 ++++++++++++++ .../Interceptors/grpc.auth.interceptor.test.ts | 14 ++++++++++++++ .../wrapper/src/test/testCerts/testCerts.ts | 14 ++++++++++++++ .../src/test/utils/queues/RetryQueue.test.ts | 14 ++++++++++++++ 4 files changed, 56 insertions(+) diff --git a/Tokenization/backend/wrapper/src/test/client/Connection/Connection.test.ts b/Tokenization/backend/wrapper/src/test/client/Connection/Connection.test.ts index 3913f72d7..dd4030715 100644 --- a/Tokenization/backend/wrapper/src/test/client/Connection/Connection.test.ts +++ b/Tokenization/backend/wrapper/src/test/client/Connection/Connection.test.ts @@ -1,3 +1,17 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + import { Connection } from "../../../client/Connection/Connection"; import { ConnectionDirection, diff --git a/Tokenization/backend/wrapper/src/test/client/ConnectionManager/Interceptors/grpc.auth.interceptor.test.ts b/Tokenization/backend/wrapper/src/test/client/ConnectionManager/Interceptors/grpc.auth.interceptor.test.ts index 65ef5919b..2d187c71e 100644 --- a/Tokenization/backend/wrapper/src/test/client/ConnectionManager/Interceptors/grpc.auth.interceptor.test.ts +++ b/Tokenization/backend/wrapper/src/test/client/ConnectionManager/Interceptors/grpc.auth.interceptor.test.ts @@ -1,3 +1,17 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + import { isRequestAllowed, isPermissionUnexpired, diff --git a/Tokenization/backend/wrapper/src/test/testCerts/testCerts.ts b/Tokenization/backend/wrapper/src/test/testCerts/testCerts.ts index 3e2f3ef93..92c6a308c 100644 --- a/Tokenization/backend/wrapper/src/test/testCerts/testCerts.ts +++ b/Tokenization/backend/wrapper/src/test/testCerts/testCerts.ts @@ -1,3 +1,17 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + import { CentralSystemConfig, gRPCWrapperConfig } from "models/config.model"; import path from "path"; import * as fs from "fs"; diff --git a/Tokenization/backend/wrapper/src/test/utils/queues/RetryQueue.test.ts b/Tokenization/backend/wrapper/src/test/utils/queues/RetryQueue.test.ts index 606a1c01e..9bcddb21a 100644 --- a/Tokenization/backend/wrapper/src/test/utils/queues/RetryQueue.test.ts +++ b/Tokenization/backend/wrapper/src/test/utils/queues/RetryQueue.test.ts @@ -1,3 +1,17 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + import { RetryQueue, RetryTask } from "../../../utils/queues/RetryQueue"; describe("RetryQueue", () => { From b71778a41958e6533ae0ccf43d5cd73af1678513 Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki <42175519+OmegaCreations@users.noreply.github.com> Date: Fri, 10 Oct 2025 19:31:45 +0200 Subject: [PATCH 78/81] Potential fix for code scanning alert no. 266: Unused variable, import, function or class Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .../wrapper/src/test/client/Commands/revokeToken.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tokenization/backend/wrapper/src/test/client/Commands/revokeToken.test.ts b/Tokenization/backend/wrapper/src/test/client/Commands/revokeToken.test.ts index 0889cb9fe..dbdd2297b 100644 --- a/Tokenization/backend/wrapper/src/test/client/Commands/revokeToken.test.ts +++ b/Tokenization/backend/wrapper/src/test/client/Commands/revokeToken.test.ts @@ -26,7 +26,7 @@ import * as grpc from "@grpc/grpc-js"; import * as protoLoader from "@grpc/proto-loader"; import path from "path"; import { getTestCerts } from "../../testCerts/testCerts"; -import { connect } from "http2"; + // Mock logger jest.mock( From 04eb756a1c15b2e2765fc8559c89aa2f5b8dc861 Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Fri, 10 Oct 2025 21:17:48 +0200 Subject: [PATCH 79/81] fix: fix events for commands --- .../src/central/Commands/getAllTokens/getAllTokens.command.ts | 2 +- .../src/central/Commands/renewToken/renewToken.command.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Tokenization/backend/wrapper/src/central/Commands/getAllTokens/getAllTokens.command.ts b/Tokenization/backend/wrapper/src/central/Commands/getAllTokens/getAllTokens.command.ts index 3c3bbd3dc..d6eb157a5 100644 --- a/Tokenization/backend/wrapper/src/central/Commands/getAllTokens/getAllTokens.command.ts +++ b/Tokenization/backend/wrapper/src/central/Commands/getAllTokens/getAllTokens.command.ts @@ -19,6 +19,6 @@ import { DuplexMessageEvent } from "../../../models/message.model"; * @description Command used to retrieve all tokens for a client. Handles structure logic. */ export class GetAllTokensCommand implements Command { - readonly event = DuplexMessageEvent.MESSAGE_EVENT_NEW_TOKEN; + readonly event = DuplexMessageEvent.MESSAGE_EVENT_GET_ALL_TOKENS; constructor() {} } diff --git a/Tokenization/backend/wrapper/src/central/Commands/renewToken/renewToken.command.ts b/Tokenization/backend/wrapper/src/central/Commands/renewToken/renewToken.command.ts index 3faff0b9b..3a99f142e 100644 --- a/Tokenization/backend/wrapper/src/central/Commands/renewToken/renewToken.command.ts +++ b/Tokenization/backend/wrapper/src/central/Commands/renewToken/renewToken.command.ts @@ -22,6 +22,6 @@ import { * @description Command used to renew token for a client after its expiration. Handles structure logic. */ export class RenewTokenCommand implements Command { - readonly event = DuplexMessageEvent.MESSAGE_EVENT_NEW_TOKEN; + readonly event = DuplexMessageEvent.MESSAGE_EVENT_RENEW_TOKEN; constructor(payload: SingleTokenPayload) {} } From 8e6b80efb5edc8fd71a026ad6e6e23c954c89bee Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Fri, 10 Oct 2025 21:27:43 +0200 Subject: [PATCH 80/81] feat: add dispatching commands to the central system --- Tokenization/backend/wrapper/src/central/CentralSystemWrapper.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/Tokenization/backend/wrapper/src/central/CentralSystemWrapper.ts b/Tokenization/backend/wrapper/src/central/CentralSystemWrapper.ts index 85eb01a72..d095be643 100644 --- a/Tokenization/backend/wrapper/src/central/CentralSystemWrapper.ts +++ b/Tokenization/backend/wrapper/src/central/CentralSystemWrapper.ts @@ -132,6 +132,7 @@ export class CentralSystemWrapper { // Listen for data events from the client call.on("data", (payload: any) => { this.logger.infoMessage(`Received from ${clientIp}:`, payload); + this.dispatcher.dispatch(payload); }); // Handle stream end event From 829db2f2d49123aca8c56913be367b0784d73957 Mon Sep 17 00:00:00 2001 From: Maksymilian Walicki Date: Fri, 10 Oct 2025 21:31:40 +0200 Subject: [PATCH 81/81] fix: make mock virtual --- .../test/client/Connection/Connection.test.ts | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/Tokenization/backend/wrapper/src/test/client/Connection/Connection.test.ts b/Tokenization/backend/wrapper/src/test/client/Connection/Connection.test.ts index dd4030715..ec49ac549 100644 --- a/Tokenization/backend/wrapper/src/test/client/Connection/Connection.test.ts +++ b/Tokenization/backend/wrapper/src/test/client/Connection/Connection.test.ts @@ -23,14 +23,18 @@ import { TokenPayload, } from "../../../models/connection.model"; -jest.mock("@aliceo2/web-ui", () => ({ - LogManager: { - getLogger: jest.fn(() => ({ - warnMessage: jest.fn(), - errorMessage: jest.fn(), - })), - }, -})); +jest.mock( + "@aliceo2/web-ui", + () => ({ + LogManager: { + getLogger: jest.fn(() => ({ + warnMessage: jest.fn(), + errorMessage: jest.fn(), + })), + }, + }), + { virtual: true } +); const mockRenewToken = jest.fn();