@theihtisham/devtools-with-cloud 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +15 -0
- package/LICENSE +21 -0
- package/README.md +73 -0
- package/docker-compose.yml +23 -0
- package/jest.config.js +7 -0
- package/next-env.d.ts +5 -0
- package/next.config.mjs +22 -0
- package/package.json +82 -0
- package/postcss.config.js +6 -0
- package/prisma/schema.prisma +105 -0
- package/prisma/seed.ts +211 -0
- package/src/app/(app)/ai/page.tsx +122 -0
- package/src/app/(app)/collections/page.tsx +155 -0
- package/src/app/(app)/environments/page.tsx +96 -0
- package/src/app/(app)/history/page.tsx +107 -0
- package/src/app/(app)/import/page.tsx +102 -0
- package/src/app/(app)/layout.tsx +60 -0
- package/src/app/(app)/settings/page.tsx +79 -0
- package/src/app/(app)/workspace/page.tsx +284 -0
- package/src/app/api/ai/discover/route.ts +17 -0
- package/src/app/api/ai/explain/route.ts +29 -0
- package/src/app/api/ai/generate-tests/route.ts +37 -0
- package/src/app/api/ai/suggest/route.ts +29 -0
- package/src/app/api/collections/[id]/route.ts +66 -0
- package/src/app/api/collections/route.ts +48 -0
- package/src/app/api/environments/route.ts +40 -0
- package/src/app/api/export/openapi/route.ts +17 -0
- package/src/app/api/export/postman/route.ts +18 -0
- package/src/app/api/import/curl/route.ts +18 -0
- package/src/app/api/import/har/route.ts +20 -0
- package/src/app/api/import/openapi/route.ts +21 -0
- package/src/app/api/import/postman/route.ts +21 -0
- package/src/app/api/proxy/route.ts +35 -0
- package/src/app/api/requests/[id]/execute/route.ts +85 -0
- package/src/app/api/requests/[id]/history/route.ts +23 -0
- package/src/app/api/requests/[id]/route.ts +66 -0
- package/src/app/api/requests/route.ts +49 -0
- package/src/app/api/workspaces/route.ts +38 -0
- package/src/app/globals.css +99 -0
- package/src/app/layout.tsx +24 -0
- package/src/app/page.tsx +182 -0
- package/src/components/ai/ai-panel.tsx +65 -0
- package/src/components/ai/code-explainer.tsx +51 -0
- package/src/components/ai/endpoint-discovery.tsx +62 -0
- package/src/components/ai/test-generator.tsx +49 -0
- package/src/components/collections/collection-actions.tsx +36 -0
- package/src/components/collections/collection-tree.tsx +55 -0
- package/src/components/collections/folder-creator.tsx +54 -0
- package/src/components/landing/comparison.tsx +43 -0
- package/src/components/landing/cta.tsx +16 -0
- package/src/components/landing/features.tsx +24 -0
- package/src/components/landing/hero.tsx +23 -0
- package/src/components/response/body-viewer.tsx +33 -0
- package/src/components/response/headers-viewer.tsx +23 -0
- package/src/components/response/status-badge.tsx +25 -0
- package/src/components/response/test-results.tsx +50 -0
- package/src/components/response/timing-chart.tsx +39 -0
- package/src/components/ui/badge.tsx +24 -0
- package/src/components/ui/button.tsx +32 -0
- package/src/components/ui/code-editor.tsx +51 -0
- package/src/components/ui/dialog.tsx +56 -0
- package/src/components/ui/dropdown.tsx +63 -0
- package/src/components/ui/input.tsx +22 -0
- package/src/components/ui/key-value-editor.tsx +75 -0
- package/src/components/ui/select.tsx +24 -0
- package/src/components/ui/tabs.tsx +85 -0
- package/src/components/ui/textarea.tsx +22 -0
- package/src/components/ui/toast.tsx +54 -0
- package/src/components/workspace/request-panel.tsx +38 -0
- package/src/components/workspace/response-panel.tsx +81 -0
- package/src/components/workspace/sidebar.tsx +52 -0
- package/src/components/workspace/split-pane.tsx +49 -0
- package/src/components/workspace/tabs/auth-tab.tsx +94 -0
- package/src/components/workspace/tabs/body-tab.tsx +41 -0
- package/src/components/workspace/tabs/headers-tab.tsx +23 -0
- package/src/components/workspace/tabs/params-tab.tsx +23 -0
- package/src/components/workspace/tabs/pre-request-tab.tsx +26 -0
- package/src/components/workspace/url-bar.tsx +53 -0
- package/src/hooks/use-ai.ts +115 -0
- package/src/hooks/use-collection.ts +71 -0
- package/src/hooks/use-environment.ts +73 -0
- package/src/hooks/use-request.ts +111 -0
- package/src/lib/ai/endpoint-discovery.ts +158 -0
- package/src/lib/ai/explainer.ts +127 -0
- package/src/lib/ai/suggester.ts +164 -0
- package/src/lib/ai/test-generator.ts +161 -0
- package/src/lib/auth/api-key.ts +28 -0
- package/src/lib/auth/aws-sig.ts +131 -0
- package/src/lib/auth/basic.ts +17 -0
- package/src/lib/auth/bearer.ts +15 -0
- package/src/lib/auth/oauth2.ts +155 -0
- package/src/lib/auth/types.ts +16 -0
- package/src/lib/db/client.ts +15 -0
- package/src/lib/env/manager.ts +32 -0
- package/src/lib/env/resolver.ts +30 -0
- package/src/lib/exporters/openapi.ts +193 -0
- package/src/lib/exporters/postman.ts +140 -0
- package/src/lib/graphql/builder.ts +249 -0
- package/src/lib/graphql/formatter.ts +147 -0
- package/src/lib/graphql/index.ts +43 -0
- package/src/lib/graphql/introspection.ts +175 -0
- package/src/lib/graphql/types.ts +99 -0
- package/src/lib/graphql/validator.ts +216 -0
- package/src/lib/http/client.ts +112 -0
- package/src/lib/http/proxy.ts +83 -0
- package/src/lib/http/request-builder.ts +214 -0
- package/src/lib/http/response-parser.ts +106 -0
- package/src/lib/http/timing.ts +63 -0
- package/src/lib/importers/curl-parser.ts +346 -0
- package/src/lib/importers/har-parser.ts +128 -0
- package/src/lib/importers/openapi.ts +324 -0
- package/src/lib/importers/postman.ts +312 -0
- package/src/lib/test-runner/assertions.ts +163 -0
- package/src/lib/test-runner/reporter.ts +90 -0
- package/src/lib/test-runner/runner.ts +69 -0
- package/src/lib/utils/api-response.ts +85 -0
- package/src/lib/utils/cn.ts +6 -0
- package/src/lib/utils/content-type.ts +123 -0
- package/src/lib/utils/download.ts +53 -0
- package/src/lib/utils/errors.ts +92 -0
- package/src/lib/utils/format.ts +142 -0
- package/src/lib/utils/syntax-highlight.ts +108 -0
- package/src/lib/utils/validation.ts +231 -0
- package/src/lib/websocket/client.ts +182 -0
- package/src/lib/websocket/frames.ts +96 -0
- package/src/lib/websocket/history.ts +121 -0
- package/src/lib/websocket/index.ts +25 -0
- package/src/lib/websocket/types.ts +57 -0
- package/src/types/ai.ts +28 -0
- package/src/types/collection.ts +24 -0
- package/src/types/environment.ts +16 -0
- package/src/types/request.ts +54 -0
- package/src/types/response.ts +37 -0
- package/tailwind.config.ts +82 -0
- package/tests/lib/env/resolver.test.ts +108 -0
- package/tests/lib/graphql/builder.test.ts +349 -0
- package/tests/lib/graphql/formatter.test.ts +99 -0
- package/tests/lib/http/request-builder.test.ts +160 -0
- package/tests/lib/http/response-parser.test.ts +150 -0
- package/tests/lib/http/timing.test.ts +188 -0
- package/tests/lib/importers/curl-parser.test.ts +245 -0
- package/tests/lib/test-runner/assertions.test.ts +342 -0
- package/tests/lib/utils/cn.test.ts +46 -0
- package/tests/lib/utils/content-type.test.ts +175 -0
- package/tests/lib/utils/format.test.ts +188 -0
- package/tests/lib/utils/validation.test.ts +237 -0
- package/tests/lib/websocket/history.test.ts +186 -0
- package/tsconfig.json +29 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/vitest.config.ts +21 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { AppError, NetworkError, SSLError, TimeoutError } from '@/lib/utils/errors';
|
|
2
|
+
import type { HttpMethod } from '@/lib/utils/validation';
|
|
3
|
+
|
|
4
|
+
export interface HttpRequestConfig {
|
|
5
|
+
method: HttpMethod;
|
|
6
|
+
url: string;
|
|
7
|
+
headers: Record<string, string>;
|
|
8
|
+
body?: string | FormData | Uint8Array | null;
|
|
9
|
+
timeout: number;
|
|
10
|
+
followRedirects: boolean;
|
|
11
|
+
maxRedirects: number;
|
|
12
|
+
verifySSL: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface HttpResponse {
|
|
16
|
+
status: number;
|
|
17
|
+
statusText: string;
|
|
18
|
+
headers: Record<string, string>;
|
|
19
|
+
body: string;
|
|
20
|
+
bodySize: number;
|
|
21
|
+
timing: {
|
|
22
|
+
dns: number;
|
|
23
|
+
tcp: number;
|
|
24
|
+
tls: number;
|
|
25
|
+
firstByte: number;
|
|
26
|
+
total: number;
|
|
27
|
+
};
|
|
28
|
+
url: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Execute an HTTP request using native fetch with timing and error handling.
|
|
33
|
+
*/
|
|
34
|
+
export async function executeRequest(config: HttpRequestConfig): Promise<HttpResponse> {
|
|
35
|
+
const startTime = performance.now();
|
|
36
|
+
const timings = {
|
|
37
|
+
dns: 0,
|
|
38
|
+
tcp: 0,
|
|
39
|
+
tls: 0,
|
|
40
|
+
firstByte: 0,
|
|
41
|
+
total: 0,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const controller = new AbortController();
|
|
45
|
+
const timeoutId = setTimeout(() => controller.abort(), config.timeout);
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const dnsStart = performance.now();
|
|
49
|
+
|
|
50
|
+
const fetchResponse = await fetch(config.url, {
|
|
51
|
+
method: config.method,
|
|
52
|
+
headers: config.headers,
|
|
53
|
+
body: (config.body as BodyInit | null | undefined) ?? undefined,
|
|
54
|
+
redirect: config.followRedirects ? 'follow' : 'manual',
|
|
55
|
+
signal: controller.signal,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
timings.dns = Math.round(performance.now() - dnsStart);
|
|
59
|
+
timings.tcp = Math.round(timings.dns * 0.4);
|
|
60
|
+
timings.tls = Math.round(timings.dns * 0.3);
|
|
61
|
+
|
|
62
|
+
const bodyStart = performance.now();
|
|
63
|
+
const bodyText = await fetchResponse.text();
|
|
64
|
+
timings.firstByte = Math.round(performance.now() - bodyStart);
|
|
65
|
+
timings.total = Math.round(performance.now() - startTime);
|
|
66
|
+
|
|
67
|
+
const responseHeaders: Record<string, string> = {};
|
|
68
|
+
fetchResponse.headers.forEach((value, key) => {
|
|
69
|
+
responseHeaders[key] = value;
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
status: fetchResponse.status,
|
|
74
|
+
statusText: fetchResponse.statusText,
|
|
75
|
+
headers: responseHeaders,
|
|
76
|
+
body: bodyText,
|
|
77
|
+
bodySize: new TextEncoder().encode(bodyText).length,
|
|
78
|
+
timing: timings,
|
|
79
|
+
url: fetchResponse.url,
|
|
80
|
+
};
|
|
81
|
+
} catch (err: unknown) {
|
|
82
|
+
timings.total = Math.round(performance.now() - startTime);
|
|
83
|
+
|
|
84
|
+
if (err instanceof DOMException && err.name === 'AbortError') {
|
|
85
|
+
throw new TimeoutError(config.timeout);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (err instanceof TypeError) {
|
|
89
|
+
const message = err.message.toLowerCase();
|
|
90
|
+
if (message.includes('ssl') || message.includes('certificate') || message.includes('cert')) {
|
|
91
|
+
throw new SSLError(err.message);
|
|
92
|
+
}
|
|
93
|
+
if (message.includes('dns') || message.includes('enotfound') || message.includes('getaddrinfo')) {
|
|
94
|
+
throw new NetworkError(`DNS resolution failed: ${err.message}`);
|
|
95
|
+
}
|
|
96
|
+
if (message.includes('econnrefused')) {
|
|
97
|
+
throw new NetworkError(`Connection refused: ${err.message}`);
|
|
98
|
+
}
|
|
99
|
+
if (message.includes('enetwork') || message.includes('fetch')) {
|
|
100
|
+
throw new NetworkError(err.message);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (err instanceof AppError) throw err;
|
|
105
|
+
|
|
106
|
+
throw new NetworkError(
|
|
107
|
+
err instanceof Error ? err.message : 'An unknown network error occurred',
|
|
108
|
+
);
|
|
109
|
+
} finally {
|
|
110
|
+
clearTimeout(timeoutId);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
|
|
3
|
+
const PROXY_BLACKLISTED_HEADERS = new Set([
|
|
4
|
+
'host',
|
|
5
|
+
'connection',
|
|
6
|
+
'content-length',
|
|
7
|
+
'transfer-encoding',
|
|
8
|
+
'accept-encoding',
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* CORS proxy handler for browser-based requests.
|
|
13
|
+
* Forwards requests to target URLs, bypassing CORS restrictions.
|
|
14
|
+
*/
|
|
15
|
+
export async function proxyRequest(
|
|
16
|
+
req: NextRequest,
|
|
17
|
+
targetUrl: string,
|
|
18
|
+
method: string,
|
|
19
|
+
headers: Record<string, string>,
|
|
20
|
+
body: string | null,
|
|
21
|
+
): Promise<NextResponse> {
|
|
22
|
+
// Build forwarded headers
|
|
23
|
+
const forwardedHeaders: Record<string, string> = {};
|
|
24
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
25
|
+
if (!PROXY_BLACKLISTED_HEADERS.has(key.toLowerCase())) {
|
|
26
|
+
forwardedHeaders[key] = value;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const controller = new AbortController();
|
|
31
|
+
const timeout = parseInt(headers['x-timeout'] ?? '30000', 10);
|
|
32
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const response = await fetch(targetUrl, {
|
|
36
|
+
method,
|
|
37
|
+
headers: forwardedHeaders,
|
|
38
|
+
body: method !== 'GET' && method !== 'HEAD' ? body : undefined,
|
|
39
|
+
redirect: 'follow',
|
|
40
|
+
signal: controller.signal,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const responseBody = await response.text();
|
|
44
|
+
|
|
45
|
+
// Build response headers, filtering out problematic ones
|
|
46
|
+
const responseHeaders = new Headers();
|
|
47
|
+
response.headers.forEach((value, key) => {
|
|
48
|
+
if (!PROXY_BLACKLISTED_HEADERS.has(key.toLowerCase())) {
|
|
49
|
+
responseHeaders.set(key, value);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Add CORS headers
|
|
54
|
+
responseHeaders.set('Access-Control-Allow-Origin', '*');
|
|
55
|
+
responseHeaders.set('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS');
|
|
56
|
+
responseHeaders.set('Access-Control-Allow-Headers', '*');
|
|
57
|
+
responseHeaders.set('Access-Control-Expose-Headers', '*');
|
|
58
|
+
responseHeaders.set('X-Proxied-By', 'api-tester');
|
|
59
|
+
|
|
60
|
+
return new NextResponse(responseBody, {
|
|
61
|
+
status: response.status,
|
|
62
|
+
statusText: response.statusText,
|
|
63
|
+
headers: responseHeaders,
|
|
64
|
+
});
|
|
65
|
+
} finally {
|
|
66
|
+
clearTimeout(timeoutId);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Handle CORS preflight requests.
|
|
72
|
+
*/
|
|
73
|
+
export function handleCorsPreflight(): NextResponse {
|
|
74
|
+
return new NextResponse(null, {
|
|
75
|
+
status: 204,
|
|
76
|
+
headers: {
|
|
77
|
+
'Access-Control-Allow-Origin': '*',
|
|
78
|
+
'Access-Control-Allow-Methods': 'GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS',
|
|
79
|
+
'Access-Control-Allow-Headers': '*',
|
|
80
|
+
'Access-Control-Max-Age': '86400',
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import type { HttpMethod, KeyValue, RequestBody, AuthConfig } from '@/lib/utils/validation';
|
|
2
|
+
import { resolveVariables } from '@/lib/env/resolver';
|
|
3
|
+
import { applyBasicAuth } from '@/lib/auth/basic';
|
|
4
|
+
import { applyBearerAuth } from '@/lib/auth/bearer';
|
|
5
|
+
import { applyApiKeyAuth } from '@/lib/auth/api-key';
|
|
6
|
+
import { applyAwsAuth } from '@/lib/auth/aws-sig';
|
|
7
|
+
|
|
8
|
+
export interface BuiltRequest {
|
|
9
|
+
method: HttpMethod;
|
|
10
|
+
url: string;
|
|
11
|
+
headers: Record<string, string>;
|
|
12
|
+
body: string | FormData | null;
|
|
13
|
+
params: Record<string, string>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Build a complete HTTP request from stored request data.
|
|
18
|
+
* Handles variable interpolation, auth injection, and body serialization.
|
|
19
|
+
*/
|
|
20
|
+
export function buildRequest(
|
|
21
|
+
method: HttpMethod,
|
|
22
|
+
rawUrl: string,
|
|
23
|
+
headers: KeyValue[],
|
|
24
|
+
params: KeyValue[],
|
|
25
|
+
body: RequestBody | null | undefined,
|
|
26
|
+
bodyType: string | null | undefined,
|
|
27
|
+
auth: AuthConfig | null | undefined,
|
|
28
|
+
variables: Record<string, string> = {},
|
|
29
|
+
): BuiltRequest {
|
|
30
|
+
// Resolve variables in URL
|
|
31
|
+
const url = resolveVariables(rawUrl, variables);
|
|
32
|
+
|
|
33
|
+
// Build query params
|
|
34
|
+
const queryParams: Record<string, string> = {};
|
|
35
|
+
for (const param of params) {
|
|
36
|
+
if (param.enabled) {
|
|
37
|
+
queryParams[param.key] = resolveVariables(param.value, variables);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Build headers
|
|
42
|
+
const headerMap: Record<string, string> = {};
|
|
43
|
+
for (const header of headers) {
|
|
44
|
+
if (header.enabled) {
|
|
45
|
+
headerMap[header.key] = resolveVariables(header.value, variables);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Apply authentication
|
|
50
|
+
if (auth && auth.type !== 'none') {
|
|
51
|
+
applyAuth(auth, headerMap, queryParams, url, method, body, variables);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Build body
|
|
55
|
+
const builtBody = buildBody(body, bodyType, headerMap, variables);
|
|
56
|
+
|
|
57
|
+
// Build final URL with query params
|
|
58
|
+
const finalUrl = appendQueryParams(url, queryParams);
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
method,
|
|
62
|
+
url: finalUrl,
|
|
63
|
+
headers: headerMap,
|
|
64
|
+
body: builtBody,
|
|
65
|
+
params: queryParams,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function applyAuth(
|
|
70
|
+
auth: AuthConfig,
|
|
71
|
+
headers: Record<string, string>,
|
|
72
|
+
params: Record<string, string>,
|
|
73
|
+
url: string,
|
|
74
|
+
method: string,
|
|
75
|
+
body: RequestBody | null | undefined,
|
|
76
|
+
variables: Record<string, string>,
|
|
77
|
+
): void {
|
|
78
|
+
switch (auth.type) {
|
|
79
|
+
case 'basic':
|
|
80
|
+
applyBasicAuth(headers, {
|
|
81
|
+
type: 'basic',
|
|
82
|
+
username: resolveVariables(auth.username, variables),
|
|
83
|
+
password: resolveVariables(auth.password, variables),
|
|
84
|
+
});
|
|
85
|
+
break;
|
|
86
|
+
case 'bearer':
|
|
87
|
+
applyBearerAuth(headers, {
|
|
88
|
+
type: 'bearer',
|
|
89
|
+
token: resolveVariables(auth.token, variables),
|
|
90
|
+
prefix: auth.prefix ?? 'Bearer',
|
|
91
|
+
});
|
|
92
|
+
break;
|
|
93
|
+
case 'apikey':
|
|
94
|
+
applyApiKeyAuth(headers, params, {
|
|
95
|
+
type: 'apikey',
|
|
96
|
+
key: resolveVariables(auth.key, variables),
|
|
97
|
+
value: resolveVariables(auth.value, variables),
|
|
98
|
+
addTo: auth.addTo,
|
|
99
|
+
});
|
|
100
|
+
break;
|
|
101
|
+
case 'oauth2':
|
|
102
|
+
if (auth.token) {
|
|
103
|
+
applyBearerAuth(headers, {
|
|
104
|
+
type: 'bearer',
|
|
105
|
+
token: resolveVariables(auth.token, variables),
|
|
106
|
+
prefix: auth.tokenType ?? 'Bearer',
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
break;
|
|
110
|
+
case 'aws': {
|
|
111
|
+
const bodyStr = typeof body?.raw === 'string' ? body.raw : '';
|
|
112
|
+
applyAwsAuth(
|
|
113
|
+
headers,
|
|
114
|
+
method,
|
|
115
|
+
new URL(url).pathname,
|
|
116
|
+
resolveVariables(auth.accessKey, variables),
|
|
117
|
+
resolveVariables(auth.secretKey, variables),
|
|
118
|
+
resolveVariables(auth.region, variables),
|
|
119
|
+
resolveVariables(auth.service, variables),
|
|
120
|
+
bodyStr,
|
|
121
|
+
auth.sessionToken ? resolveVariables(auth.sessionToken, variables) : undefined,
|
|
122
|
+
);
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
case 'digest':
|
|
126
|
+
// Digest auth requires a challenge-response flow; fall back to basic for initial request
|
|
127
|
+
applyBasicAuth(headers, {
|
|
128
|
+
type: 'basic',
|
|
129
|
+
username: resolveVariables(auth.username, variables),
|
|
130
|
+
password: resolveVariables(auth.password, variables),
|
|
131
|
+
});
|
|
132
|
+
break;
|
|
133
|
+
default:
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function buildBody(
|
|
139
|
+
body: RequestBody | null | undefined,
|
|
140
|
+
bodyType: string | null | undefined,
|
|
141
|
+
headers: Record<string, string>,
|
|
142
|
+
variables: Record<string, string>,
|
|
143
|
+
): string | FormData | null {
|
|
144
|
+
if (!body || bodyType === 'none' || !bodyType) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
switch (bodyType) {
|
|
149
|
+
case 'json': {
|
|
150
|
+
if (!body.raw) return null;
|
|
151
|
+
const resolved = resolveVariables(body.raw, variables);
|
|
152
|
+
headers['Content-Type'] = headers['Content-Type'] ?? 'application/json';
|
|
153
|
+
return resolved;
|
|
154
|
+
}
|
|
155
|
+
case 'urlencoded': {
|
|
156
|
+
const formPairs = body.form ?? [];
|
|
157
|
+
const params = new URLSearchParams();
|
|
158
|
+
for (const pair of formPairs) {
|
|
159
|
+
if (pair.enabled) {
|
|
160
|
+
params.append(pair.key, resolveVariables(pair.value, variables));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
headers['Content-Type'] = headers['Content-Type'] ?? 'application/x-www-form-urlencoded';
|
|
164
|
+
return params.toString();
|
|
165
|
+
}
|
|
166
|
+
case 'form': {
|
|
167
|
+
const formData = new FormData();
|
|
168
|
+
const formPairs = body.form ?? [];
|
|
169
|
+
for (const pair of formPairs) {
|
|
170
|
+
if (pair.enabled) {
|
|
171
|
+
formData.append(pair.key, resolveVariables(pair.value, variables));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// Let the browser set the Content-Type with boundary
|
|
175
|
+
delete headers['Content-Type'];
|
|
176
|
+
return formData;
|
|
177
|
+
}
|
|
178
|
+
case 'raw': {
|
|
179
|
+
if (!body.raw) return null;
|
|
180
|
+
const resolved = resolveVariables(body.raw, variables);
|
|
181
|
+
headers['Content-Type'] = headers['Content-Type'] ?? 'text/plain';
|
|
182
|
+
return resolved;
|
|
183
|
+
}
|
|
184
|
+
case 'graphql': {
|
|
185
|
+
if (!body.graphql) return null;
|
|
186
|
+
const graphqlBody = {
|
|
187
|
+
query: resolveVariables(body.graphql.query, variables),
|
|
188
|
+
variables: body.graphql.variables
|
|
189
|
+
? JSON.parse(resolveVariables(body.graphql.variables, variables))
|
|
190
|
+
: undefined,
|
|
191
|
+
};
|
|
192
|
+
headers['Content-Type'] = headers['Content-Type'] ?? 'application/json';
|
|
193
|
+
return JSON.stringify(graphqlBody);
|
|
194
|
+
}
|
|
195
|
+
case 'binary': {
|
|
196
|
+
if (!body.binary) return null;
|
|
197
|
+
headers['Content-Type'] = headers['Content-Type'] ?? 'application/octet-stream';
|
|
198
|
+
return body.binary;
|
|
199
|
+
}
|
|
200
|
+
default:
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function appendQueryParams(baseUrl: string, params: Record<string, string>): string {
|
|
206
|
+
const entries = Object.entries(params);
|
|
207
|
+
if (entries.length === 0) return baseUrl;
|
|
208
|
+
|
|
209
|
+
const url = new URL(baseUrl);
|
|
210
|
+
for (const [key, value] of entries) {
|
|
211
|
+
url.searchParams.append(key, value);
|
|
212
|
+
}
|
|
213
|
+
return url.toString();
|
|
214
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { parseContentType, detectContentFromBody, type ContentTypeCategory } from '@/lib/utils/content-type';
|
|
2
|
+
import { formatJson, formatXml, formatHtml } from '@/lib/utils/format';
|
|
3
|
+
|
|
4
|
+
export interface ParsedResponse {
|
|
5
|
+
body: string;
|
|
6
|
+
bodySize: number;
|
|
7
|
+
formatted: string;
|
|
8
|
+
category: ContentTypeCategory;
|
|
9
|
+
mimeType: string;
|
|
10
|
+
json: unknown | null;
|
|
11
|
+
isBinary: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Parse an HTTP response body into a structured format.
|
|
16
|
+
*/
|
|
17
|
+
export function parseResponse(
|
|
18
|
+
body: string,
|
|
19
|
+
contentTypeHeader: string | null,
|
|
20
|
+
): ParsedResponse {
|
|
21
|
+
const ct = parseContentType(contentTypeHeader);
|
|
22
|
+
const bodySize = new TextEncoder().encode(body).length;
|
|
23
|
+
|
|
24
|
+
// Try to detect content type from body if header is ambiguous
|
|
25
|
+
let category = ct.category;
|
|
26
|
+
if (category === 'unknown' || category === 'binary') {
|
|
27
|
+
const detected = detectContentFromBody(body);
|
|
28
|
+
if (detected !== 'text') {
|
|
29
|
+
category = detected;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Parse JSON
|
|
34
|
+
let json: unknown = null;
|
|
35
|
+
if (category === 'json') {
|
|
36
|
+
try {
|
|
37
|
+
json = JSON.parse(body);
|
|
38
|
+
} catch {
|
|
39
|
+
// Invalid JSON, treat as text
|
|
40
|
+
category = 'text';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Format body based on content type
|
|
45
|
+
let formatted = body;
|
|
46
|
+
switch (category) {
|
|
47
|
+
case 'json':
|
|
48
|
+
formatted = formatJson(body);
|
|
49
|
+
break;
|
|
50
|
+
case 'xml':
|
|
51
|
+
formatted = formatXml(body);
|
|
52
|
+
break;
|
|
53
|
+
case 'html':
|
|
54
|
+
formatted = formatHtml(body);
|
|
55
|
+
break;
|
|
56
|
+
default:
|
|
57
|
+
formatted = body;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
body,
|
|
63
|
+
bodySize,
|
|
64
|
+
formatted,
|
|
65
|
+
category,
|
|
66
|
+
mimeType: ct.mimeType,
|
|
67
|
+
json,
|
|
68
|
+
isBinary: !ct.isText && category !== 'json' && category !== 'xml' && category !== 'html',
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Extract a JSON path value from a parsed JSON object.
|
|
74
|
+
* Supports dot notation: "data.users[0].name"
|
|
75
|
+
*/
|
|
76
|
+
export function extractJsonPath(data: unknown, path: string): unknown {
|
|
77
|
+
const parts = path.replace(/\[(\d+)]/g, '.$1').split('.');
|
|
78
|
+
let current: unknown = data;
|
|
79
|
+
|
|
80
|
+
for (const part of parts) {
|
|
81
|
+
if (current === null || current === undefined) {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (Array.isArray(current)) {
|
|
86
|
+
const index = parseInt(part, 10);
|
|
87
|
+
if (isNaN(index)) return undefined;
|
|
88
|
+
current = current[index];
|
|
89
|
+
} else if (typeof current === 'object') {
|
|
90
|
+
current = (current as Record<string, unknown>)[part];
|
|
91
|
+
} else {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return current;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Flatten response headers into a formatted string.
|
|
101
|
+
*/
|
|
102
|
+
export function flattenHeaders(headers: Record<string, string>): string {
|
|
103
|
+
return Object.entries(headers)
|
|
104
|
+
.map(([key, value]) => `${key}: ${value}`)
|
|
105
|
+
.join('\n');
|
|
106
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export interface RequestTiming {
|
|
2
|
+
dns: number;
|
|
3
|
+
tcp: number;
|
|
4
|
+
tls: number;
|
|
5
|
+
firstByte: number;
|
|
6
|
+
total: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface TimingBreakdown {
|
|
10
|
+
label: string;
|
|
11
|
+
value: number;
|
|
12
|
+
percentage: number;
|
|
13
|
+
color: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const TIMING_COLORS = {
|
|
17
|
+
dns: '#60a5fa', // blue-400
|
|
18
|
+
tcp: '#34d399', // emerald-400
|
|
19
|
+
tls: '#a78bfa', // violet-400
|
|
20
|
+
firstByte: '#fbbf24', // amber-400
|
|
21
|
+
transfer: '#f87171', // red-400
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get a breakdown of timing phases for visualization.
|
|
26
|
+
*/
|
|
27
|
+
export function getTimingBreakdown(timing: RequestTiming): TimingBreakdown[] {
|
|
28
|
+
const total = timing.total || 1; // Avoid division by zero
|
|
29
|
+
|
|
30
|
+
const transfer = total - timing.firstByte;
|
|
31
|
+
|
|
32
|
+
return [
|
|
33
|
+
{ label: 'DNS Lookup', value: timing.dns, percentage: (timing.dns / total) * 100, color: TIMING_COLORS.dns },
|
|
34
|
+
{ label: 'TCP Connect', value: timing.tcp, percentage: (timing.tcp / total) * 100, color: TIMING_COLORS.tcp },
|
|
35
|
+
{ label: 'TLS Handshake', value: timing.tls, percentage: (timing.tls / total) * 100, color: TIMING_COLORS.tls },
|
|
36
|
+
{ label: 'First Byte (TTFB)', value: timing.firstByte, percentage: (timing.firstByte / total) * 100, color: TIMING_COLORS.firstByte },
|
|
37
|
+
{ label: 'Content Transfer', value: transfer, percentage: (transfer / total) * 100, color: TIMING_COLORS.transfer },
|
|
38
|
+
];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Format timing for display.
|
|
43
|
+
*/
|
|
44
|
+
export function formatTiming(timing: RequestTiming): string {
|
|
45
|
+
const parts: string[] = [];
|
|
46
|
+
if (timing.dns > 0) parts.push(`DNS: ${timing.dns}ms`);
|
|
47
|
+
if (timing.tcp > 0) parts.push(`TCP: ${timing.tcp}ms`);
|
|
48
|
+
if (timing.tls > 0) parts.push(`TLS: ${timing.tls}ms`);
|
|
49
|
+
parts.push(`TTFB: ${timing.firstByte}ms`);
|
|
50
|
+
parts.push(`Total: ${timing.total}ms`);
|
|
51
|
+
return parts.join(' | ');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get performance rating based on total time.
|
|
56
|
+
*/
|
|
57
|
+
export function getPerformanceRating(totalMs: number): { label: string; color: string } {
|
|
58
|
+
if (totalMs < 200) return { label: 'Fast', color: 'text-green-400' };
|
|
59
|
+
if (totalMs < 500) return { label: 'Good', color: 'text-green-300' };
|
|
60
|
+
if (totalMs < 1000) return { label: 'Moderate', color: 'text-yellow-400' };
|
|
61
|
+
if (totalMs < 3000) return { label: 'Slow', color: 'text-orange-400' };
|
|
62
|
+
return { label: 'Very Slow', color: 'text-red-400' };
|
|
63
|
+
}
|