@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.
Files changed (150) hide show
  1. package/.env.example +15 -0
  2. package/LICENSE +21 -0
  3. package/README.md +73 -0
  4. package/docker-compose.yml +23 -0
  5. package/jest.config.js +7 -0
  6. package/next-env.d.ts +5 -0
  7. package/next.config.mjs +22 -0
  8. package/package.json +82 -0
  9. package/postcss.config.js +6 -0
  10. package/prisma/schema.prisma +105 -0
  11. package/prisma/seed.ts +211 -0
  12. package/src/app/(app)/ai/page.tsx +122 -0
  13. package/src/app/(app)/collections/page.tsx +155 -0
  14. package/src/app/(app)/environments/page.tsx +96 -0
  15. package/src/app/(app)/history/page.tsx +107 -0
  16. package/src/app/(app)/import/page.tsx +102 -0
  17. package/src/app/(app)/layout.tsx +60 -0
  18. package/src/app/(app)/settings/page.tsx +79 -0
  19. package/src/app/(app)/workspace/page.tsx +284 -0
  20. package/src/app/api/ai/discover/route.ts +17 -0
  21. package/src/app/api/ai/explain/route.ts +29 -0
  22. package/src/app/api/ai/generate-tests/route.ts +37 -0
  23. package/src/app/api/ai/suggest/route.ts +29 -0
  24. package/src/app/api/collections/[id]/route.ts +66 -0
  25. package/src/app/api/collections/route.ts +48 -0
  26. package/src/app/api/environments/route.ts +40 -0
  27. package/src/app/api/export/openapi/route.ts +17 -0
  28. package/src/app/api/export/postman/route.ts +18 -0
  29. package/src/app/api/import/curl/route.ts +18 -0
  30. package/src/app/api/import/har/route.ts +20 -0
  31. package/src/app/api/import/openapi/route.ts +21 -0
  32. package/src/app/api/import/postman/route.ts +21 -0
  33. package/src/app/api/proxy/route.ts +35 -0
  34. package/src/app/api/requests/[id]/execute/route.ts +85 -0
  35. package/src/app/api/requests/[id]/history/route.ts +23 -0
  36. package/src/app/api/requests/[id]/route.ts +66 -0
  37. package/src/app/api/requests/route.ts +49 -0
  38. package/src/app/api/workspaces/route.ts +38 -0
  39. package/src/app/globals.css +99 -0
  40. package/src/app/layout.tsx +24 -0
  41. package/src/app/page.tsx +182 -0
  42. package/src/components/ai/ai-panel.tsx +65 -0
  43. package/src/components/ai/code-explainer.tsx +51 -0
  44. package/src/components/ai/endpoint-discovery.tsx +62 -0
  45. package/src/components/ai/test-generator.tsx +49 -0
  46. package/src/components/collections/collection-actions.tsx +36 -0
  47. package/src/components/collections/collection-tree.tsx +55 -0
  48. package/src/components/collections/folder-creator.tsx +54 -0
  49. package/src/components/landing/comparison.tsx +43 -0
  50. package/src/components/landing/cta.tsx +16 -0
  51. package/src/components/landing/features.tsx +24 -0
  52. package/src/components/landing/hero.tsx +23 -0
  53. package/src/components/response/body-viewer.tsx +33 -0
  54. package/src/components/response/headers-viewer.tsx +23 -0
  55. package/src/components/response/status-badge.tsx +25 -0
  56. package/src/components/response/test-results.tsx +50 -0
  57. package/src/components/response/timing-chart.tsx +39 -0
  58. package/src/components/ui/badge.tsx +24 -0
  59. package/src/components/ui/button.tsx +32 -0
  60. package/src/components/ui/code-editor.tsx +51 -0
  61. package/src/components/ui/dialog.tsx +56 -0
  62. package/src/components/ui/dropdown.tsx +63 -0
  63. package/src/components/ui/input.tsx +22 -0
  64. package/src/components/ui/key-value-editor.tsx +75 -0
  65. package/src/components/ui/select.tsx +24 -0
  66. package/src/components/ui/tabs.tsx +85 -0
  67. package/src/components/ui/textarea.tsx +22 -0
  68. package/src/components/ui/toast.tsx +54 -0
  69. package/src/components/workspace/request-panel.tsx +38 -0
  70. package/src/components/workspace/response-panel.tsx +81 -0
  71. package/src/components/workspace/sidebar.tsx +52 -0
  72. package/src/components/workspace/split-pane.tsx +49 -0
  73. package/src/components/workspace/tabs/auth-tab.tsx +94 -0
  74. package/src/components/workspace/tabs/body-tab.tsx +41 -0
  75. package/src/components/workspace/tabs/headers-tab.tsx +23 -0
  76. package/src/components/workspace/tabs/params-tab.tsx +23 -0
  77. package/src/components/workspace/tabs/pre-request-tab.tsx +26 -0
  78. package/src/components/workspace/url-bar.tsx +53 -0
  79. package/src/hooks/use-ai.ts +115 -0
  80. package/src/hooks/use-collection.ts +71 -0
  81. package/src/hooks/use-environment.ts +73 -0
  82. package/src/hooks/use-request.ts +111 -0
  83. package/src/lib/ai/endpoint-discovery.ts +158 -0
  84. package/src/lib/ai/explainer.ts +127 -0
  85. package/src/lib/ai/suggester.ts +164 -0
  86. package/src/lib/ai/test-generator.ts +161 -0
  87. package/src/lib/auth/api-key.ts +28 -0
  88. package/src/lib/auth/aws-sig.ts +131 -0
  89. package/src/lib/auth/basic.ts +17 -0
  90. package/src/lib/auth/bearer.ts +15 -0
  91. package/src/lib/auth/oauth2.ts +155 -0
  92. package/src/lib/auth/types.ts +16 -0
  93. package/src/lib/db/client.ts +15 -0
  94. package/src/lib/env/manager.ts +32 -0
  95. package/src/lib/env/resolver.ts +30 -0
  96. package/src/lib/exporters/openapi.ts +193 -0
  97. package/src/lib/exporters/postman.ts +140 -0
  98. package/src/lib/graphql/builder.ts +249 -0
  99. package/src/lib/graphql/formatter.ts +147 -0
  100. package/src/lib/graphql/index.ts +43 -0
  101. package/src/lib/graphql/introspection.ts +175 -0
  102. package/src/lib/graphql/types.ts +99 -0
  103. package/src/lib/graphql/validator.ts +216 -0
  104. package/src/lib/http/client.ts +112 -0
  105. package/src/lib/http/proxy.ts +83 -0
  106. package/src/lib/http/request-builder.ts +214 -0
  107. package/src/lib/http/response-parser.ts +106 -0
  108. package/src/lib/http/timing.ts +63 -0
  109. package/src/lib/importers/curl-parser.ts +346 -0
  110. package/src/lib/importers/har-parser.ts +128 -0
  111. package/src/lib/importers/openapi.ts +324 -0
  112. package/src/lib/importers/postman.ts +312 -0
  113. package/src/lib/test-runner/assertions.ts +163 -0
  114. package/src/lib/test-runner/reporter.ts +90 -0
  115. package/src/lib/test-runner/runner.ts +69 -0
  116. package/src/lib/utils/api-response.ts +85 -0
  117. package/src/lib/utils/cn.ts +6 -0
  118. package/src/lib/utils/content-type.ts +123 -0
  119. package/src/lib/utils/download.ts +53 -0
  120. package/src/lib/utils/errors.ts +92 -0
  121. package/src/lib/utils/format.ts +142 -0
  122. package/src/lib/utils/syntax-highlight.ts +108 -0
  123. package/src/lib/utils/validation.ts +231 -0
  124. package/src/lib/websocket/client.ts +182 -0
  125. package/src/lib/websocket/frames.ts +96 -0
  126. package/src/lib/websocket/history.ts +121 -0
  127. package/src/lib/websocket/index.ts +25 -0
  128. package/src/lib/websocket/types.ts +57 -0
  129. package/src/types/ai.ts +28 -0
  130. package/src/types/collection.ts +24 -0
  131. package/src/types/environment.ts +16 -0
  132. package/src/types/request.ts +54 -0
  133. package/src/types/response.ts +37 -0
  134. package/tailwind.config.ts +82 -0
  135. package/tests/lib/env/resolver.test.ts +108 -0
  136. package/tests/lib/graphql/builder.test.ts +349 -0
  137. package/tests/lib/graphql/formatter.test.ts +99 -0
  138. package/tests/lib/http/request-builder.test.ts +160 -0
  139. package/tests/lib/http/response-parser.test.ts +150 -0
  140. package/tests/lib/http/timing.test.ts +188 -0
  141. package/tests/lib/importers/curl-parser.test.ts +245 -0
  142. package/tests/lib/test-runner/assertions.test.ts +342 -0
  143. package/tests/lib/utils/cn.test.ts +46 -0
  144. package/tests/lib/utils/content-type.test.ts +175 -0
  145. package/tests/lib/utils/format.test.ts +188 -0
  146. package/tests/lib/utils/validation.test.ts +237 -0
  147. package/tests/lib/websocket/history.test.ts +186 -0
  148. package/tsconfig.json +29 -0
  149. package/tsconfig.tsbuildinfo +1 -0
  150. 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
+ }