From 8b4a859afbc855001eb845aa41cf0725877f87a9 Mon Sep 17 00:00:00 2001 From: Astitva Patle Date: Thu, 28 Aug 2025 13:05:05 +0530 Subject: [PATCH 01/17] fix: added private ipcheck before api call to bypass dnsrebinding [SPRW-1974] --- src/proxy/http/http.service.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/proxy/http/http.service.ts b/src/proxy/http/http.service.ts index 105dc7a..2e10b10 100644 --- a/src/proxy/http/http.service.ts +++ b/src/proxy/http/http.service.ts @@ -230,6 +230,22 @@ export class HttpService { // Add custom user agent config.headers['User-Agent'] = 'SparrowRuntime/1.0.0'; + + // DNS rebinding protection: re-validate resolved IP before request + const resolvedAddresses = await lookup(new URL(url).hostname, { all: true }); + for (const addr of resolvedAddresses) { + const ip = ipaddr.parse(addr.address); + if ( + ip.range() === 'linkLocal' || + ip.range() === 'loopback' || + ip.range() === 'private' || + ip.range() === 'reserved' + ) { + throw new BadRequestException( + `Access to internal IP addresses is not allowed: ${addr.address}`, + ); + } + } try { const response = await this.httpService.axiosRef({ From 1e6cae32f036d2a087d433847e4487c583bda0b9 Mon Sep 17 00:00:00 2001 From: Jatin Lodhi Date: Wed, 3 Sep 2025 14:21:39 +0530 Subject: [PATCH 02/17] build: added selfhost workflow --- .github/workflows/sparrowproxy-selfhost.yml | 41 +++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 .github/workflows/sparrowproxy-selfhost.yml diff --git a/.github/workflows/sparrowproxy-selfhost.yml b/.github/workflows/sparrowproxy-selfhost.yml new file mode 100644 index 0000000..70cfed6 --- /dev/null +++ b/.github/workflows/sparrowproxy-selfhost.yml @@ -0,0 +1,41 @@ +name: Self-host Docker Image +on: + push: + branches: [ main ] + workflow_dispatch: + +permissions: + contents: read + +env: + DOCKER_IMAGE_VERSION: ${{ vars.SELF_HOST_DOCKER_IMAGE_VERSION }} + +jobs: + build: + runs-on: ubuntu-latest + environment: self-host + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_PASSWORD }} + + - name: Build and push (multi-arch) + run: | + docker buildx build \ + --platform linux/amd64,linux/arm64 \ + --build-arg GITHUB_TOKEN=${{ secrets.SPARROW_GITHUB_TOKEN }} \ + -t ssparrowapi/sparrow-proxy:self-host-${DOCKER_IMAGE_VERSION} \ + -t ssparrowapi/sparrow-proxy:latest \ + --push . \ No newline at end of file From 1211abb80cf97f1a81c419183ac18975b316fcb1 Mon Sep 17 00:00:00 2001 From: Jatin Lodhi Date: Wed, 3 Sep 2025 14:34:40 +0530 Subject: [PATCH 03/17] build: added selfhost workflow --- .github/workflows/sparrowproxy-selfhost.yml | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/.github/workflows/sparrowproxy-selfhost.yml b/.github/workflows/sparrowproxy-selfhost.yml index 70cfed6..a8c04f3 100644 --- a/.github/workflows/sparrowproxy-selfhost.yml +++ b/.github/workflows/sparrowproxy-selfhost.yml @@ -1,19 +1,21 @@ name: Self-host Docker Image on: - push: - branches: [ main ] workflow_dispatch: + inputs: + image_version: + description: "Docker image version (e.g. 1.2.3)" + required: true + type: string permissions: contents: read -env: - DOCKER_IMAGE_VERSION: ${{ vars.SELF_HOST_DOCKER_IMAGE_VERSION }} - jobs: build: runs-on: ubuntu-latest environment: self-host + env: + DOCKER_IMAGE_VERSION: ${{ inputs.image_version }} steps: - name: Checkout @@ -33,9 +35,10 @@ jobs: - name: Build and push (multi-arch) run: | + echo "Building version: ${DOCKER_IMAGE_VERSION}" docker buildx build \ --platform linux/amd64,linux/arm64 \ --build-arg GITHUB_TOKEN=${{ secrets.SPARROW_GITHUB_TOKEN }} \ -t ssparrowapi/sparrow-proxy:self-host-${DOCKER_IMAGE_VERSION} \ -t ssparrowapi/sparrow-proxy:latest \ - --push . \ No newline at end of file + --push . From a19eb1946c49e24bafd6884f2c7409b7cca00781 Mon Sep 17 00:00:00 2001 From: Jatin Lodhi Date: Wed, 3 Sep 2025 15:54:49 +0530 Subject: [PATCH 04/17] fix: edit the image tag --- .github/workflows/sparrowproxy-selfhost.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sparrowproxy-selfhost.yml b/.github/workflows/sparrowproxy-selfhost.yml index a8c04f3..8877935 100644 --- a/.github/workflows/sparrowproxy-selfhost.yml +++ b/.github/workflows/sparrowproxy-selfhost.yml @@ -39,6 +39,6 @@ jobs: docker buildx build \ --platform linux/amd64,linux/arm64 \ --build-arg GITHUB_TOKEN=${{ secrets.SPARROW_GITHUB_TOKEN }} \ - -t ssparrowapi/sparrow-proxy:self-host-${DOCKER_IMAGE_VERSION} \ + -t ssparrowapi/sparrow-proxy:${DOCKER_IMAGE_VERSION} \ -t ssparrowapi/sparrow-proxy:latest \ --push . From 6b86d88d0a0825526c5dc50919167243b271a105 Mon Sep 17 00:00:00 2001 From: Jatin Lodhi Date: Wed, 3 Sep 2025 15:59:44 +0530 Subject: [PATCH 05/17] fix: edit the image tag --- .github/workflows/sparrowproxy-selfhost.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/sparrowproxy-selfhost.yml b/.github/workflows/sparrowproxy-selfhost.yml index 8877935..8e5361e 100644 --- a/.github/workflows/sparrowproxy-selfhost.yml +++ b/.github/workflows/sparrowproxy-selfhost.yml @@ -39,6 +39,6 @@ jobs: docker buildx build \ --platform linux/amd64,linux/arm64 \ --build-arg GITHUB_TOKEN=${{ secrets.SPARROW_GITHUB_TOKEN }} \ - -t ssparrowapi/sparrow-proxy:${DOCKER_IMAGE_VERSION} \ - -t ssparrowapi/sparrow-proxy:latest \ + -t sparrowapi/sparrow-proxy:${DOCKER_IMAGE_VERSION} \ + -t sparrowapi/sparrow-proxy:latest \ --push . From 96bd4bfe3cbc81ea18a5d238ab597b1787eed2a8 Mon Sep 17 00:00:00 2001 From: Jatin Lodhi Date: Wed, 3 Sep 2025 16:19:27 +0530 Subject: [PATCH 06/17] fix: edit the image tag --- .github/workflows/sparrowproxy-selfhost.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/sparrowproxy-selfhost.yml b/.github/workflows/sparrowproxy-selfhost.yml index 8e5361e..4bb8480 100644 --- a/.github/workflows/sparrowproxy-selfhost.yml +++ b/.github/workflows/sparrowproxy-selfhost.yml @@ -38,7 +38,6 @@ jobs: echo "Building version: ${DOCKER_IMAGE_VERSION}" docker buildx build \ --platform linux/amd64,linux/arm64 \ - --build-arg GITHUB_TOKEN=${{ secrets.SPARROW_GITHUB_TOKEN }} \ -t sparrowapi/sparrow-proxy:${DOCKER_IMAGE_VERSION} \ -t sparrowapi/sparrow-proxy:latest \ --push . From a89e8d7a5291c56461b5e50a560239fe25d6db88 Mon Sep 17 00:00:00 2001 From: Jatin Lodhi Date: Thu, 4 Sep 2025 13:25:21 +0530 Subject: [PATCH 07/17] update the workflow to get branch name --- .github/workflows/sparrowproxy-selfhost.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/sparrowproxy-selfhost.yml b/.github/workflows/sparrowproxy-selfhost.yml index 4bb8480..863c719 100644 --- a/.github/workflows/sparrowproxy-selfhost.yml +++ b/.github/workflows/sparrowproxy-selfhost.yml @@ -2,6 +2,11 @@ name: Self-host Docker Image on: workflow_dispatch: inputs: + branch: + description: "Git branch to build" + required: true + default: main + type: string image_version: description: "Docker image version (e.g. 1.2.3)" required: true @@ -16,10 +21,14 @@ jobs: environment: self-host env: DOCKER_IMAGE_VERSION: ${{ inputs.image_version }} + BUILD_BRANCH: ${{ inputs.branch }} steps: - name: Checkout uses: actions/checkout@v4 + with: + ref: ${{ env.BUILD_BRANCH }} + fetch-depth: 0 - name: Set up QEMU uses: docker/setup-qemu-action@v3 From 199c74c7655b16f51ec30aa74246ed7195e237a8 Mon Sep 17 00:00:00 2001 From: Jatin Lodhi Date: Thu, 4 Sep 2025 17:03:06 +0530 Subject: [PATCH 08/17] ci: introduce self-host docker publish workflow (auto tag from package.json) [] --- .github/workflows/sparrowproxy-selfhost.yml | 41 ++++++++++++--------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/.github/workflows/sparrowproxy-selfhost.yml b/.github/workflows/sparrowproxy-selfhost.yml index 863c719..5808a33 100644 --- a/.github/workflows/sparrowproxy-selfhost.yml +++ b/.github/workflows/sparrowproxy-selfhost.yml @@ -1,16 +1,9 @@ name: Self-host Docker Image on: + push: + branches: + - main workflow_dispatch: - inputs: - branch: - description: "Git branch to build" - required: true - default: main - type: string - image_version: - description: "Docker image version (e.g. 1.2.3)" - required: true - type: string permissions: contents: read @@ -19,16 +12,26 @@ jobs: build: runs-on: ubuntu-latest environment: self-host - env: - DOCKER_IMAGE_VERSION: ${{ inputs.image_version }} - BUILD_BRANCH: ${{ inputs.branch }} steps: - name: Checkout uses: actions/checkout@v4 - with: - ref: ${{ env.BUILD_BRANCH }} - fetch-depth: 0 + + - name: Read version from package.json + id: version + run: | + if ! command -v jq >/dev/null 2>&1; then + echo "jq not found"; exit 1 + fi + V=$(jq -r '.version' package.json) + if [ -z "$V" ] || [ "$V" = "null" ]; then + echo "Version not found in package.json"; exit 1 + fi + if ! [[ "$V" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Version $V is not valid semver (expected X.Y.Z)"; exit 1 + fi + echo "full=$V" >> "$GITHUB_OUTPUT" + - name: Set up QEMU uses: docker/setup-qemu-action@v3 @@ -44,9 +47,11 @@ jobs: - name: Build and push (multi-arch) run: | - echo "Building version: ${DOCKER_IMAGE_VERSION}" + set -euo pipefail + VERSION='${{ steps.version.outputs.full }}' + echo "Building image tag: $VERSION" docker buildx build \ --platform linux/amd64,linux/arm64 \ - -t sparrowapi/sparrow-proxy:${DOCKER_IMAGE_VERSION} \ + -t $DOCKER_IMAGE_NAME:$VERSION \ -t sparrowapi/sparrow-proxy:latest \ --push . From 790699ee46be39feb5bbde414aa003898d54baf8 Mon Sep 17 00:00:00 2001 From: Jatin Lodhi Date: Thu, 4 Sep 2025 17:10:27 +0530 Subject: [PATCH 09/17] ci: give version in package.json [] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2971335..c3b36e5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sparrow-proxy", - "version": "0.0.1", + "version": "2.29.0", "description": "", "author": "", "private": true, From a6b7d65cfce7dfe029a50c643f838fc3a6926aca Mon Sep 17 00:00:00 2001 From: Aakash Reddy Date: Thu, 25 Sep 2025 17:45:06 +0530 Subject: [PATCH 10/17] feat: setup to test the apis for testflow --- package.json | 6 + src/Types/http-client.ts | 12 + src/enum/httpRequest.enum.ts | 33 ++ src/enum/httpResponseFormat.ts | 25 + src/enum/testflow.enum.ts | 55 +++ src/payloads/testflow.payload.ts | 268 +++++++++++ src/proxy/http/http.module.ts | 1 + src/proxy/proxy.module.ts | 3 +- src/proxy/testflow/testflow.controller.ts | 36 ++ src/proxy/testflow/testflow.module.ts | 12 + src/proxy/testflow/testflow.service.ts | 415 +++++++++++++++++ src/utils/base64Converter.ts | 50 ++ src/utils/decode-testflow.ts | 543 ++++++++++++++++++++++ src/utils/parse-time.ts | 17 + src/utils/status-code.ts | 80 ++++ yarn.lock | 507 +++++++++++++++++++- 16 files changed, 2056 insertions(+), 7 deletions(-) create mode 100644 src/Types/http-client.ts create mode 100644 src/enum/httpRequest.enum.ts create mode 100644 src/enum/httpResponseFormat.ts create mode 100644 src/enum/testflow.enum.ts create mode 100644 src/payloads/testflow.payload.ts create mode 100644 src/proxy/testflow/testflow.controller.ts create mode 100644 src/proxy/testflow/testflow.module.ts create mode 100644 src/proxy/testflow/testflow.service.ts create mode 100644 src/utils/base64Converter.ts create mode 100644 src/utils/decode-testflow.ts create mode 100644 src/utils/parse-time.ts create mode 100644 src/utils/status-code.ts diff --git a/package.json b/package.json index c3b36e5..e8daf58 100644 --- a/package.json +++ b/package.json @@ -24,14 +24,20 @@ "@nestjs/common": "^10.0.0", "@nestjs/core": "^10.0.0", "@nestjs/platform-express": "^10.0.0", + "@nestjs/platform-fastify": "^11.1.6", "@nestjs/platform-socket.io": "^10.4.8", "@nestjs/platform-ws": "^10.4.8", "@nestjs/swagger": "^8.0.7", "@nestjs/websockets": "^10.4.8", "@types/ws": "^8.5.13", "axios": "^1.7.7", + "class-transformer": "0.5.1", + "class-transformer-validator": "^0.9.1", + "class-validator": "^0.14.0", + "fastify": "4.28.1", "form-data": "^4.0.1", "ipaddr.js": "^2.2.0", + "json5": "^2.2.3", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", "socket.io-client": "^4.8.1" diff --git a/src/Types/http-client.ts b/src/Types/http-client.ts new file mode 100644 index 0000000..2f4d6e0 --- /dev/null +++ b/src/Types/http-client.ts @@ -0,0 +1,12 @@ +export interface HttpClientResponseInterface { + status: "success" | "error"; + isSuccessful: boolean; + message: string; + data: T; +} + +export interface HttpClientBackendResponseInterface { + data: T; + message: string; + statusCode: number; +} diff --git a/src/enum/httpRequest.enum.ts b/src/enum/httpRequest.enum.ts new file mode 100644 index 0000000..485347d --- /dev/null +++ b/src/enum/httpRequest.enum.ts @@ -0,0 +1,33 @@ +export interface KeyWrapper { + key: string; +} + +export interface ValueWrapper { + value: string; +} + +export enum RequestDataTypeEnum { + JSON = "JSON", + XML = "XML", + HTML = "HTML", + TEXT = "Text", + JAVASCRIPT = "JavaScript", + IMAGE = "Image", +} + +export enum ResponseStatusCode { + OK = "200 OK", + CREATED = "201 Created", + ACCEPTED = "202 Accepted", + NO_CONTENT = "204 No Content", + BAD_REQUEST = "400 Bad Request", + UNAUTHORIZED = "401 Unauthorized", + FORBIDDEN = "403 Forbidden", + NOT_FOUND = "404 Not Found", + METHOD_NOT_ALLOWED = "405 Method Not Allowed", + INTERNAL_SERVER_ERROR = "500 Internal Server Error", + SERVICE_UNAVAILABLE = "503 Service Unavailable", + ERROR = "Not Found", +} + +export interface KeyValue extends KeyWrapper, ValueWrapper {} \ No newline at end of file diff --git a/src/enum/httpResponseFormat.ts b/src/enum/httpResponseFormat.ts new file mode 100644 index 0000000..cefcfc0 --- /dev/null +++ b/src/enum/httpResponseFormat.ts @@ -0,0 +1,25 @@ +import type { HttpClientResponseInterface } from "src/Types/http-client"; + +export const success = (data: T): HttpClientResponseInterface => { + return { + status: "success", + isSuccessful: true, + message: "", + data, + }; +}; + +export const error = ( + error: string, + data?: T, + tabId: string = "", +): HttpClientResponseInterface => { + return { + status: "error", + isSuccessful: false, + message: error, + data, + }; +}; + + diff --git a/src/enum/testflow.enum.ts b/src/enum/testflow.enum.ts new file mode 100644 index 0000000..36ee6f3 --- /dev/null +++ b/src/enum/testflow.enum.ts @@ -0,0 +1,55 @@ +export enum BodyModeEnum { + "none" = "none", + "application/json" = "application/json", + "application/xml" = "application/xml", + "application/yaml" = "application/yaml", + "application/x-www-form-urlencoded" = "application/x-www-form-urlencoded", + "multipart/form-data" = "multipart/form-data", + "application/javascript" = "application/javascript", + "text/plain" = "text/plain", + "text/html" = "text/html", +} + +export enum AuthModeEnum { + "No Auth" = "No Auth", + "Inherit Auth" = "Inherit Auth", + "API Key" = "API Key", + "Bearer Token" = "Bearer Token", + "Basic Auth" = "Basic Auth", +} + + +export enum AddTo { + Header = "Header", + QueryParameter = "Query Parameter", +} + +export class Auth { + bearerToken?: string; + basicAuth?: { + username: string; + password: string; + }; + apiKey?: { + authKey: string; + authValue: string | unknown; + addTo: AddTo; + }; +} + +export enum WorkspaceUserAgentBaseEnum { + BROWSER_AGENT= "Browser Agent", + CLOUD_AGENT= "Cloud Agent" +} + +export type TFKeyValueStoreType = { + key: string; + value: string; + checked?: boolean; +}; + +export interface TFAPIResponseType { + body?: string; + headers?: object; + status?: string; +} \ No newline at end of file diff --git a/src/payloads/testflow.payload.ts b/src/payloads/testflow.payload.ts new file mode 100644 index 0000000..35e62d4 --- /dev/null +++ b/src/payloads/testflow.payload.ts @@ -0,0 +1,268 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { + ValidateNested, + IsArray, + IsNotEmptyObject, + IsString, + IsBoolean, + IsNotEmpty, + IsOptional, + IsEnum, + IsNumber, + IsDate, +} from 'class-validator'; +import { HTTPMethods } from "fastify"; +import { BodyModeEnum, WorkspaceUserAgentBaseEnum } from 'src/enum/testflow.enum'; +import { AuthModeEnum } from 'src/enum/testflow.enum'; +import { Auth } from 'src/enum/testflow.enum'; + + +export class KeyValue { + key: string; + value: string | unknown; + checked: boolean; +} + +export class VariableDto { + @IsString() + key: string; + + @IsString() + value: string; + + @IsBoolean() + @IsOptional() + checked?: boolean; +} + + +export class SparrowRequestBody { + raw?: string; + urlencoded?: KeyValue[]; + formdata?: FormData; +} + +export class RequestMetaData { + @ApiProperty({ example: 'put' }) + @IsNotEmpty() + method: HTTPMethods; + + @ApiProperty({ example: 'pet' }) + @IsString() + @IsNotEmpty() + name: string; + + @ApiProperty({ example: 'updatePet' }) + @IsString() + @IsOptional() + operationId?: string; + + @ApiProperty({ example: '/pet' }) + @IsString() + @IsNotEmpty() + url: string; + + @ApiProperty({ type: [SparrowRequestBody] }) + @Type(() => SparrowRequestBody) + @ValidateNested({ each: true }) + @IsOptional() + body?: SparrowRequestBody[]; + + @ApiProperty({ + enum: [ + 'application/json', + 'application/xml', + 'application/x-www-form-urlencoded', + 'multipart/form-data', + 'application/javascript', + 'text/plain', + 'text/html', + ], + }) + @IsEnum(BodyModeEnum) + @IsOptional() + selectedRequestBodyType?: BodyModeEnum; + + @ApiProperty({ + enum: AuthModeEnum, + }) + @IsEnum(AuthModeEnum) + @IsNotEmpty() + selectedRequestAuthType?: AuthModeEnum; + + @ApiProperty({ + example: { + name: 'search', + description: 'The search term to filter results', + required: false, + schema: {}, + }, + }) + @IsArray() + @Type(() => KeyValue) + @ValidateNested({ each: true }) + @IsOptional() + queryParams?: KeyValue[]; + + @ApiProperty({ + type: [KeyValue], + example: { + name: 'userID', + description: 'The unique identifier of the user', + required: true, + schema: {}, + }, + }) + @IsArray() + @Type(() => KeyValue) + @ValidateNested({ each: true }) + @IsOptional() + pathParams?: KeyValue[]; + + @ApiProperty({ + type: [KeyValue], + example: { + name: 'Authorization', + description: 'Bearer token for authentication', + }, + }) + @IsArray() + @Type(() => KeyValue) + @ValidateNested({ each: true }) + @IsOptional() + headers?: KeyValue[]; + + @ApiProperty({ + type: [Auth], + example: { + bearerToken: 'Bearer xyz', + }, + }) + @IsArray() + @Type(() => Auth) + @ValidateNested({ each: true }) + @IsOptional() + auth?: Auth[]; +} + +export class TestflowRunDto { + @IsArray() + @Type(() => TestflowNodes) + @ValidateNested({ each: true }) + @IsOptional() + nodes: TestflowNodes[]; + + @ApiProperty({ type: [VariableDto] }) + @IsArray() + @Type(() => VariableDto) + @ValidateNested({ each: true }) + variables: VariableDto[]; + + @IsString() + @IsOptional() + userId:string; + + @IsString() + @IsOptional() + selectedAgent:WorkspaceUserAgentBaseEnum +} + +export class NodeData { + @IsString() + @IsNotEmpty() + blockName: string; + + @IsString() + @IsNotEmpty() + @IsOptional() + requestId?: string; + + @IsString() + @IsNotEmpty() + @IsOptional() + folderId?: string; + + @IsString() + @IsNotEmpty() + @IsOptional() + collectionId?: string; + + @ApiProperty({ type: RequestMetaData }) + @IsOptional() + @Type(() => RequestMetaData) + requestData?: RequestMetaData; +} + +export class TestflowNodes { + @IsString() + @IsNotEmpty() + id: string; + + @IsString() + @IsNotEmpty() + type: string; + + @IsOptional() + position?: any; + + @Type(() => NodeData) + @IsOptional() + data?: NodeData; +} + +export class TestflowSchedularHistoryRequest { + @IsString() + @IsOptional() + method?: string; + + @IsString() + @IsOptional() + name?: string; + + @IsString() + @IsOptional() + status?: string; + + @IsString() + @IsOptional() + time: string; +} + +export class TestFlowSchedularRunHistory { + @IsString() + @IsNotEmpty() + failedRequests: number; + + @IsArray() + @IsOptional() + requests?: TestflowSchedularHistoryRequest[]; + + @IsString() + @IsNotEmpty() + status: string; + + @IsNumber() + @IsNotEmpty() + successRequests: number; + + @IsString() + @IsNotEmpty() + totalTime: string; + + @IsDate() + @IsOptional() + createdAt?: Date; + + @IsDate() + @IsOptional() + updatedAt?: Date; + + @IsString() + @IsOptional() + createdBy?: string; + + @IsString() + @IsOptional() + updatedBy?: string; +} \ No newline at end of file diff --git a/src/proxy/http/http.module.ts b/src/proxy/http/http.module.ts index 849420e..a73c5d0 100644 --- a/src/proxy/http/http.module.ts +++ b/src/proxy/http/http.module.ts @@ -7,5 +7,6 @@ import { HttpModule as NestHttpModule } from '@nestjs/axios'; imports: [NestHttpModule], controllers: [HttpController], providers: [HttpService], + exports: [HttpService], }) export class HttpModule {} diff --git a/src/proxy/proxy.module.ts b/src/proxy/proxy.module.ts index 3efbe93..b354624 100644 --- a/src/proxy/proxy.module.ts +++ b/src/proxy/proxy.module.ts @@ -3,8 +3,9 @@ import { SocketIoModule } from './socketio/socketio.module'; import { HttpModule } from './http/http.module'; import { WebSocketModule } from './websocket/websocket.module'; import { GraphqlModule } from "./graphql/graphql.module"; +import { TestflowModule } from './testflow/testflow.module'; @Module({ - imports: [HttpModule, SocketIoModule, WebSocketModule, GraphqlModule], + imports: [HttpModule, SocketIoModule, WebSocketModule, GraphqlModule,TestflowModule], }) export class ProxyModule {} diff --git a/src/proxy/testflow/testflow.controller.ts b/src/proxy/testflow/testflow.controller.ts new file mode 100644 index 0000000..56918fa --- /dev/null +++ b/src/proxy/testflow/testflow.controller.ts @@ -0,0 +1,36 @@ +import { + Controller, + Post, + Body, + Req, + Res, + HttpException, + HttpStatus +} from '@nestjs/common'; +import { Request, Response } from 'express'; +import { TestflowService } from './testflow.service'; +import { TestflowRunDto } from 'src/payloads/testflow.payload'; + +@Controller('proxy/testflow') +export class TestflowController { + constructor( + private readonly testflowService: TestflowService, + ) {} + + @Post('/execute') + async testflowRun( + @Body() payload: TestflowRunDto, + @Req() req: Request, + @Res() res: Response, + ) { + try { + const result = await this.testflowService.runTestflow(payload); + return res.status(200).send(result); + } catch (error: any) { + throw new HttpException( + error?.message || 'Failed to run testflow', + HttpStatus.BAD_GATEWAY, + ); + } + } +} diff --git a/src/proxy/testflow/testflow.module.ts b/src/proxy/testflow/testflow.module.ts new file mode 100644 index 0000000..47a0cc9 --- /dev/null +++ b/src/proxy/testflow/testflow.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { TestflowController } from './testflow.controller'; +import { TestflowService } from './testflow.service'; +import { HttpModule } from '../http/http.module'; +import { DecodeTestflow } from 'src/utils/decode-testflow'; + +@Module({ + imports: [HttpModule], + controllers: [TestflowController], + providers: [TestflowService, DecodeTestflow], +}) +export class TestflowModule {} diff --git a/src/proxy/testflow/testflow.service.ts b/src/proxy/testflow/testflow.service.ts new file mode 100644 index 0000000..d34921a --- /dev/null +++ b/src/proxy/testflow/testflow.service.ts @@ -0,0 +1,415 @@ +import { Injectable } from '@nestjs/common'; +import { TestflowRunDto, TestFlowSchedularRunHistory } from 'src/payloads/testflow.payload'; +import { HttpService } from '../http/http.service'; +import { WorkspaceUserAgentBaseEnum } from 'src/enum/testflow.enum'; +import { Base64Converter } from 'src/utils/base64Converter'; +import { Logger } from '@nestjs/common'; +import axios from "axios"; +import { success,error } from 'src/enum/httpResponseFormat'; +import { StatusCode } from 'src/utils/status-code'; +import { DecodeTestflow, RequestData } from 'src/utils/decode-testflow'; +import { ParseTime } from 'src/utils/parse-time'; +import { TFAPIResponseType } from 'src/enum/testflow.enum'; +import { TFKeyValueStoreType } from 'src/enum/testflow.enum'; +import { ResponseStatusCode } from 'src/enum/httpRequest.enum'; + +@Injectable() +export class TestflowService { + private readonly logger = new Logger(TestflowService.name); + private _decodeRequest = new DecodeTestflow(); + constructor( + private readonly httpService: HttpService, + ) {} + + async makeRequest( + url: string, + method: string, + headers: string, + body: string, + contentType: string, + selectedAgent: WorkspaceUserAgentBaseEnum, + signal?: AbortSignal, + ) { + const startTime = performance.now(); + try { + let response; + // Cloud Agent - call makeHttpRequest directly + if (selectedAgent === "Cloud Agent") { + try { + const cloudResponse = await this.httpService.makeHttpRequest({ + url, + method, + headers, + body, + contentType, + }); + return success({ + body: cloudResponse.data, + status: cloudResponse.status, + headers: cloudResponse.headers, + }); + } catch (cloudError) { + return error(cloudError.message || "Cloud agent request failed"); + } + } else { + try { + let jsonHeader; + try { + jsonHeader = JSON.parse(headers); + console.log("[makeHttpRequestV2] parsed headers", jsonHeader); + } catch { + console.warn("[makeHttpRequestV2] failed to parse headers, using []"); + jsonHeader = []; + } + const headersObject = jsonHeader.reduce( + ( + acc: Record, + header: { key: string; value: string }, + ) => { + acc[header.key] = header.value; + return acc; + }, + {}, + ); + let requestData = body || {}; + console.log("[makeHttpRequestV2] initial requestData", requestData); + if (contentType === "multipart/form-data") { + console.log("[makeHttpRequestV2] handling multipart/form-data"); + const formData = new FormData(); + const parsedBody = JSON.parse(body); + for (const field of parsedBody || []) { + try { + if (field?.base) { + const file = await new Base64Converter().base64ToFile( + field.base, + field.value, + ); + formData.append(field.key, file); + } else { + formData.append(field.key, field.value); + } + } catch (e) { + console.error("[makeHttpRequestV2] formData field error", e); + formData.append(field.key, field.value); + } + } + requestData = formData; + delete headersObject["Content-Type"]; // let axios set boundary + } else if (contentType === "application/x-www-form-urlencoded") { + console.log("[makeHttpRequestV2] handling urlencoded body"); + const urlSearchParams = new URLSearchParams(); + const parsedBody = JSON.parse(body); + (parsedBody || []).forEach( + (field: { key: string; value: string }) => { + urlSearchParams.append(field.key, field.value); + }, + ); + requestData = urlSearchParams; + } else if ( + contentType === "application/json" || + contentType === "text/plain" + ) { + headersObject["Content-Type"] = contentType; + } + const axiosResponse = await Promise.race([ + axios({ + method, + url, + data: requestData || {}, + headers: { ...headersObject }, + responseType: "arraybuffer", + validateStatus: () => true, + }), + this.waitForAbort(signal), + ]); + if (signal?.aborted) { + console.warn("[makeHttpRequestV2] request was aborted"); + throw new DOMException("Request was aborted", "AbortError"); + } + let responseData = ""; + const responseContentType = axiosResponse.headers["content-type"] || ""; + if (responseContentType.startsWith("image/")) { + console.log("[makeHttpRequestV2] handling image response"); + const base64 = btoa( + new Uint8Array(axiosResponse.data).reduce( + (data, byte) => data + String.fromCharCode(byte), + "", + ), + ); + responseData = `data:${responseContentType};base64,${base64}`; + } else { + responseData = new TextDecoder("utf-8").decode(axiosResponse.data); + } + const status = `${axiosResponse.status} ${ + axiosResponse.statusText || + new StatusCode().getText(axiosResponse.status) + }`; + return success({ + body: responseData, + status: status, + headers: Object.fromEntries( + Object.entries(axiosResponse.headers), + ), + }); + } catch (axiosError: any) { + console.error("[makeHttpRequestV2] axios error", axiosError); + if (signal?.aborted) { + throw new DOMException("Request was aborted", "AbortError"); + } + return error(axiosError.message || "Browser agent request failed"); + } + } + } catch (e) { + if (signal?.aborted) { + console.warn("[makeHttpRequestV2] aborted at outer catch"); + throw new DOMException("Request was aborted", "AbortError"); + } + console.error("[makeHttpRequestV2] request error", e); + return error(String(e)); + } + } + + async waitForAbort(signal: AbortSignal): Promise { + return new Promise((_, reject) => { + if (signal?.aborted) { + return reject(new Error("Aborted before starting")); + } + signal?.addEventListener( + "abort", + () => { + reject(new Error("Aborted during request")); + }, + { once: true }, + ); + }); + } + + async runTestflow(payload: TestflowRunDto) { + const { nodes, variables } = payload; + const abortController = new AbortController(); + const { signal } = abortController; + let successRequests = 0; + let failedRequests = 0; + let totalTime = 0; + const history: TestFlowSchedularRunHistory = { + status: "fail", + successRequests: 0, + failedRequests: 0, + totalTime: "", + createdAt: new Date(), + createdBy: payload.userId, + requests: [], + }; + + let requestChainResponse: Record = {}; + const executedNodes: any[] = []; + const environmentVariables = variables || []; + for (const element of nodes) { + if (element?.type !== "requestBlock" || !element?.data?.requestData) { + continue; + } + const requestData: RequestData = element.data.requestData as RequestData; + try { + // Decode request + const decodeData = this._decodeRequest.init( + requestData, + environmentVariables.filter( + (env: { key: string; value: string; checked: boolean }) => + env.key?.trim() && env.value?.trim(), + ), + requestChainResponse, + ); + const [url, method, headers, body, contentType] = decodeData; + const start = Date.now(); + let resData: any; + try { + console.log("🌐 Sending request:", { url, method, headers, body, contentType }); + const response: any = await this.makeRequest( + url, + method, + headers, + body, + contentType, + payload.selectedAgent, + signal, + ); + const duration = Date.now() - start; + if (response.isSuccessful) { + const byteLength = new TextEncoder().encode( + JSON.stringify(response), + ).length; + const responseSizeKB = byteLength / 1024; + const responseData: TFAPIResponseType = response.data; + const responseBody = responseData.body; + const formattedHeaders = Object.entries( + response?.data?.headers || {}, + ).map(([key, value]) => ({ + key, + value: String(value), + })) as TFKeyValueStoreType[]; + const responseStatus = response?.data?.status; + resData = { + body: responseBody, + headers: formattedHeaders, + status: responseStatus, + time: duration, + size: responseSizeKB, + responseContentType: + this._decodeRequest.setResponseContentType(formattedHeaders), + }; + const statusCode = Number(resData.status.split(" ")[0]); + if (statusCode >= 200 && statusCode < 300) { + successRequests++; + } else { + failedRequests++; + } + totalTime += duration; + history.requests.push({ + method: requestData.method as string, + name: requestData.name as string, + status: resData.status, + time: new ParseTime().convertMilliseconds(duration), + }); + // Build chaining object + const responseHeader = + this._decodeRequest.setResponseContentType(formattedHeaders); + const reqParam: Record = {}; + const params = new URL(url).searchParams; + for (const [key, value] of params.entries()) { + reqParam[key] = value; + } + const parsedHeaders = JSON.parse(headers) as { + key: string; + value: string; + }[]; + const headersObject = Object.fromEntries( + parsedHeaders.map(({ key, value }) => [key, value]), + ); + let reqBody: any; + if (contentType === "application/json") { + try { + reqBody = JSON.parse(body); + } catch { + reqBody = {}; + } + } else if ( + contentType === "multipart/form-data" || + contentType === "application/x-www-form-urlencoded" + ) { + try { + const parsedBody = JSON.parse(body) as { + key: string; + value: string; + }[]; + reqBody = Object.fromEntries( + parsedBody.map(({ key, value }) => [key, value]), + ); + } catch { + reqBody = {}; + } + } else { + reqBody = body; + } + const responseObject = { + response: { + body: + responseHeader === "JSON" + ? JSON.parse(resData.body) + : resData.body, + headers: response?.data?.headers, + }, + request: { + headers: headersObject || {}, + body: reqBody, + parameters: reqParam || {}, + }, + }; + const sanitizedRequestName = requestData.name.replace( + /[^a-zA-Z0-9_]/g, + "_", + ); + const sanitizedBlockName = ( + element.data.blockName || element.id + ).replace(/[^a-zA-Z0-9_]/g, "_"); + requestChainResponse[`$$${sanitizedRequestName}`] = responseObject; + requestChainResponse[`$$${sanitizedBlockName}`] = responseObject; + } else { + resData = { + body: response.message || "Request failed", + headers: [], + status: ResponseStatusCode.ERROR, + time: duration, + size: 0, + }; + failedRequests++; + totalTime += duration; + history.requests.push({ + method: requestData.method as string, + name: requestData.name as string, + status: ResponseStatusCode.ERROR, + time: new ParseTime().convertMilliseconds(duration), + }); + } + } catch (error) { + const duration = Date.now() - start; + if (error?.name === "AbortError") { + console.warn("🛑 Request aborted, breaking loop"); + break; + } + resData = { + body: error?.message || "Request failed", + headers: [], + status: ResponseStatusCode.ERROR, + time: duration, + size: 0, + }; + failedRequests++; + totalTime += duration; + history.requests.push({ + method: requestData.method as string, + name: requestData.name as string, + status: ResponseStatusCode.ERROR, + time: new ParseTime().convertMilliseconds(duration), + }); + } + executedNodes.push({ + id: element.id, + response: resData, + request: requestData, + }); + } catch (error) { + failedRequests++; + history.requests.push({ + method: requestData?.method || "UNKNOWN", + name: requestData?.name || "Unknown Request", + status: ResponseStatusCode.ERROR, + time: "0 ms", + }); + executedNodes.push({ + id: element.id, + response: { + body: error?.message || "Processing failed", + headers: [], + status: ResponseStatusCode.ERROR, + time: 0, + size: 0, + }, + request: requestData, + }); + } + } + + // Finalize history + history.totalTime = new ParseTime().convertMilliseconds(totalTime); + history.successRequests = successRequests; + history.failedRequests = failedRequests; + history.status = failedRequests === 0 ? "pass" : "fail"; + console.log("Final history:", history); + return { + history, + requestChainResponse, + nodes: executedNodes, + }; + } +} + diff --git a/src/utils/base64Converter.ts b/src/utils/base64Converter.ts new file mode 100644 index 0000000..e079407 --- /dev/null +++ b/src/utils/base64Converter.ts @@ -0,0 +1,50 @@ +export class Base64Converter { + /** + * Converts a File object to a Base64 string. + * @param file - The File object to convert. + * @returns A Promise that resolves to the Base64 string. + */ + public fileToBase64(file: File): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + + reader.onload = () => { + if (reader.result) { + resolve(reader.result as string); + } else { + reject(new Error("Failed to convert file to Base64")); + } + }; + + reader.onerror = () => { + reject(new Error("Error reading file")); + }; + + reader.readAsDataURL(file); + }); + } + + /** + * Converts a Base64 string back to a File object. + * Extracts the MIME type automatically from the string. + * @param base64 - The Base64 string. + * @param fileName - The name for the new file. + * @returns A File object. + */ + public base64ToFile(base64: string, fileName: string): File { + const [metadata, data] = base64.split(","); + const mimeType = + metadata.match(/data:(.*?);base64/)?.[1] || "application/octet-stream"; + const byteString = atob(data); + const byteNumbers = new Array(byteString.length); + + for (let i = 0; i < byteString.length; i++) { + byteNumbers[i] = byteString.charCodeAt(i); + } + + const byteArray = new Uint8Array(byteNumbers); + const blob = new Blob([byteArray], { type: mimeType }); + + return new File([blob], fileName, { type: mimeType }); + } +} diff --git a/src/utils/decode-testflow.ts b/src/utils/decode-testflow.ts new file mode 100644 index 0000000..db914b7 --- /dev/null +++ b/src/utils/decode-testflow.ts @@ -0,0 +1,543 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import JSON5 from "json5" +import type { KeyValue} from "src/enum/httpRequest.enum"; +import { RequestDataTypeEnum } from "src/enum/httpRequest.enum"; + +/** + * Updated RequestData interface for the new structure + */ +interface RequestData { + headers?: Array<{ key: string; value: string; checked: boolean }>; + queryParams?: Array<{ key: string; value: string; checked: boolean }>; + body?: { + raw?: string; + urlencoded?: Array<{ key: string; value: string; checked: boolean }>; + formdata?: { + text?: Array<{ key: string; value: string; checked: boolean }>; + file?: Array<{ + key: string; + value?: string; + base?: string; + checked?: boolean; + }>; + }; + }; + auth?: { + bearerToken?: string; + basicAuth?: { + username: string; + password: string; + }; + apiKey?: { + authKey: string; + authValue: string; + addTo: string; + }; + }; + url: string; + method: string; + name: string; + selectedRequestBodyType?: string; + selectedRequestAuthType?: string; +} + +/** + * DecodeTestflow - Updated for new RequestData structure + * + * Parses requests (URL, headers, body, content-type) for testflow execution. + * Designed for Node/Next.js backend. Uses JSON5 for permissive JSON parsing of raw bodies. + */ +class DecodeTestflow { + constructor() {} + + /** + * Determine response content type enum from response headers. + */ + public setResponseContentType = ( + responseHeaders: KeyValue[] | undefined, + ): RequestDataTypeEnum => { + if (!responseHeaders) return RequestDataTypeEnum.TEXT; + + for (let i = 0; i < responseHeaders.length; i++) { + const key = (responseHeaders[i].key || "").toLowerCase(); + const value = String(responseHeaders[i].value || ""); + if (key === "content-type") { + if (value.includes("text/html")) return RequestDataTypeEnum.HTML; + if ( + value.includes("application/json") || + value.includes("application/hal+json") + ) + return RequestDataTypeEnum.JSON; + if (value.includes("application/xml")) return RequestDataTypeEnum.XML; + if (value.includes("application/javascript")) + return RequestDataTypeEnum.JAVASCRIPT; + if (value.startsWith("image/")) return RequestDataTypeEnum.IMAGE; + return RequestDataTypeEnum.TEXT; + } + } + return RequestDataTypeEnum.TEXT; + }; + + /** + * Ensure URL has protocol; handles protocol-relative URLs. + */ + private ensureHttpOrHttps = (str: string): string => { + if (!str) return "http://"; + if (str.startsWith("http://") || str.startsWith("https://")) { + return ""; + } else if (str.startsWith("//")) { + return "http:"; + } else { + return "http://"; + } + }; + + /** + * Return only checked KeyValue entries (preserves order). + */ + private extractKeyValue = ( + pairs?: Array<{ key: string; value: string; checked: boolean }>, + ): KeyValue[] => { + if (!Array.isArray(pairs)) return []; + const checkedPairs: KeyValue[] = []; + for (const pair of pairs) { + if (pair && pair.checked && pair.key) { + checkedPairs.push({ key: pair.key, value: String(pair.value || "") }); + } + } + return checkedPairs; + }; + + /** + * Build URL with query parameters and authentication parameters (if any). + */ + private extractURL = ( + requestData: RequestData, + environmentVariables: any[] = [], + previousResponse?: any, + ): string => { + let url = (requestData.url || "").trim(); + // Replace environment variables in URL + url = this.setEnvironmentVariables(url, environmentVariables); + url = this.setDynamicExpression(url, previousResponse); + // Add query parameters + const queryParams = this.extractKeyValue(requestData.queryParams); + if (queryParams.length > 0) { + const processedParams = queryParams.map((param) => ({ + key: this.setEnvironmentVariables(param.key, environmentVariables), + value: this.setEnvironmentVariables( + String(param.value), + environmentVariables, + ), + })); + const queryString = processedParams + .map( + (param) => + `${encodeURIComponent(param.key)}=${encodeURIComponent(param.value)}`, + ) + .join("&"); + const hasQuery = url.includes("?"); + url = `${url}${hasQuery ? "&" : "?"}${queryString}`; + } + + // Handle API Key in query parameters + if ( + requestData.selectedRequestAuthType === "API Key" && + requestData.auth?.apiKey?.addTo === "Query" && + requestData.auth?.apiKey?.authKey && + requestData.auth?.apiKey?.authValue + ) { + const processedKey = this.setEnvironmentVariables( + requestData.auth.apiKey.authKey, + environmentVariables, + ); + const processedValue = this.setEnvironmentVariables( + requestData.auth.apiKey.authValue, + environmentVariables, + ); + + const hasQuery = url.includes("?"); + url = `${url}${hasQuery ? "&" : "?"}${encodeURIComponent(processedKey)}=${encodeURIComponent(processedValue)}`; + } + + return this.ensureHttpOrHttps(url) + url; + }; + + /** + * Process authentication and return auth header if needed. + */ + private processAuthentication = ( + requestData: RequestData, + environmentVariables: any[] = [], + ): KeyValue | null => { + const authType = requestData.selectedRequestAuthType; + const auth = requestData.auth; + if (!auth) return null; + switch (authType) { + case "Bearer Token": + if (auth.bearerToken) { + const processedToken = this.setEnvironmentVariables( + auth.bearerToken, + environmentVariables, + ); + return { + key: "Authorization", + value: `Bearer ${processedToken}`, + }; + } + break; + + case "Basic Auth": + if (auth.basicAuth?.username && auth.basicAuth?.password) { + const processedUsername = this.setEnvironmentVariables( + auth.basicAuth.username, + environmentVariables, + ); + const processedPassword = this.setEnvironmentVariables( + auth.basicAuth.password, + environmentVariables, + ); + const credentials = Buffer.from( + `${processedUsername}:${processedPassword}`, + ).toString("base64"); + return { + key: "Authorization", + value: `Basic ${credentials}`, + }; + } + break; + + case "API Key": + if ( + auth.apiKey?.addTo === "Header" && + auth.apiKey?.authKey && + auth.apiKey?.authValue + ) { + const processedKey = this.setEnvironmentVariables( + auth.apiKey.authKey, + environmentVariables, + ); + const processedValue = this.setEnvironmentVariables( + auth.apiKey.authValue, + environmentVariables, + ); + return { + key: processedKey, + value: processedValue, + }; + } + break; + + case "No Auth": + default: + return null; + } + + return null; + }; + + /** + * Extract and process headers, including authentication headers. + */ + private extractHeaders = ( + requestData: RequestData, + environmentVariables: any[] = [], + previousResponse?: any, + ): string => { + // Get regular headers + const headers = this.extractKeyValue(requestData.headers); + // Process environment variables in headers + const processedHeaders = headers.map((header) => ({ + key: this.setEnvironmentVariables(header.key, environmentVariables), + value: this.setEnvironmentVariables( + String(header.value), + environmentVariables, + ), + })); + // Get authentication header + const authHeader = this.processAuthentication( + requestData, + environmentVariables, + ); + if (authHeader) { + processedHeaders.unshift(authHeader); + } + // Remove duplicates (keep first occurrence) and filter empty keys + const uniqueHeaders = new Map(); + for (const header of processedHeaders) { + const key = header.key.toLowerCase(); + if (key && !uniqueHeaders.has(key) && key !== "content-length") { + uniqueHeaders.set(key, header.value); + } + } + + // Convert back to array format + const result = Array.from(uniqueHeaders.entries()).map(([key, value]) => ({ + key, + value, + })); + + // Apply dynamic expressions + const jsonString = JSON.stringify(result); + const processed = this.setDynamicExpression(jsonString, previousResponse); + + return processed; + }; + + /** + * Replace environment variables in the text. Uses {{KEY}} syntax. + */ + public setEnvironmentVariables = ( + text: string, + environmentVariables: any[] = [], + ): string => { + if (typeof text !== "string") return String(text || ""); + let updatedText = text.replace( + /\[\*\$\[(.*?)\]\$\*\]/gs, + (_, squareContent) => { + const updated = squareContent + .replace(/\\/g, "") + .replace(/"/g, `'`) + .replace(/\{\{(.*?)\}\}/g, (_: any, inner: string) => { + return `'{{${inner.trim()}}}'`; + }); + return `[*$[${updated}]$*]`; + }, + ); + + if (!Array.isArray(environmentVariables)) environmentVariables = []; + + for (const element of environmentVariables) { + if (!element || typeof element.key !== "string" || !element.checked) + continue; + const keyEscaped = element.key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const regex = new RegExp(`{{(${keyEscaped})}}`, "g"); + updatedText = updatedText.replace(regex, String(element.value ?? "")); + } + + return updatedText; + }; + + /** + * Evaluate dynamic expressions inside the special wrapper [*$[ ... ]$*] + */ + private setDynamicExpression = (text: string, response: any): string => { + if (!text || typeof text !== "string") return String(text || ""); + const result = text.replace(/\[\*\$\[(.*?)\]\$\*\]/gs, (_, expr) => { + try { + const de = expr.replace(/'\{\{(.*?)\}\}'/g, "undefined"); + const fn = new Function( + "response", + `with (response) { return (${de}); }`, + ); + const s = fn(response); + if (typeof s === "string") { + return s + .replace(/\n/g, "") + .replace(/\\/g, "\\\\") + .replace(/\"/g, '\\"') + .replace(/\t/g, "\\t"); + } else { + return String(s); + } + } catch (e: any) { + return ""; + } + }); + return result; + }; + + /** + * Similar to setDynamicExpression but returns properly quoted JSON-safe values. + */ + public setDynamicExpression2 = (text: string, response: any): string => { + if (!text || typeof text !== "string") return String(text || ""); + const result = text.replace(/\[\*\$\[(.*?)\]\$\*\]/gs, (_, expr) => { + try { + const de = expr.replace(/'\{\{(.*?)\}\}'/g, "undefined"); + const fn = new Function( + "response", + `with (response) { return (${de}); }`, + ); + const s = fn(response); + if (typeof s === "string") return `"${s}"`; + if (typeof s === "object" && s !== null) return `${JSON.stringify(s)}`; + return String(s); + } catch (e: any) { + return ""; + } + }); + return result; + }; + + /** + * Entry point - returns [url, method, headers, body, contentType] + * Updated to work with new RequestData structure + */ + public init( + requestData: RequestData, + environmentVariables: any[] = [], + previousResponse?: any, + ): string[] { + const url = this.extractURL( + requestData, + environmentVariables, + previousResponse, + ); + const method = (requestData.method || "GET").toUpperCase(); + const headers = this.extractHeaders( + requestData, + environmentVariables, + previousResponse, + ); + const body = this.extractBody( + requestData, + environmentVariables, + previousResponse, + ); + const contentType = this.extractDataType(requestData); + return [url, method, headers, body, contentType]; + } + + private mapToBodyType = (selectedBodyType: string): string => { + switch (selectedBodyType) { + case "application/json": + case "application/xml": + case "text/plain": + case "text/html": + case "raw": + return "raw"; + case "application/x-www-form-urlencoded": + case "urlencoded": + return "urlencoded"; + case "multipart/form-data": + case "formdata": + return "formdata"; + case "none": + default: + return "none"; + } + }; + + /** + * Extract and format the request body based on body type. + */ + private extractBody = ( + requestData: RequestData, + environmentVariables: any[] = [], + previousResponse?: any, + ): string => { + const bodyType = this.mapToBodyType( + requestData.selectedRequestBodyType || "", + ); + const body = requestData.body; + + if (!body) return ""; + + switch (bodyType) { + case "raw": + const rawText = this.setEnvironmentVariables( + body.raw || "", + environmentVariables, + ); + if (!rawText || rawText.trim() === "") return "{}"; + + const evaluated = this.setDynamicExpression2(rawText, previousResponse); + if (evaluated === "") return "{}"; + + // Try to parse as JSON for formatting + try { + const parsed = JSON5.parse(evaluated); + return JSON.stringify(parsed, null, 2); + } catch { + return evaluated; + } + + case "urlencoded": + const urlencodedData = this.extractKeyValue(body.urlencoded); + const processedUrlencoded = urlencodedData.map((item) => ({ + key: this.setEnvironmentVariables(item.key, environmentVariables), + value: this.setEnvironmentVariables( + String(item.value), + environmentVariables, + ), + })); + + const urlEncodedJson = JSON.stringify(processedUrlencoded); + return this.setDynamicExpression(urlEncodedJson, previousResponse); + + case "formdata": + const formDataItems: any[] = []; + + // Add text fields + if (body.formdata?.text) { + const textFields = this.extractKeyValue(body.formdata.text); + textFields.forEach((field) => { + formDataItems.push({ + key: this.setEnvironmentVariables( + field.key, + environmentVariables, + ), + value: this.setEnvironmentVariables( + String(field.value), + environmentVariables, + ), + type: "text", + }); + }); + } + + // Add file fields + if (body.formdata?.file) { + body.formdata.file.forEach((field) => { + if (field.checked !== false) { + formDataItems.push({ + key: this.setEnvironmentVariables( + field.key || "", + environmentVariables, + ), + value: field.value || "", + base: field.base || "", + type: "file", + }); + } + }); + } + + return this.setDynamicExpression( + JSON.stringify(formDataItems), + previousResponse, + ); + + case "none": + default: + return ""; + } + }; + + /** + * Map body type to content type - now handles both body types and content types + */ + private extractDataType = (requestData: RequestData): string => { + const selectedType = requestData.selectedRequestBodyType; + // If it's already a content type, return it + if (selectedType?.includes("/")) { + return selectedType; + } + // Otherwise map from body type to content type + const bodyType = this.mapToBodyType(selectedType || ""); + switch (bodyType) { + case "raw": + return "application/json"; + case "urlencoded": + return "application/x-www-form-urlencoded"; + case "formdata": + return "multipart/form-data"; + case "none": + default: + return "text/plain"; + } + }; +} + +export { DecodeTestflow, type RequestData }; diff --git a/src/utils/parse-time.ts b/src/utils/parse-time.ts new file mode 100644 index 0000000..0976996 --- /dev/null +++ b/src/utils/parse-time.ts @@ -0,0 +1,17 @@ +export class ParseTime { + public convertMilliseconds(ms: number): string { + const seconds = ms / 1000; + const minutes = seconds / 60; + const hours = minutes / 60; + + if (hours >= 1) { + return `${hours.toFixed(2)} hr`; + } else if (minutes >= 1) { + return `${minutes.toFixed(2)} min`; + } else if (seconds >= 1) { + return `${seconds.toFixed(2)} sec`; + } else { + return `${ms} ms`; + } + } +} diff --git a/src/utils/status-code.ts b/src/utils/status-code.ts new file mode 100644 index 0000000..0b10646 --- /dev/null +++ b/src/utils/status-code.ts @@ -0,0 +1,80 @@ +export class StatusCode { + private statusMessages: Record = { + // 1xx Informational + 100: "Continue", + 101: "Switching Protocols", + 102: "Processing", + 103: "Early Hints", + + // 2xx Success + 200: "OK", + 201: "Created", + 202: "Accepted", + 203: "Non-Authoritative Information", + 204: "No Content", + 205: "Reset Content", + 206: "Partial Content", + 207: "Multi-Status", + 208: "Already Reported", + 226: "IM Used", + + // 3xx Redirection + 300: "Multiple Choices", + 301: "Moved Permanently", + 302: "Found", + 303: "See Other", + 304: "Not Modified", + 305: "Use Proxy", + 307: "Temporary Redirect", + 308: "Permanent Redirect", + + // 4xx Client Errors + 400: "Bad Request", + 401: "Unauthorized", + 402: "Payment Required", + 403: "Forbidden", + 404: "Not Found", + 405: "Method Not Allowed", + 406: "Not Acceptable", + 407: "Proxy Authentication Required", + 408: "Request Timeout", + 409: "Conflict", + 410: "Gone", + 411: "Length Required", + 412: "Precondition Failed", + 413: "Payload Too Large", + 414: "URI Too Long", + 415: "Unsupported Media Type", + 416: "Range Not Satisfiable", + 417: "Expectation Failed", + 418: "I'm a teapot", + 421: "Misdirected Request", + 422: "Unprocessable Entity", + 423: "Locked", + 424: "Failed Dependency", + 425: "Too Early", + 426: "Upgrade Required", + 428: "Precondition Required", + 429: "Too Many Requests", + 431: "Request Header Fields Too Large", + 451: "Unavailable For Legal Reasons", + + // 5xx Server Errors + 500: "Internal Server Error", + 501: "Not Implemented", + 502: "Bad Gateway", + 503: "Service Unavailable", + 504: "Gateway Timeout", + 505: "HTTP Version Not Supported", + 506: "Variant Also Negotiates", + 507: "Insufficient Storage", + 508: "Loop Detected", + 510: "Not Extended", + 511: "Network Authentication Required", + }; + + public getText(status: number): string { + return this.statusMessages[status] || "Unknown Status"; + } + } + \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 2660054..f924998 100644 --- a/yarn.lock +++ b/yarn.lock @@ -352,6 +352,101 @@ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.1.tgz#de633db3ec2ef6a3c89e2f19038063e8a122e2c2" integrity sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q== +"@fastify/ajv-compiler@^3.5.0": + version "3.6.0" + resolved "https://registry.yarnpkg.com/@fastify/ajv-compiler/-/ajv-compiler-3.6.0.tgz#907497a0e62a42b106ce16e279cf5788848e8e79" + integrity sha512-LwdXQJjmMD+GwLOkP7TVC68qa+pSSogeWWmznRJ/coyTcfe9qA05AHFSe1eZFwK6q+xVRpChnvFUkf1iYaSZsQ== + dependencies: + ajv "^8.11.0" + ajv-formats "^2.1.1" + fast-uri "^2.0.0" + +"@fastify/ajv-compiler@^4.0.0": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@fastify/ajv-compiler/-/ajv-compiler-4.0.2.tgz#da05938cf852901bfb953738764f553b5449b80b" + integrity sha512-Rkiu/8wIjpsf46Rr+Fitd3HRP+VsxUFDDeag0hs9L0ksfnwx2g7SPQQTFL0E8Qv+rfXzQOxBJnjUB9ITUDjfWQ== + dependencies: + ajv "^8.12.0" + ajv-formats "^3.0.1" + fast-uri "^3.0.0" + +"@fastify/cors@11.1.0": + version "11.1.0" + resolved "https://registry.yarnpkg.com/@fastify/cors/-/cors-11.1.0.tgz#09f79748f08f147d19cfc3f1807b59791bc77cf0" + integrity sha512-sUw8ed8wP2SouWZTIbA7V2OQtMNpLj2W6qJOYhNdcmINTu6gsxVYXjQiM9mdi8UUDlcoDDJ/W2syPo1WB2QjYA== + dependencies: + fastify-plugin "^5.0.0" + toad-cache "^3.7.0" + +"@fastify/error@^3.3.0", "@fastify/error@^3.4.0": + version "3.4.1" + resolved "https://registry.yarnpkg.com/@fastify/error/-/error-3.4.1.tgz#b14bb4cac3dd4ec614becbc643d1511331a6425c" + integrity sha512-wWSvph+29GR783IhmvdwWnN4bUxTD01Vm5Xad4i7i1VuAOItLvbPAb69sb0IQ2N57yprvhNIwAP5B6xfKTmjmQ== + +"@fastify/error@^4.0.0": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@fastify/error/-/error-4.2.0.tgz#d40f46ba75f541fdcc4dc276b7308bbc8e8e6d7a" + integrity sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ== + +"@fastify/fast-json-stringify-compiler@^4.3.0": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-4.3.0.tgz#5df89fa4d1592cbb8780f78998355feb471646d5" + integrity sha512-aZAXGYo6m22Fk1zZzEUKBvut/CIIQe/BapEORnxiD5Qr0kPHqqI69NtEMCme74h+at72sPhbkb4ZrLd1W3KRLA== + dependencies: + fast-json-stringify "^5.7.0" + +"@fastify/fast-json-stringify-compiler@^5.0.0": + version "5.0.3" + resolved "https://registry.yarnpkg.com/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-5.0.3.tgz#fae495bf30dbbd029139839ec5c2ea111bde7d3f" + integrity sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ== + dependencies: + fast-json-stringify "^6.0.0" + +"@fastify/formbody@8.0.2": + version "8.0.2" + resolved "https://registry.yarnpkg.com/@fastify/formbody/-/formbody-8.0.2.tgz#7f97c8ab25933db77760bbeaacd2ff5355a54682" + integrity sha512-84v5J2KrkXzjgBpYnaNRPqwgMsmY7ZDjuj0YVuMR3NXCJRCgKEZy/taSP1wUYGn0onfxJpLyRGDLa+NMaDJtnA== + dependencies: + fast-querystring "^1.1.2" + fastify-plugin "^5.0.0" + +"@fastify/forwarded@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@fastify/forwarded/-/forwarded-3.0.0.tgz#0fc96cdbbb5a38ad453d2d5533a34f09b4949b37" + integrity sha512-kJExsp4JCms7ipzg7SJ3y8DwmePaELHxKYtg+tZow+k0znUTf3cb+npgyqm8+ATZOdmfgfydIebPDWM172wfyA== + +"@fastify/merge-json-schemas@^0.1.0": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@fastify/merge-json-schemas/-/merge-json-schemas-0.1.1.tgz#3551857b8a17a24e8c799e9f51795edb07baa0bc" + integrity sha512-fERDVz7topgNjtXsJTTW1JKLy0rhuLRcquYqNR9rF7OcVpCa2OVW49ZPDIhaRRCaUuvVxI+N416xUoF76HNSXA== + dependencies: + fast-deep-equal "^3.1.3" + +"@fastify/merge-json-schemas@^0.2.0": + version "0.2.1" + resolved "https://registry.yarnpkg.com/@fastify/merge-json-schemas/-/merge-json-schemas-0.2.1.tgz#3aa30d2f0c81a8ac5995b6d94ed4eaa2c3055824" + integrity sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A== + dependencies: + dequal "^2.0.3" + +"@fastify/middie@9.0.3": + version "9.0.3" + resolved "https://registry.yarnpkg.com/@fastify/middie/-/middie-9.0.3.tgz#7d8edfd90dca9ff8e82dc721a460ff3d8646f230" + integrity sha512-7OYovKXp9UKYeVMcjcFLMcSpoMkmcZmfnG+eAvtdiatN35W7c+r9y1dRfpA+pfFVNuHGGqI3W+vDTmjvcfLcMA== + dependencies: + "@fastify/error" "^4.0.0" + fastify-plugin "^5.0.0" + path-to-regexp "^8.1.0" + reusify "^1.0.4" + +"@fastify/proxy-addr@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@fastify/proxy-addr/-/proxy-addr-5.0.0.tgz#e9d1c7a49b8380d9f92a879fdc623ac47ee27de3" + integrity sha512-37qVVA1qZ5sgH7KpHkkC4z9SK6StIsIcOmpjvMPXNb3vx2GQxhZocogVYbr2PbbeLCQxYIPDok307xEvRZOzGA== + dependencies: + "@fastify/forwarded" "^3.0.0" + ipaddr.js "^2.1.0" + "@humanwhocodes/config-array@^0.13.0": version "0.13.0" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.13.0.tgz#fb907624df3256d04b9aa2df50d7aa97ec648748" @@ -723,6 +818,20 @@ multer "1.4.4-lts.1" tslib "2.7.0" +"@nestjs/platform-fastify@^11.1.6": + version "11.1.6" + resolved "https://registry.yarnpkg.com/@nestjs/platform-fastify/-/platform-fastify-11.1.6.tgz#22524df44cad32012858566f1b3bb627ce2788f8" + integrity sha512-udnIg7vfA103wppRkcMRVWX71S7NfeDnlprTndhcZzYXcDY2i5c+RwrQN/xU4Aw5X22Fg8ryi7bFbn6/Lquv8w== + dependencies: + "@fastify/cors" "11.1.0" + "@fastify/formbody" "8.0.2" + "@fastify/middie" "9.0.3" + fast-querystring "1.1.2" + fastify "5.4.0" + light-my-request "6.6.0" + path-to-regexp "8.2.0" + tslib "2.8.1" + "@nestjs/platform-socket.io@^10.4.8": version "10.4.8" resolved "https://registry.yarnpkg.com/@nestjs/platform-socket.io/-/platform-socket.io-10.4.8.tgz#cf483794f3b1831d804a3ac3a3f7b999664489d4" @@ -1096,6 +1205,11 @@ "@types/methods" "^1.1.4" "@types/superagent" "^8.1.0" +"@types/validator@^13.11.8": + version "13.15.3" + resolved "https://registry.yarnpkg.com/@types/validator/-/validator-13.15.3.tgz#67e8aeacbace03517f9bd3f99e750bb666207ff4" + integrity sha512-7bcUmDyS6PN3EuD9SlGGOxM77F8WLVsrwkxyWxKnxzmXoequ6c7741QBrANq6htVRGOITJ7z72mTP6Z4XyuG+Q== + "@types/ws@^8.5.13": version "8.5.13" resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.13.tgz#6414c280875e2691d0d1e080b05addbf5cb91e20" @@ -1337,6 +1451,11 @@ resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== +abstract-logging@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/abstract-logging/-/abstract-logging-2.0.1.tgz#6b0c371df212db7129b57d2e7fcf282b8bf1c839" + integrity sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA== + accepts@~1.3.4, accepts@~1.3.8: version "1.3.8" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" @@ -1362,13 +1481,20 @@ acorn@^8.11.0, acorn@^8.14.0, acorn@^8.4.1, acorn@^8.8.2, acorn@^8.9.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.0.tgz#063e2c70cac5fb4f6467f0b11152e04c682795b0" integrity sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA== -ajv-formats@2.1.1: +ajv-formats@2.1.1, ajv-formats@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA== dependencies: ajv "^8.0.0" +ajv-formats@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-3.0.1.tgz#3d5dc762bca17679c3c2ea7e90ad6b7532309578" + integrity sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ== + dependencies: + ajv "^8.0.0" + ajv-keywords@^3.5.2: version "3.5.2" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" @@ -1394,7 +1520,7 @@ ajv@^6.12.4, ajv@^6.12.5: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ajv@^8.0.0: +ajv@^8.0.0, ajv@^8.10.0, ajv@^8.11.0, ajv@^8.12.0: version "8.17.1" resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6" integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g== @@ -1503,6 +1629,27 @@ asynckit@^0.4.0: resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== +atomic-sleep@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b" + integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ== + +avvio@^8.3.0: + version "8.4.0" + resolved "https://registry.yarnpkg.com/avvio/-/avvio-8.4.0.tgz#7cbd5bca74f0c9effa944ced601f94ffd8afc5ed" + integrity sha512-CDSwaxINFy59iNwhYnkvALBwZiTydGkOecZyPkqBpABYR1KqGEsET0VOOYDwtleZSUIdeY36DC2bSZ24CO1igA== + dependencies: + "@fastify/error" "^3.3.0" + fastq "^1.17.1" + +avvio@^9.0.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/avvio/-/avvio-9.1.0.tgz#0ff80ed211682441d8aa39ff21a4b9d022109c44" + integrity sha512-fYASnYi600CsH/j9EQov7lECAniYiBFiiAtBNuZYLA2leLe9qOvZzqYHFjtIj6gD2VMoMLP14834LFWvr4IfDw== + dependencies: + "@fastify/error" "^4.0.0" + fastq "^1.17.1" + axios@^1.7.7: version "1.7.7" resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.7.tgz#2f554296f9892a72ac8d8e4c5b79c14a91d0a47f" @@ -1777,6 +1924,25 @@ cjs-module-lexer@^1.0.0: resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.4.1.tgz#707413784dbb3a72aa11c2f2b042a0bef4004170" integrity sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA== +class-transformer-validator@^0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/class-transformer-validator/-/class-transformer-validator-0.9.1.tgz#81af4bab5e13ce619a25a74cc70f723a8c4e2779" + integrity sha512-83/KFCyd6UiiwH6PlQS5y17O5TTx58CawvNI+XdrMs0Ig9QI5kiuzRqGcC/WrEpd1F7i4KIxCwdn6m4B6fl0jw== + +class-transformer@0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/class-transformer/-/class-transformer-0.5.1.tgz#24147d5dffd2a6cea930a3250a677addf96ab336" + integrity sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw== + +class-validator@^0.14.0: + version "0.14.2" + resolved "https://registry.yarnpkg.com/class-validator/-/class-validator-0.14.2.tgz#a3de95edd26b703e89c151a2023d3c115030340d" + integrity sha512-3kMVRF2io8N8pY1IFIXlho9r8IPUUIfHe2hYVtiebvAzU2XeQFXTv+XI4WX+TnXmtwXMDcjngcpkiPM0O9PvLw== + dependencies: + "@types/validator" "^13.11.8" + libphonenumber-js "^1.11.1" + validator "^13.9.0" + cli-cursor@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" @@ -1924,11 +2090,16 @@ cookie@0.7.1: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.1.tgz#2f73c42142d5d5cf71310a74fc4ae61670e5dbc9" integrity sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w== -cookie@~0.7.2: +cookie@^0.7.0, cookie@~0.7.2: version "0.7.2" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7" integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w== +cookie@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-1.0.2.tgz#27360701532116bd3f1f9416929d176afe1e4610" + integrity sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA== + cookiejar@^2.1.4: version "2.1.4" resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.4.tgz#ee669c1fea2cf42dc31585469d193fef0d65771b" @@ -2039,6 +2210,11 @@ depd@2.0.0: resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== +dequal@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" + integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== + destroy@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" @@ -2425,6 +2601,16 @@ external-editor@^3.0.3, external-editor@^3.1.0: iconv-lite "^0.4.24" tmp "^0.0.33" +fast-content-type-parse@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fast-content-type-parse/-/fast-content-type-parse-1.1.0.tgz#4087162bf5af3294d4726ff29b334f72e3a1092c" + integrity sha512-fBHHqSTFLVnR61C+gltJuE5GkVQMV0S2nqUO8TJ+5Z3qAKG8vAx4FKai1s5jq/inV1+sREynIWSuQ6HgoSXpDQ== + +fast-decode-uri-component@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz#46f8b6c22b30ff7a81357d4f59abfae938202543" + integrity sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg== + fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -2451,21 +2637,123 @@ fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0, fast-json-sta resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== +fast-json-stringify@^5.7.0, fast-json-stringify@^5.8.0: + version "5.16.1" + resolved "https://registry.yarnpkg.com/fast-json-stringify/-/fast-json-stringify-5.16.1.tgz#a6d0c575231a3a08c376a00171d757372f2ca46e" + integrity sha512-KAdnLvy1yu/XrRtP+LJnxbBGrhN+xXu+gt3EUvZhYGKCr3lFHq/7UFJHHFgmJKoqlh6B40bZLEv7w46B0mqn1g== + dependencies: + "@fastify/merge-json-schemas" "^0.1.0" + ajv "^8.10.0" + ajv-formats "^3.0.1" + fast-deep-equal "^3.1.3" + fast-uri "^2.1.0" + json-schema-ref-resolver "^1.0.1" + rfdc "^1.2.0" + +fast-json-stringify@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/fast-json-stringify/-/fast-json-stringify-6.0.1.tgz#82f1cb45fa96d0ca24b601f1738066976d6e2430" + integrity sha512-s7SJE83QKBZwg54dIbD5rCtzOBVD43V1ReWXXYqBgwCwHLYAAT0RQc/FmrQglXqWPpz6omtryJQOau5jI4Nrvg== + dependencies: + "@fastify/merge-json-schemas" "^0.2.0" + ajv "^8.12.0" + ajv-formats "^3.0.1" + fast-uri "^3.0.0" + json-schema-ref-resolver "^2.0.0" + rfdc "^1.2.0" + fast-levenshtein@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== +fast-querystring@1.1.2, fast-querystring@^1.0.0, fast-querystring@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/fast-querystring/-/fast-querystring-1.1.2.tgz#a6d24937b4fc6f791b4ee31dcb6f53aeafb89f53" + integrity sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg== + dependencies: + fast-decode-uri-component "^1.0.1" + +fast-redact@^3.1.1: + version "3.5.0" + resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.5.0.tgz#e9ea02f7e57d0cd8438180083e93077e496285e4" + integrity sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A== + fast-safe-stringify@2.1.1, fast-safe-stringify@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== +fast-uri@^2.0.0, fast-uri@^2.1.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-2.4.0.tgz#67eae6fbbe9f25339d5d3f4c4234787b65d7d55e" + integrity sha512-ypuAmmMKInk5q7XcepxlnUWDLWv4GFtaJqAzWKqn62IpQ3pejtr5dTVbt3vwqVaMKmkNR55sTT+CqUKIaT21BA== + +fast-uri@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.0.tgz#66eecff6c764c0df9b762e62ca7edcfb53b4edfa" + integrity sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA== + fast-uri@^3.0.1: version "3.0.3" resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.3.tgz#892a1c91802d5d7860de728f18608a0573142241" integrity sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw== +fastify-plugin@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/fastify-plugin/-/fastify-plugin-5.0.1.tgz#82d44e6fe34d1420bb5a4f7bee434d501e41939f" + integrity sha512-HCxs+YnRaWzCl+cWRYFnHmeRFyR5GVnJTAaCJQiYzQSDwK9MgJdyAsuL3nh0EWRCYMgQ5MeziymvmAhUHYHDUQ== + +fastify@4.28.1: + version "4.28.1" + resolved "https://registry.yarnpkg.com/fastify/-/fastify-4.28.1.tgz#39626dedf445d702ef03818da33064440b469cd1" + integrity sha512-kFWUtpNr4i7t5vY2EJPCN2KgMVpuqfU4NjnJNCgiNB900oiDeYqaNDRcAfeBbOF5hGixixxcKnOU4KN9z6QncQ== + dependencies: + "@fastify/ajv-compiler" "^3.5.0" + "@fastify/error" "^3.4.0" + "@fastify/fast-json-stringify-compiler" "^4.3.0" + abstract-logging "^2.0.1" + avvio "^8.3.0" + fast-content-type-parse "^1.1.0" + fast-json-stringify "^5.8.0" + find-my-way "^8.0.0" + light-my-request "^5.11.0" + pino "^9.0.0" + process-warning "^3.0.0" + proxy-addr "^2.0.7" + rfdc "^1.3.0" + secure-json-parse "^2.7.0" + semver "^7.5.4" + toad-cache "^3.3.0" + +fastify@5.4.0: + version "5.4.0" + resolved "https://registry.yarnpkg.com/fastify/-/fastify-5.4.0.tgz#82bf56e0bc36ba8dfb0bd372a0de8b62ccf3287c" + integrity sha512-I4dVlUe+WNQAhKSyv15w+dwUh2EPiEl4X2lGYMmNSgF83WzTMAPKGdWEv5tPsCQOb+SOZwz8Vlta2vF+OeDgRw== + dependencies: + "@fastify/ajv-compiler" "^4.0.0" + "@fastify/error" "^4.0.0" + "@fastify/fast-json-stringify-compiler" "^5.0.0" + "@fastify/proxy-addr" "^5.0.0" + abstract-logging "^2.0.1" + avvio "^9.0.0" + fast-json-stringify "^6.0.0" + find-my-way "^9.0.0" + light-my-request "^6.0.0" + pino "^9.0.0" + process-warning "^5.0.0" + rfdc "^1.3.1" + secure-json-parse "^4.0.0" + semver "^7.6.0" + toad-cache "^3.7.0" + +fastq@^1.17.1: + version "1.19.1" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.19.1.tgz#d50eaba803c8846a883c16492821ebcd2cda55f5" + integrity sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ== + dependencies: + reusify "^1.0.4" + fastq@^1.6.0: version "1.17.1" resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47" @@ -2521,6 +2809,24 @@ finalhandler@1.3.1: statuses "2.0.1" unpipe "~1.0.0" +find-my-way@^8.0.0: + version "8.2.2" + resolved "https://registry.yarnpkg.com/find-my-way/-/find-my-way-8.2.2.tgz#f3e78bc6ead2da4fdaa201335da3228600ed0285" + integrity sha512-Dobi7gcTEq8yszimcfp/R7+owiT4WncAJ7VTTgFH1jYJ5GaG1FbhjwDG820hptN0QDFvzVY3RfCzdInvGPGzjA== + dependencies: + fast-deep-equal "^3.1.3" + fast-querystring "^1.0.0" + safe-regex2 "^3.1.0" + +find-my-way@^9.0.0: + version "9.3.0" + resolved "https://registry.yarnpkg.com/find-my-way/-/find-my-way-9.3.0.tgz#9f57786b5d772cc45142bf39dd5349f9cc883f91" + integrity sha512-eRoFWQw+Yv2tuYlK2pjFS2jGXSxSppAs3hSQjfxVKxM5amECzIgYYc1FEI8ZmhSh/Ig+FrKEz43NLRKJjYCZVg== + dependencies: + fast-deep-equal "^3.1.3" + fast-querystring "^1.0.0" + safe-regex2 "^5.0.0" + find-up@^4.0.0, find-up@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" @@ -2913,7 +3219,7 @@ ipaddr.js@1.9.1: resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== -ipaddr.js@^2.2.0: +ipaddr.js@^2.1.0, ipaddr.js@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.2.0.tgz#d33fa7bac284f4de7af949638c9d68157c6b92e8" integrity sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA== @@ -3473,6 +3779,20 @@ json-parse-even-better-errors@^2.3.0, json-parse-even-better-errors@^2.3.1: resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== +json-schema-ref-resolver@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-schema-ref-resolver/-/json-schema-ref-resolver-1.0.1.tgz#6586f483b76254784fc1d2120f717bdc9f0a99bf" + integrity sha512-EJAj1pgHc1hxF6vo2Z3s69fMjO1INq6eGHXZ8Z6wCQeldCuwxGK9Sxf4/cScGn3FZubCVUehfWtcDM/PLteCQw== + dependencies: + fast-deep-equal "^3.1.3" + +json-schema-ref-resolver@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/json-schema-ref-resolver/-/json-schema-ref-resolver-2.0.1.tgz#c92f16b452df069daac53e1984159e0f9af0598d" + integrity sha512-HG0SIB9X4J8bwbxCbnd5FfPEbcXAJYTi1pBJeP/QPON+w8ovSME8iRG+ElHNxZNX2Qh6eYn1GdzJFS4cDFfx0Q== + dependencies: + dequal "^2.0.3" + json-schema-traverse@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" @@ -3537,6 +3857,29 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" +libphonenumber-js@^1.11.1: + version "1.12.22" + resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.12.22.tgz#2a56f633173cdb91bd173f5984ec23198eebe5d0" + integrity sha512-nzdkDyqlcLV754o1RrOJxh8kycG+63odJVUqnK4dxhw7buNkdTqJc/a/CE0h599dTJgFbzvr6GEOemFBSBryAA== + +light-my-request@6.6.0, light-my-request@^6.0.0: + version "6.6.0" + resolved "https://registry.yarnpkg.com/light-my-request/-/light-my-request-6.6.0.tgz#c9448772323f65f33720fb5979c7841f14060add" + integrity sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A== + dependencies: + cookie "^1.0.1" + process-warning "^4.0.0" + set-cookie-parser "^2.6.0" + +light-my-request@^5.11.0: + version "5.14.0" + resolved "https://registry.yarnpkg.com/light-my-request/-/light-my-request-5.14.0.tgz#11ddae56de4053fd5c1845cbfbee5c29e8a257e7" + integrity sha512-aORPWntbpH5esaYpGOOmri0OHDOe3wC5M2MQxZ9dvMLZm6DnaAn0kJlcbU9hwsQgLzmZyReKwFwwPkR+nHu5kA== + dependencies: + cookie "^0.7.0" + process-warning "^3.0.0" + set-cookie-parser "^2.4.1" + lines-and-columns@^1.1.6: version "1.2.4" resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" @@ -3838,6 +4181,11 @@ object-inspect@^1.13.1: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.3.tgz#f14c183de51130243d6d18ae149375ff50ea488a" integrity sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA== +on-exit-leak-free@^2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz#fed195c9ebddb7d9e4c3842f93f281ac8dadd3b8" + integrity sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA== + on-finished@2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" @@ -3989,6 +4337,16 @@ path-to-regexp@3.3.0: resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-3.3.0.tgz#f7f31d32e8518c2660862b644414b6d5c63a611b" integrity sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw== +path-to-regexp@8.2.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-8.2.0.tgz#73990cc29e57a3ff2a0d914095156df5db79e8b4" + integrity sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ== + +path-to-regexp@^8.1.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-8.3.0.tgz#aa818a6981f99321003a08987d3cec9c3474cd1f" + integrity sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA== + path-type@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" @@ -4009,6 +4367,35 @@ picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== +pino-abstract-transport@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz#de241578406ac7b8a33ce0d77ae6e8a0b3b68a60" + integrity sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw== + dependencies: + split2 "^4.0.0" + +pino-std-serializers@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz#7c625038b13718dbbd84ab446bd673dc52259e3b" + integrity sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA== + +pino@^9.0.0: + version "9.11.0" + resolved "https://registry.yarnpkg.com/pino/-/pino-9.11.0.tgz#7fc383f815cf6bf5979b4d791eafd2f2496c42a6" + integrity sha512-+YIodBB9sxcWeR8PrXC2K3gEDyfkUuVEITOcbqrfcj+z5QW4ioIcqZfYFbrLTYLsmAwunbS7nfU/dpBB6PZc1g== + dependencies: + atomic-sleep "^1.0.0" + fast-redact "^3.1.1" + on-exit-leak-free "^2.1.0" + pino-abstract-transport "^2.0.0" + pino-std-serializers "^7.0.0" + process-warning "^5.0.0" + quick-format-unescaped "^4.0.3" + real-require "^0.2.0" + safe-stable-stringify "^2.3.1" + sonic-boom "^4.0.1" + thread-stream "^3.0.0" + pirates@^4.0.4: version "4.0.6" resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9" @@ -4057,6 +4444,21 @@ process-nextick-args@~2.0.0: resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== +process-warning@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-3.0.0.tgz#96e5b88884187a1dce6f5c3166d611132058710b" + integrity sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ== + +process-warning@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-4.0.1.tgz#5c1db66007c67c756e4e09eb170cdece15da32fb" + integrity sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q== + +process-warning@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-5.0.0.tgz#566e0bf79d1dff30a72d8bbbe9e8ecefe8d378d7" + integrity sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA== + prompts@^2.0.1: version "2.4.2" resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" @@ -4065,7 +4467,7 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.5" -proxy-addr@~2.0.7: +proxy-addr@^2.0.7, proxy-addr@~2.0.7: version "2.0.7" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== @@ -4107,6 +4509,11 @@ queue-microtask@^1.2.2: resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== +quick-format-unescaped@^4.0.3: + version "4.0.4" + resolved "https://registry.yarnpkg.com/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz#93ef6dd8d3453cbc7970dd614fad4c5954d6b5a7" + integrity sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg== + randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" @@ -4163,6 +4570,11 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" +real-require@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/real-require/-/real-require-0.2.0.tgz#209632dea1810be2ae063a6ac084fee7e33fba78" + integrity sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg== + reflect-metadata@^0.2.0: version "0.2.2" resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.2.2.tgz#400c845b6cba87a21f2c65c4aeb158f4fa4d9c5b" @@ -4222,11 +4634,26 @@ restore-cursor@^3.1.0: onetime "^5.1.0" signal-exit "^3.0.2" +ret@~0.4.0: + version "0.4.3" + resolved "https://registry.yarnpkg.com/ret/-/ret-0.4.3.tgz#5243fa30e704a2e78a9b9b1e86079e15891aa85c" + integrity sha512-0f4Memo5QP7WQyUEAYUO3esD/XjOc3Zjjg5CPsAq1p8sIu0XPeMbHJemKA0BO7tV0X7+A0FoEpbmHXWxPyD3wQ== + +ret@~0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/ret/-/ret-0.5.0.tgz#30a4d38a7e704bd96dc5ffcbe7ce2a9274c41c95" + integrity sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw== + reusify@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== +rfdc@^1.2.0, rfdc@^1.3.0, rfdc@^1.3.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.4.1.tgz#778f76c4fb731d93414e8f925fbecf64cce7f6ca" + integrity sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA== + rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" @@ -4268,6 +4695,25 @@ safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== +safe-regex2@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/safe-regex2/-/safe-regex2-3.1.0.tgz#fd7ec23908e2c730e1ce7359a5b72883a87d2763" + integrity sha512-RAAZAGbap2kBfbVhvmnTFv73NWLMvDGOITFYTZBAaY8eR+Ir4ef7Up/e7amo+y1+AH+3PtLkrt9mvcTsG9LXug== + dependencies: + ret "~0.4.0" + +safe-regex2@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/safe-regex2/-/safe-regex2-5.0.0.tgz#762e4a4c328603427281d2b99662f2d04e4ae811" + integrity sha512-YwJwe5a51WlK7KbOJREPdjNrpViQBI3p4T50lfwPuDhZnE3XGVTlGvi+aolc5+RvxDD6bnUmjVsU9n1eboLUYw== + dependencies: + ret "~0.5.0" + +safe-stable-stringify@^2.3.1: + version "2.5.0" + resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz#4ca2f8e385f2831c432a719b108a3bf7af42a1dd" + integrity sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA== + "safer-buffer@>= 2.1.2 < 3": version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" @@ -4282,6 +4728,16 @@ schema-utils@^3.1.1, schema-utils@^3.2.0: ajv "^6.12.5" ajv-keywords "^3.5.2" +secure-json-parse@^2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/secure-json-parse/-/secure-json-parse-2.7.0.tgz#5a5f9cd6ae47df23dba3151edd06855d47e09862" + integrity sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw== + +secure-json-parse@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/secure-json-parse/-/secure-json-parse-4.0.0.tgz#2ee1b7581be38ab348bab5a3e49280ba80a89c85" + integrity sha512-dxtLJO6sc35jWidmLxo7ij+Eg48PM/kleBsxpC8QJE0qJICe+KawkDQmvCMZUr9u7WKVHgMW6vy3fQ7zMiFZMA== + semver@^6.3.0, semver@^6.3.1: version "6.3.1" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" @@ -4292,6 +4748,11 @@ semver@^7.3.4, semver@^7.3.5, semver@^7.3.8, semver@^7.5.3, semver@^7.5.4, semve resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== +semver@^7.6.0: + version "7.7.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58" + integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA== + send@0.19.0: version "0.19.0" resolved "https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8" @@ -4328,6 +4789,11 @@ serve-static@1.16.2: parseurl "~1.3.3" send "0.19.0" +set-cookie-parser@^2.4.1, set-cookie-parser@^2.6.0: + version "2.7.1" + resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz#3016f150072202dfbe90fadee053573cc89d2943" + integrity sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ== + set-function-length@^1.2.1: version "1.2.2" resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" @@ -4426,6 +4892,13 @@ socket.io@4.8.0: socket.io-adapter "~2.5.2" socket.io-parser "~4.2.4" +sonic-boom@^4.0.1: + version "4.2.0" + resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-4.2.0.tgz#e59a525f831210fa4ef1896428338641ac1c124d" + integrity sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww== + dependencies: + atomic-sleep "^1.0.0" + source-map-support@0.5.13: version "0.5.13" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932" @@ -4452,6 +4925,11 @@ source-map@^0.6.0, source-map@^0.6.1: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== +split2@^4.0.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4" + integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg== + sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" @@ -4667,6 +5145,13 @@ text-table@^0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== +thread-stream@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/thread-stream/-/thread-stream-3.1.0.tgz#4b2ef252a7c215064507d4ef70c05a5e2d34c4f1" + integrity sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A== + dependencies: + real-require "^0.2.0" + through@^2.3.6: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" @@ -4691,6 +5176,11 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" +toad-cache@^3.3.0, toad-cache@^3.7.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/toad-cache/-/toad-cache-3.7.0.tgz#b9b63304ea7c45ec34d91f1d2fa513517025c441" + integrity sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw== + toidentifier@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" @@ -4779,7 +5269,7 @@ tslib@2.7.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.7.0.tgz#d9b40c5c40ab59e8738f297df3087bf1a2690c01" integrity sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA== -tslib@^2.1.0, tslib@^2.6.2: +tslib@2.8.1, tslib@^2.1.0, tslib@^2.6.2: version "2.8.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== @@ -4885,6 +5375,11 @@ v8-to-istanbul@^9.0.1: "@types/istanbul-lib-coverage" "^2.0.1" convert-source-map "^2.0.0" +validator@^13.9.0: + version "13.15.15" + resolved "https://registry.yarnpkg.com/validator/-/validator-13.15.15.tgz#246594be5671dc09daa35caec5689fcd18c6e7e4" + integrity sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A== + vary@^1, vary@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" From 68837d21d720b6a2be9b74fc9cd612f535e703a2 Mon Sep 17 00:00:00 2001 From: Aakash Reddy Date: Thu, 25 Sep 2025 18:06:27 +0530 Subject: [PATCH 11/17] feat: setup to test the apis for testflow --- src/payloads/testflow.payload.ts | 4 - src/proxy/testflow/testflow.service.ts | 151 +++---------------------- 2 files changed, 14 insertions(+), 141 deletions(-) diff --git a/src/payloads/testflow.payload.ts b/src/payloads/testflow.payload.ts index 35e62d4..96dd996 100644 --- a/src/payloads/testflow.payload.ts +++ b/src/payloads/testflow.payload.ts @@ -162,10 +162,6 @@ export class TestflowRunDto { @IsString() @IsOptional() userId:string; - - @IsString() - @IsOptional() - selectedAgent:WorkspaceUserAgentBaseEnum } export class NodeData { diff --git a/src/proxy/testflow/testflow.service.ts b/src/proxy/testflow/testflow.service.ts index d34921a..e2441f5 100644 --- a/src/proxy/testflow/testflow.service.ts +++ b/src/proxy/testflow/testflow.service.ts @@ -27,145 +27,23 @@ export class TestflowService { headers: string, body: string, contentType: string, - selectedAgent: WorkspaceUserAgentBaseEnum, signal?: AbortSignal, ) { - const startTime = performance.now(); try { - let response; - // Cloud Agent - call makeHttpRequest directly - if (selectedAgent === "Cloud Agent") { - try { - const cloudResponse = await this.httpService.makeHttpRequest({ - url, - method, - headers, - body, - contentType, - }); - return success({ - body: cloudResponse.data, - status: cloudResponse.status, - headers: cloudResponse.headers, - }); - } catch (cloudError) { - return error(cloudError.message || "Cloud agent request failed"); - } - } else { - try { - let jsonHeader; - try { - jsonHeader = JSON.parse(headers); - console.log("[makeHttpRequestV2] parsed headers", jsonHeader); - } catch { - console.warn("[makeHttpRequestV2] failed to parse headers, using []"); - jsonHeader = []; - } - const headersObject = jsonHeader.reduce( - ( - acc: Record, - header: { key: string; value: string }, - ) => { - acc[header.key] = header.value; - return acc; - }, - {}, - ); - let requestData = body || {}; - console.log("[makeHttpRequestV2] initial requestData", requestData); - if (contentType === "multipart/form-data") { - console.log("[makeHttpRequestV2] handling multipart/form-data"); - const formData = new FormData(); - const parsedBody = JSON.parse(body); - for (const field of parsedBody || []) { - try { - if (field?.base) { - const file = await new Base64Converter().base64ToFile( - field.base, - field.value, - ); - formData.append(field.key, file); - } else { - formData.append(field.key, field.value); - } - } catch (e) { - console.error("[makeHttpRequestV2] formData field error", e); - formData.append(field.key, field.value); - } - } - requestData = formData; - delete headersObject["Content-Type"]; // let axios set boundary - } else if (contentType === "application/x-www-form-urlencoded") { - console.log("[makeHttpRequestV2] handling urlencoded body"); - const urlSearchParams = new URLSearchParams(); - const parsedBody = JSON.parse(body); - (parsedBody || []).forEach( - (field: { key: string; value: string }) => { - urlSearchParams.append(field.key, field.value); - }, - ); - requestData = urlSearchParams; - } else if ( - contentType === "application/json" || - contentType === "text/plain" - ) { - headersObject["Content-Type"] = contentType; - } - const axiosResponse = await Promise.race([ - axios({ - method, - url, - data: requestData || {}, - headers: { ...headersObject }, - responseType: "arraybuffer", - validateStatus: () => true, - }), - this.waitForAbort(signal), - ]); - if (signal?.aborted) { - console.warn("[makeHttpRequestV2] request was aborted"); - throw new DOMException("Request was aborted", "AbortError"); - } - let responseData = ""; - const responseContentType = axiosResponse.headers["content-type"] || ""; - if (responseContentType.startsWith("image/")) { - console.log("[makeHttpRequestV2] handling image response"); - const base64 = btoa( - new Uint8Array(axiosResponse.data).reduce( - (data, byte) => data + String.fromCharCode(byte), - "", - ), - ); - responseData = `data:${responseContentType};base64,${base64}`; - } else { - responseData = new TextDecoder("utf-8").decode(axiosResponse.data); - } - const status = `${axiosResponse.status} ${ - axiosResponse.statusText || - new StatusCode().getText(axiosResponse.status) - }`; - return success({ - body: responseData, - status: status, - headers: Object.fromEntries( - Object.entries(axiosResponse.headers), - ), - }); - } catch (axiosError: any) { - console.error("[makeHttpRequestV2] axios error", axiosError); - if (signal?.aborted) { - throw new DOMException("Request was aborted", "AbortError"); - } - return error(axiosError.message || "Browser agent request failed"); - } - } - } catch (e) { - if (signal?.aborted) { - console.warn("[makeHttpRequestV2] aborted at outer catch"); - throw new DOMException("Request was aborted", "AbortError"); - } - console.error("[makeHttpRequestV2] request error", e); - return error(String(e)); + const response = await this.httpService.makeHttpRequest({ + url, + method, + headers, + body, + contentType, + }); + return success({ + body: response.data, + status: response.status, + headers: response.headers, + }); + } catch (cloudError) { + return error(cloudError.message || "Cloud agent request failed"); } } @@ -230,7 +108,6 @@ export class TestflowService { headers, body, contentType, - payload.selectedAgent, signal, ); const duration = Date.now() - start; From 943aeb566c062166f9c4a9c443f5a875b931aaeb Mon Sep 17 00:00:00 2001 From: Aakash Reddy Date: Fri, 26 Sep 2025 12:38:51 +0530 Subject: [PATCH 12/17] feat: added nodes running based on edges --- src/enum/testflow.enum.ts | 5 - src/payloads/testflow.payload.ts | 30 +- src/proxy/testflow/testflow.module.ts | 4 +- src/proxy/testflow/testflow.service.ts | 401 +++++++++++++++++++++++-- src/utils/base64Converter.ts | 50 --- src/utils/status-code.ts | 80 ----- 6 files changed, 410 insertions(+), 160 deletions(-) delete mode 100644 src/utils/base64Converter.ts delete mode 100644 src/utils/status-code.ts diff --git a/src/enum/testflow.enum.ts b/src/enum/testflow.enum.ts index 36ee6f3..f3f0730 100644 --- a/src/enum/testflow.enum.ts +++ b/src/enum/testflow.enum.ts @@ -37,11 +37,6 @@ export class Auth { }; } -export enum WorkspaceUserAgentBaseEnum { - BROWSER_AGENT= "Browser Agent", - CLOUD_AGENT= "Cloud Agent" -} - export type TFKeyValueStoreType = { key: string; value: string; diff --git a/src/payloads/testflow.payload.ts b/src/payloads/testflow.payload.ts index 96dd996..0885314 100644 --- a/src/payloads/testflow.payload.ts +++ b/src/payloads/testflow.payload.ts @@ -13,7 +13,7 @@ import { IsDate, } from 'class-validator'; import { HTTPMethods } from "fastify"; -import { BodyModeEnum, WorkspaceUserAgentBaseEnum } from 'src/enum/testflow.enum'; +import { BodyModeEnum,} from 'src/enum/testflow.enum'; import { AuthModeEnum } from 'src/enum/testflow.enum'; import { Auth } from 'src/enum/testflow.enum'; @@ -146,6 +146,20 @@ export class RequestMetaData { auth?: Auth[]; } +export class TestflowEdges { + @IsString() + @IsNotEmpty() + id: string; + + @IsString() + @IsNotEmpty() + source: string; + + @IsString() + @IsNotEmpty() + target: string; +} + export class TestflowRunDto { @IsArray() @Type(() => TestflowNodes) @@ -159,6 +173,12 @@ export class TestflowRunDto { @ValidateNested({ each: true }) variables: VariableDto[]; + @IsArray() + @Type(() => TestflowEdges) + @ValidateNested({ each: true }) + @IsOptional() + edges: TestflowEdges[]; + @IsString() @IsOptional() userId:string; @@ -223,6 +243,14 @@ export class TestflowSchedularHistoryRequest { @IsString() @IsOptional() time: string; + + @IsString() + @IsOptional() + errorMessage?: string; + + @IsString() + @IsOptional() + error?: string; } export class TestFlowSchedularRunHistory { diff --git a/src/proxy/testflow/testflow.module.ts b/src/proxy/testflow/testflow.module.ts index 47a0cc9..20e7b16 100644 --- a/src/proxy/testflow/testflow.module.ts +++ b/src/proxy/testflow/testflow.module.ts @@ -1,11 +1,11 @@ import { Module } from '@nestjs/common'; import { TestflowController } from './testflow.controller'; import { TestflowService } from './testflow.service'; -import { HttpModule } from '../http/http.module'; import { DecodeTestflow } from 'src/utils/decode-testflow'; +import { HttpModule as NestHttpModule } from '@nestjs/axios'; @Module({ - imports: [HttpModule], + imports: [NestHttpModule], controllers: [TestflowController], providers: [TestflowService, DecodeTestflow], }) diff --git a/src/proxy/testflow/testflow.service.ts b/src/proxy/testflow/testflow.service.ts index e2441f5..eefaf81 100644 --- a/src/proxy/testflow/testflow.service.ts +++ b/src/proxy/testflow/testflow.service.ts @@ -1,27 +1,340 @@ import { Injectable } from '@nestjs/common'; -import { TestflowRunDto, TestFlowSchedularRunHistory } from 'src/payloads/testflow.payload'; -import { HttpService } from '../http/http.service'; -import { WorkspaceUserAgentBaseEnum } from 'src/enum/testflow.enum'; -import { Base64Converter } from 'src/utils/base64Converter'; +import { TestflowNodes, TestflowRunDto, TestFlowSchedularRunHistory } from 'src/payloads/testflow.payload'; import { Logger } from '@nestjs/common'; -import axios from "axios"; import { success,error } from 'src/enum/httpResponseFormat'; -import { StatusCode } from 'src/utils/status-code'; import { DecodeTestflow, RequestData } from 'src/utils/decode-testflow'; import { ParseTime } from 'src/utils/parse-time'; import { TFAPIResponseType } from 'src/enum/testflow.enum'; import { TFKeyValueStoreType } from 'src/enum/testflow.enum'; import { ResponseStatusCode } from 'src/enum/httpRequest.enum'; +import { BadRequestException } from '@nestjs/common'; +import { HttpService as NestHttpService } from '@nestjs/axios'; +import * as https from 'https'; +import FormData from 'form-data'; +import { lookup } from 'dns/promises'; +import * as ipaddr from 'ipaddr.js'; @Injectable() export class TestflowService { private readonly logger = new Logger(TestflowService.name); private _decodeRequest = new DecodeTestflow(); constructor( - private readonly httpService: HttpService, + private readonly httpService: NestHttpService, ) {} - async makeRequest( + private base64ToBuffer(base64: string): { buffer: Buffer; mime: string } { + const arr = base64.split(','); + const mime = arr[0].match(/:(.*?);/)?.[1]; + const bstr = Buffer.from(arr[1], 'base64'); + return { buffer: bstr, mime }; + } + + private getStatusText(statusCode: number): string { + const statusMap: Record = { + // 1xx Informational + 100: 'Continue', + 101: 'Switching Protocols', + 102: 'Processing', + 103: 'Early Hints', + + // 2xx Success + 200: 'OK', + 201: 'Created', + 202: 'Accepted', + 203: 'Non-Authoritative Information', + 204: 'No Content', + 205: 'Reset Content', + 206: 'Partial Content', + 207: 'Multi-Status', + 208: 'Already Reported', + 226: 'IM Used', + + // 3xx Redirection + 300: 'Multiple Choices', + 301: 'Moved Permanently', + 302: 'Found', + 303: 'See Other', + 304: 'Not Modified', + 305: 'Use Proxy', + 307: 'Temporary Redirect', + 308: 'Permanent Redirect', + + // 4xx Client Errors + 400: 'Bad Request', + 401: 'Unauthorized', + 402: 'Payment Required', + 403: 'Forbidden', + 404: 'Not Found', + 405: 'Method Not Allowed', + 406: 'Not Acceptable', + 407: 'Proxy Authentication Required', + 408: 'Request Timeout', + 409: 'Conflict', + 410: 'Gone', + 411: 'Length Required', + 412: 'Precondition Failed', + 413: 'Payload Too Large', + 414: 'URI Too Long', + 415: 'Unsupported Media Type', + 416: 'Range Not Satisfiable', + 417: 'Expectation Failed', + 418: `I\'m a teapot`, + 421: 'Misdirected Request', + 422: 'Unprocessable Entity', + 423: 'Locked', + 424: 'Failed Dependency', + 425: 'Too Early', + 426: 'Upgrade Required', + 428: 'Precondition Required', + 429: 'Too Many Requests', + 431: 'Request Header Fields Too Large', + 451: 'Unavailable For Legal Reasons', + + // 5xx Server Errors + 500: 'Internal Server Error', + 501: 'Not Implemented', + 502: 'Bad Gateway', + 503: 'Service Unavailable', + 504: 'Gateway Timeout', + 505: 'HTTP Version Not Supported', + 506: 'Variant Also Negotiates', + 507: 'Insufficient Storage', + 508: 'Loop Detected', + 509: 'Bandwidth Limit Exceeded', + 510: 'Not Extended', + 511: 'Network Authentication Required', + }; + + return statusMap[statusCode] || 'Unknown Status'; + } + + private async validateUrl(targetUrl: string) { + try { + const url = new URL(targetUrl); + + // Resolve hostname to IPs + const addresses = await lookup(url.hostname, { all: true }); + + for (const addr of addresses) { + const ip = ipaddr.parse(addr.address); + + // Block local, private, or reserved IPs + if ( + ip.range() === 'linkLocal' || // 169.254.0.0/16 (Azure IMDS lives here) + ip.range() === 'loopback' || // 127.0.0.0/8 + ip.range() === 'private' || // 10.x, 192.168.x, 172.16-31.x + ip.range() === 'reserved' // Other reserved ranges + ) { + throw new BadRequestException( + `Access to internal IP addresses is not allowed: ${addr.address}`, + ); + } + } + } catch (err) { + throw new BadRequestException('Invalid or disallowed URL'); + } + } + + private async makeHttpRequest({ + url, + method, + headers, + body, + contentType, + }: { + url: string; + method: string; + headers: string; + body: any; + contentType: string; + }): Promise<{ status: string; data: any; headers: any }> { + try { + await this.validateUrl(url); + // Parse headers from stringified JSON + const parsedHeaders: Record = {}; + let headersArray; + + try { + headersArray = JSON.parse(headers); + if (Array.isArray(headersArray)) { + headersArray.forEach((item: any) => { + parsedHeaders[item.key] = item.value; + }); + } + } catch (headerError) { + console.error('Error parsing headers:', headerError); + throw new Error('Invalid headers format'); + } + + // Prepare the request configuration + const config: any = { + url, + method, + headers: parsedHeaders, + data: null, + }; + + // Handle body based on content type + try { + switch (contentType) { + case 'application/json': + if (typeof body === 'string') { + // Check if the body is a numeric string + const isNumeric = !isNaN(body as any) && !isNaN(parseFloat(body)); + if (isNumeric) { + config.data = body; // Keep numeric string as is + } else { + // Try parsing as JSON only if it's not a numeric string + try { + config.data = JSON.parse(body); + } catch (e) { + config.data = body; // If parsing fails, use the original string + } + } + } else { + config.data = body; + } + break; + + case 'application/x-www-form-urlencoded': + // Parse body if it's a string + const formParsedBody = + typeof body === 'string' ? JSON.parse(body) : body; + + if (!Array.isArray(formParsedBody)) { + throw new Error('Body must be an array for URL-encoded data.'); + } + + // Filter and transform the body into key-value pairs + const formBody: Record = {}; + formParsedBody.forEach((item: any) => { + formBody[item.key] = item.value; + }); + + const formUrlEncoded = new URLSearchParams(formBody); + config.data = formUrlEncoded.toString(); + config.headers['Content-Type'] = + 'application/x-www-form-urlencoded'; + break; + + case 'multipart/form-data': + const formData = new FormData(); + const parsedBody = + typeof body === 'string' ? JSON.parse(body) : body; + if (Array.isArray(parsedBody)) { + for (const field of parsedBody || []) { + try { + if (field?.base) { + const { buffer, mime } = this.base64ToBuffer(field.base); + formData.append(field.key, buffer, { + filename: field.value, + contentType: mime, + }); + } else { + formData.append(field.key, field.value); + } + } catch (e) { + formData.append(field.key, field.value); + } + } + } + + config.data = formData; + config.headers = { + ...parsedHeaders, + ...formData.getHeaders(), + }; + break; + + case 'text/plain': + config.data = body; + config.headers['Content-Type'] = 'text/plain'; + break; + + default: + break; + } + } catch (bodyError) { + console.error('Error processing request body:', bodyError); + throw new Error('Invalid request body format'); + } + + // Add custom user agent + config.headers['User-Agent'] = 'SparrowRuntime/1.0.0'; + + // DNS rebinding protection: re-validate resolved IP before request + const resolvedAddresses = await lookup(new URL(url).hostname, { all: true }); + for (const addr of resolvedAddresses) { + const ip = ipaddr.parse(addr.address); + if ( + ip.range() === 'linkLocal' || + ip.range() === 'loopback' || + ip.range() === 'private' || + ip.range() === 'reserved' + ) { + throw new BadRequestException( + `Access to internal IP addresses is not allowed: ${addr.address}`, + ); + } + } + + try { + const response = await this.httpService.axiosRef({ + url: config.url, + method: config.method, + headers: config.headers, + data: config.data, + responseType: 'arraybuffer', + httpsAgent: new https.Agent({ rejectUnauthorized: false }), // allows expired SSL certs. + }); + + let contentType = response.headers['content-type']; + let responseData = ''; + if (contentType?.startsWith('image/')) { + const base64 = Buffer.from(response.data).toString('base64'); + responseData = `data:${contentType};base64,${base64}`; + } else { + responseData = Buffer.from(response.data).toString('utf-8'); + } + + return { + status: + response.status + + ' ' + + (response.statusText || this.getStatusText(response.status)), + data: `${responseData}`, + headers: response.headers, + }; + } catch (axiosError: any) { + try { + const responseData = Buffer.from(axiosError.response?.data).toString( + 'utf-8', + ); + return { + status: axiosError.response?.status + ? axiosError.response?.status + + ' ' + + (axiosError.response?.statusText || + this.getStatusText(axiosError.response?.status)) + : null, + data: responseData || { message: axiosError.message }, + headers: axiosError.response?.headers, + }; + } catch (e) { + return { + status: null, + data: { message: axiosError.message }, + headers: axiosError.response?.headers, + }; + } + } + } catch (error: any) { + console.error('HTTP Service Error:', error); + throw new Error(error.message || 'Unknown error occurred'); + } + } + + private async makeRequest( url: string, method: string, headers: string, @@ -30,7 +343,7 @@ export class TestflowService { signal?: AbortSignal, ) { try { - const response = await this.httpService.makeHttpRequest({ + const response = await this.makeHttpRequest({ url, method, headers, @@ -62,8 +375,27 @@ export class TestflowService { }); } + private findConnectedNodes = ( + adj: any[], + start: number, + nodes:TestflowNodes[], + result:TestflowNodes[], + visited = new Set(), + ) => { + if (visited.has(start)) return; + for (let i = 0; i < nodes.length; i++) { + if (Number(nodes[i].id) === start) { + result.push(nodes[i]); + } + } + visited.add(start); + for (const neighbor of adj[start]) { + this.findConnectedNodes(adj, neighbor, nodes, result, visited); + } + }; + async runTestflow(payload: TestflowRunDto) { - const { nodes, variables } = payload; + const { nodes, variables, edges } = payload; const abortController = new AbortController(); const { signal } = abortController; let successRequests = 0; @@ -82,7 +414,21 @@ export class TestflowService { let requestChainResponse: Record = {}; const executedNodes: any[] = []; const environmentVariables = variables || []; - for (const element of nodes) { + let runningNodes: any[] = []; + let maxNodeId = 1; + for (let i = 0; i < nodes.length; i++) { + maxNodeId = Math.max(maxNodeId, Number(nodes[i].id)); + } + // Initialize adjacency list + const graph = Array.from({ length: maxNodeId + 1 }, () => []); + // Populate adjacency list + for (let i = 0; i < edges.length; i++) { + graph[Number(edges[i].source)].push(Number(edges[i].target)); + } + let result = []; + this.findConnectedNodes(graph, Number("1"), nodes, result); + runningNodes = [...result]; + for (const element of runningNodes) { if (element?.type !== "requestBlock" || !element?.data?.requestData) { continue; } @@ -101,7 +447,6 @@ export class TestflowService { const start = Date.now(); let resData: any; try { - console.log("🌐 Sending request:", { url, method, headers, body, contentType }); const response: any = await this.makeRequest( url, method, @@ -141,11 +486,18 @@ export class TestflowService { failedRequests++; } totalTime += duration; + const resBody = JSON.parse(response.data.body); history.requests.push({ method: requestData.method as string, name: requestData.name as string, status: resData.status, time: new ParseTime().convertMilliseconds(duration), + ...(statusCode < 200 || statusCode >= 300 + ? { + errorMessage:resBody?.message, + error: resBody?.error || undefined, + } + : {}), }); // Build chaining object const responseHeader = @@ -225,6 +577,8 @@ export class TestflowService { name: requestData.name as string, status: ResponseStatusCode.ERROR, time: new ParseTime().convertMilliseconds(duration), + errorMessage:response.message, + error:response.error }); } } catch (error) { @@ -234,19 +588,21 @@ export class TestflowService { break; } resData = { - body: error?.message || "Request failed", - headers: [], - status: ResponseStatusCode.ERROR, - time: duration, - size: 0, + body: error?.message || "Request failed", + headers: [], + status: ResponseStatusCode.ERROR, + time: duration, + size: 0, }; failedRequests++; totalTime += duration; history.requests.push({ - method: requestData.method as string, - name: requestData.name as string, - status: ResponseStatusCode.ERROR, - time: new ParseTime().convertMilliseconds(duration), + method: requestData.method as string, + name: requestData.name as string, + status: ResponseStatusCode.ERROR, + time: new ParseTime().convertMilliseconds(duration), + errorMessage:error.message, + error:error }); } executedNodes.push({ @@ -261,6 +617,8 @@ export class TestflowService { name: requestData?.name || "Unknown Request", status: ResponseStatusCode.ERROR, time: "0 ms", + errorMessage:error.message, + error:error }); executedNodes.push({ id: element.id, @@ -281,7 +639,6 @@ export class TestflowService { history.successRequests = successRequests; history.failedRequests = failedRequests; history.status = failedRequests === 0 ? "pass" : "fail"; - console.log("Final history:", history); return { history, requestChainResponse, diff --git a/src/utils/base64Converter.ts b/src/utils/base64Converter.ts deleted file mode 100644 index e079407..0000000 --- a/src/utils/base64Converter.ts +++ /dev/null @@ -1,50 +0,0 @@ -export class Base64Converter { - /** - * Converts a File object to a Base64 string. - * @param file - The File object to convert. - * @returns A Promise that resolves to the Base64 string. - */ - public fileToBase64(file: File): Promise { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - - reader.onload = () => { - if (reader.result) { - resolve(reader.result as string); - } else { - reject(new Error("Failed to convert file to Base64")); - } - }; - - reader.onerror = () => { - reject(new Error("Error reading file")); - }; - - reader.readAsDataURL(file); - }); - } - - /** - * Converts a Base64 string back to a File object. - * Extracts the MIME type automatically from the string. - * @param base64 - The Base64 string. - * @param fileName - The name for the new file. - * @returns A File object. - */ - public base64ToFile(base64: string, fileName: string): File { - const [metadata, data] = base64.split(","); - const mimeType = - metadata.match(/data:(.*?);base64/)?.[1] || "application/octet-stream"; - const byteString = atob(data); - const byteNumbers = new Array(byteString.length); - - for (let i = 0; i < byteString.length; i++) { - byteNumbers[i] = byteString.charCodeAt(i); - } - - const byteArray = new Uint8Array(byteNumbers); - const blob = new Blob([byteArray], { type: mimeType }); - - return new File([blob], fileName, { type: mimeType }); - } -} diff --git a/src/utils/status-code.ts b/src/utils/status-code.ts deleted file mode 100644 index 0b10646..0000000 --- a/src/utils/status-code.ts +++ /dev/null @@ -1,80 +0,0 @@ -export class StatusCode { - private statusMessages: Record = { - // 1xx Informational - 100: "Continue", - 101: "Switching Protocols", - 102: "Processing", - 103: "Early Hints", - - // 2xx Success - 200: "OK", - 201: "Created", - 202: "Accepted", - 203: "Non-Authoritative Information", - 204: "No Content", - 205: "Reset Content", - 206: "Partial Content", - 207: "Multi-Status", - 208: "Already Reported", - 226: "IM Used", - - // 3xx Redirection - 300: "Multiple Choices", - 301: "Moved Permanently", - 302: "Found", - 303: "See Other", - 304: "Not Modified", - 305: "Use Proxy", - 307: "Temporary Redirect", - 308: "Permanent Redirect", - - // 4xx Client Errors - 400: "Bad Request", - 401: "Unauthorized", - 402: "Payment Required", - 403: "Forbidden", - 404: "Not Found", - 405: "Method Not Allowed", - 406: "Not Acceptable", - 407: "Proxy Authentication Required", - 408: "Request Timeout", - 409: "Conflict", - 410: "Gone", - 411: "Length Required", - 412: "Precondition Failed", - 413: "Payload Too Large", - 414: "URI Too Long", - 415: "Unsupported Media Type", - 416: "Range Not Satisfiable", - 417: "Expectation Failed", - 418: "I'm a teapot", - 421: "Misdirected Request", - 422: "Unprocessable Entity", - 423: "Locked", - 424: "Failed Dependency", - 425: "Too Early", - 426: "Upgrade Required", - 428: "Precondition Required", - 429: "Too Many Requests", - 431: "Request Header Fields Too Large", - 451: "Unavailable For Legal Reasons", - - // 5xx Server Errors - 500: "Internal Server Error", - 501: "Not Implemented", - 502: "Bad Gateway", - 503: "Service Unavailable", - 504: "Gateway Timeout", - 505: "HTTP Version Not Supported", - 506: "Variant Also Negotiates", - 507: "Insufficient Storage", - 508: "Loop Detected", - 510: "Not Extended", - 511: "Network Authentication Required", - }; - - public getText(status: number): string { - return this.statusMessages[status] || "Unknown Status"; - } - } - \ No newline at end of file From a8500253df8c6d661ed02c598589bb4b0de692b2 Mon Sep 17 00:00:00 2001 From: Aakash Reddy Date: Wed, 1 Oct 2025 15:47:57 +0530 Subject: [PATCH 13/17] feat: testflow response added --- src/enum/testflow.enum.ts | 9 +++++++ src/payloads/testflow.payload.ts | 37 +++++++++++++++++++++++++- src/proxy/testflow/testflow.service.ts | 23 ++++++++++++++++ 3 files changed, 68 insertions(+), 1 deletion(-) diff --git a/src/enum/testflow.enum.ts b/src/enum/testflow.enum.ts index f3f0730..9ce3254 100644 --- a/src/enum/testflow.enum.ts +++ b/src/enum/testflow.enum.ts @@ -47,4 +47,13 @@ export interface TFAPIResponseType { body?: string; headers?: object; status?: string; +} + +export enum RequestDataTypeEnum { + JSON = "JSON", + XML = "XML", + HTML = "HTML", + TEXT = "Text", + JAVASCRIPT = "JavaScript", + IMAGE = "Image", } \ No newline at end of file diff --git a/src/payloads/testflow.payload.ts b/src/payloads/testflow.payload.ts index 0885314..9e3e523 100644 --- a/src/payloads/testflow.payload.ts +++ b/src/payloads/testflow.payload.ts @@ -13,7 +13,7 @@ import { IsDate, } from 'class-validator'; import { HTTPMethods } from "fastify"; -import { BodyModeEnum,} from 'src/enum/testflow.enum'; +import { BodyModeEnum, RequestDataTypeEnum,} from 'src/enum/testflow.enum'; import { AuthModeEnum } from 'src/enum/testflow.enum'; import { Auth } from 'src/enum/testflow.enum'; @@ -253,6 +253,37 @@ export class TestflowSchedularHistoryRequest { error?: string; } +export class TFKeyValueStoreDto { + @IsString() + key: string; + + @IsString() + value: string; +} + +export class TestflowSchedularHistoryResponse { + @IsArray() + @ValidateNested({ each: true }) + @Type(() => TFKeyValueStoreDto) + headers: TFKeyValueStoreDto[]; + + @IsString() + status: string; + + @IsString() + body: string; + + @IsNumber() + time: number; + + @IsNumber() + size: number; + + @IsOptional() + @IsString() + responseContentType?: RequestDataTypeEnum; +} + export class TestFlowSchedularRunHistory { @IsString() @IsNotEmpty() @@ -262,6 +293,10 @@ export class TestFlowSchedularRunHistory { @IsOptional() requests?: TestflowSchedularHistoryRequest[]; + @IsArray() + @IsOptional() + responses?:TestflowSchedularHistoryResponse[]; + @IsString() @IsNotEmpty() status: string; diff --git a/src/proxy/testflow/testflow.service.ts b/src/proxy/testflow/testflow.service.ts index eefaf81..e8dfc97 100644 --- a/src/proxy/testflow/testflow.service.ts +++ b/src/proxy/testflow/testflow.service.ts @@ -409,6 +409,7 @@ export class TestflowService { createdAt: new Date(), createdBy: payload.userId, requests: [], + responses:[] }; let requestChainResponse: Record = {}; @@ -499,6 +500,14 @@ export class TestflowService { } : {}), }); + history.responses.push({ + headers: resData.headers, + status: resData.status, + body: resData.body, + time: resData.time, + size: resData.size, + responseContentType: resData.responseContentType , + }) // Build chaining object const responseHeader = this._decodeRequest.setResponseContentType(formattedHeaders); @@ -580,6 +589,13 @@ export class TestflowService { errorMessage:response.message, error:response.error }); + history.responses.push({ + headers: resData.headers, + status: resData.status, + body: resData.body, + time: resData.time, + size: resData.size, + }) } } catch (error) { const duration = Date.now() - start; @@ -604,6 +620,13 @@ export class TestflowService { errorMessage:error.message, error:error }); + history.responses.push({ + headers: resData.headers, + status: resData.status, + body: resData.body, + time: resData.time, + size: resData.size, + }) } executedNodes.push({ id: element.id, From 4f5792edf9a5b1991bea3ffe4dbf4fcb7d1e29fa Mon Sep 17 00:00:00 2001 From: Aakash Reddy Date: Fri, 3 Oct 2025 11:14:38 +0530 Subject: [PATCH 14/17] feat: removed unwanted prasing --- src/proxy/testflow/testflow.service.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/proxy/testflow/testflow.service.ts b/src/proxy/testflow/testflow.service.ts index e8dfc97..173b96d 100644 --- a/src/proxy/testflow/testflow.service.ts +++ b/src/proxy/testflow/testflow.service.ts @@ -487,18 +487,11 @@ export class TestflowService { failedRequests++; } totalTime += duration; - const resBody = JSON.parse(response.data.body); history.requests.push({ method: requestData.method as string, name: requestData.name as string, status: resData.status, time: new ParseTime().convertMilliseconds(duration), - ...(statusCode < 200 || statusCode >= 300 - ? { - errorMessage:resBody?.message, - error: resBody?.error || undefined, - } - : {}), }); history.responses.push({ headers: resData.headers, From bb6b1892f443d4bd92a040a0cc8b47b52917c7ae Mon Sep 17 00:00:00 2001 From: Md Asif Raza Date: Fri, 3 Oct 2025 15:12:21 +0530 Subject: [PATCH 15/17] feat: upgrade app version to 2.32.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c3b36e5..185258b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sparrow-proxy", - "version": "2.29.0", + "version": "2.32.1", "description": "", "author": "", "private": true, From 0359ccd7117358126e0f8c4739f9223b1da8a693 Mon Sep 17 00:00:00 2001 From: Aakash Reddy Date: Thu, 9 Oct 2025 13:38:08 +0530 Subject: [PATCH 16/17] fix: converting into string --- src/proxy/testflow/testflow.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/proxy/testflow/testflow.service.ts b/src/proxy/testflow/testflow.service.ts index 173b96d..4afe1ee 100644 --- a/src/proxy/testflow/testflow.service.ts +++ b/src/proxy/testflow/testflow.service.ts @@ -317,13 +317,13 @@ export class TestflowService { (axiosError.response?.statusText || this.getStatusText(axiosError.response?.status)) : null, - data: responseData || { message: axiosError.message }, + data: `${responseData}` || JSON.stringify({ message: axiosError.message }), headers: axiosError.response?.headers, }; } catch (e) { return { status: null, - data: { message: axiosError.message }, + data: JSON.stringify({ message: axiosError.message }), headers: axiosError.response?.headers, }; } From 6e84212237fae4861c44af6ebf92eaf93a8455e3 Mon Sep 17 00:00:00 2001 From: Md Asif Raza Date: Wed, 15 Oct 2025 14:18:54 +0530 Subject: [PATCH 17/17] feat: version upgrade to 2.33.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 04aa80b..4fa8845 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sparrow-proxy", - "version": "2.32.1", + "version": "2.33.0", "description": "", "author": "", "private": true,