@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,142 @@
1
+ /**
2
+ * Format JSON string with pretty printing and validation.
3
+ */
4
+ export function formatJson(input: string, indent: number = 2): string {
5
+ try {
6
+ const parsed = JSON.parse(input);
7
+ return JSON.stringify(parsed, null, indent);
8
+ } catch {
9
+ return input;
10
+ }
11
+ }
12
+
13
+ /**
14
+ * Minify JSON string.
15
+ */
16
+ export function minifyJson(input: string): string {
17
+ try {
18
+ const parsed = JSON.parse(input);
19
+ return JSON.stringify(parsed);
20
+ } catch {
21
+ return input;
22
+ }
23
+ }
24
+
25
+ /**
26
+ * Validate JSON string and return parsed result or error message.
27
+ */
28
+ export function validateJson(input: string): { valid: true; data: unknown } | { valid: false; error: string } {
29
+ try {
30
+ const data = JSON.parse(input);
31
+ return { valid: true, data };
32
+ } catch (e) {
33
+ const message = e instanceof Error ? e.message : 'Invalid JSON';
34
+ return { valid: false, error: message };
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Format XML string with proper indentation.
40
+ */
41
+ export function formatXml(input: string, indent: number = 2): string {
42
+ const PADDING = ' '.repeat(indent);
43
+ let formatted = '';
44
+ let depth = 0;
45
+
46
+ // Remove existing whitespace between tags
47
+ const xml = input.replace(/(>)\s*(<)/g, '$1\n$2').split('\n');
48
+
49
+ for (const node of xml) {
50
+ const trimmed = node.trim();
51
+ if (trimmed.length === 0) continue;
52
+
53
+ // Closing tag
54
+ if (trimmed.startsWith('</')) {
55
+ depth = Math.max(0, depth - 1);
56
+ }
57
+
58
+ formatted += PADDING.repeat(depth) + trimmed + '\n';
59
+
60
+ // Opening tag (not self-closing, not closing, not processing instruction)
61
+ if (
62
+ trimmed.startsWith('<') &&
63
+ !trimmed.startsWith('</') &&
64
+ !trimmed.startsWith('<?') &&
65
+ !trimmed.startsWith('<!') &&
66
+ !trimmed.endsWith('/>') &&
67
+ !trimmed.includes('</')
68
+ ) {
69
+ depth++;
70
+ }
71
+ }
72
+
73
+ return formatted.trimEnd();
74
+ }
75
+
76
+ /**
77
+ * Format HTML string with proper indentation.
78
+ */
79
+ export function formatHtml(input: string, indent: number = 2): string {
80
+ const PADDING = ' '.repeat(indent);
81
+ const selfClosingTags = new Set([
82
+ 'area', 'base', 'br', 'col', 'embed', 'hr', 'img',
83
+ 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr',
84
+ ]);
85
+
86
+ let formatted = '';
87
+ let depth = 0;
88
+
89
+ // Split on tags
90
+ const tokens = input.replace(/>\s*</g, '>\n<').split('\n');
91
+
92
+ for (const token of tokens) {
93
+ const trimmed = token.trim();
94
+ if (trimmed.length === 0) continue;
95
+
96
+ // Closing tag
97
+ const closingMatch = trimmed.match(/^<\/(\w+)/);
98
+ if (closingMatch) {
99
+ depth = Math.max(0, depth - 1);
100
+ formatted += PADDING.repeat(depth) + trimmed + '\n';
101
+ continue;
102
+ }
103
+
104
+ // Opening tag
105
+ const openingMatch = trimmed.match(/^<(\w+)/);
106
+ if (openingMatch) {
107
+ const tagName = openingMatch[1]?.toLowerCase();
108
+ formatted += PADDING.repeat(depth) + trimmed + '\n';
109
+
110
+ if (tagName && !selfClosingTags.has(tagName) && !trimmed.endsWith('/>')) {
111
+ depth++;
112
+ }
113
+ continue;
114
+ }
115
+
116
+ // Text content
117
+ formatted += PADDING.repeat(depth) + trimmed + '\n';
118
+ }
119
+
120
+ return formatted.trimEnd();
121
+ }
122
+
123
+ /**
124
+ * Format byte size to human-readable string.
125
+ */
126
+ export function formatBytes(bytes: number): string {
127
+ if (bytes === 0) return '0 B';
128
+ const units = ['B', 'KB', 'MB', 'GB'];
129
+ const k = 1024;
130
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
131
+ const size = bytes / Math.pow(k, i);
132
+ return `${size.toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
133
+ }
134
+
135
+ /**
136
+ * Format duration in milliseconds to human-readable string.
137
+ */
138
+ export function formatDuration(ms: number): string {
139
+ if (ms < 1) return `${Math.round(ms * 1000)}us`;
140
+ if (ms < 1000) return `${Math.round(ms)}ms`;
141
+ return `${(ms / 1000).toFixed(2)}s`;
142
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Lightweight syntax highlighting for JSON/XML/HTML.
3
+ * Returns HTML string with <span> elements for styling.
4
+ */
5
+
6
+ type TokenType = 'key' | 'string' | 'number' | 'boolean' | 'null' | 'bracket' | 'operator';
7
+
8
+ const TOKEN_CLASSES: Record<TokenType, string> = {
9
+ key: 'text-blue-400',
10
+ string: 'text-green-400',
11
+ number: 'text-orange-400',
12
+ boolean: 'text-purple-400',
13
+ null: 'text-gray-500',
14
+ bracket: 'text-yellow-300',
15
+ operator: 'text-yellow-300',
16
+ };
17
+
18
+ function escapeHtml(str: string): string {
19
+ return str
20
+ .replace(/&/g, '&amp;')
21
+ .replace(/</g, '&lt;')
22
+ .replace(/>/g, '&gt;')
23
+ .replace(/"/g, '&quot;');
24
+ }
25
+
26
+ function wrapToken(type: TokenType, content: string): string {
27
+ return `<span class="${TOKEN_CLASSES[type]}">${content}</span>`;
28
+ }
29
+
30
+ /**
31
+ * Highlight JSON string with HTML spans.
32
+ */
33
+ export function highlightJson(json: string): string {
34
+ const formatted = (() => {
35
+ try {
36
+ return JSON.stringify(JSON.parse(json), null, 2);
37
+ } catch {
38
+ return json;
39
+ }
40
+ })();
41
+
42
+ return formatted.replace(
43
+ /("(?:[^"\\]|\\.)*")\s*:/g,
44
+ (_, key) => wrapToken('key', escapeHtml(key)) + wrapToken('operator', ':'),
45
+ ).replace(
46
+ /:\s*("(?:[^"\\]|\\.)*")/g,
47
+ (_, val) => ': ' + wrapToken('string', escapeHtml(val)),
48
+ ).replace(
49
+ /:\s*(\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)/g,
50
+ (_, num) => ': ' + wrapToken('number', escapeHtml(num)),
51
+ ).replace(
52
+ /:\s*(true|false)/g,
53
+ (_, bool) => ': ' + wrapToken('boolean', escapeHtml(bool)),
54
+ ).replace(
55
+ /:\s*(null)/g,
56
+ (_, n) => ': ' + wrapToken('null', escapeHtml(n)),
57
+ ).replace(
58
+ /([{}[\]])/g,
59
+ (_, bracket) => wrapToken('bracket', escapeHtml(bracket)),
60
+ );
61
+ }
62
+
63
+ /**
64
+ * Highlight XML with HTML spans.
65
+ */
66
+ export function highlightXml(xml: string): string {
67
+ const escaped = escapeHtml(xml);
68
+
69
+ return escaped
70
+ .replace(
71
+ /(&lt;\/?)([\w:-]+)/g,
72
+ (_, prefix, tagName) => `<span class="text-gray-500">${prefix}</span><span class="text-red-400">${tagName}</span>`,
73
+ )
74
+ .replace(
75
+ /\s([\w:-]+)(=)(&quot;[^&]*?&quot;)/g,
76
+ (_, attr, eq, value) =>
77
+ ` <span class="text-yellow-300">${attr}</span><span class="text-gray-500">${eq}</span><span class="text-green-400">${value}</span>`,
78
+ )
79
+ .replace(
80
+ /(&lt;[\?\/!][^&]*?&gt;)/g,
81
+ '<span class="text-gray-500">$1</span>',
82
+ );
83
+ }
84
+
85
+ /**
86
+ * Highlight HTTP headers with HTML spans.
87
+ */
88
+ export function highlightHeaders(headers: string): string {
89
+ return headers
90
+ .split('\n')
91
+ .map((line) => {
92
+ const colonIndex = line.indexOf(':');
93
+ if (colonIndex === -1) return escapeHtml(line);
94
+
95
+ const name = line.slice(0, colonIndex);
96
+ const value = line.slice(colonIndex + 1);
97
+ return `<span class="text-cyan-400">${escapeHtml(name)}</span>:<span class="text-gray-300">${escapeHtml(value)}</span>`;
98
+ })
99
+ .join('\n');
100
+ }
101
+
102
+ /**
103
+ * Get line numbers HTML for a code block.
104
+ */
105
+ export function getLineNumbers(code: string): string {
106
+ const lines = code.split('\n');
107
+ return lines.map((_, i) => `<span class="block text-right pr-2 text-gray-600 select-none">${i + 1}</span>`).join('');
108
+ }
@@ -0,0 +1,231 @@
1
+ import { z } from 'zod';
2
+
3
+ // --- HTTP Method ---
4
+ export const HttpMethodSchema = z.enum([
5
+ 'GET', 'POST', 'PUT', 'PATCH', 'DELETE',
6
+ 'HEAD', 'OPTIONS', 'TRACE', 'CONNECT',
7
+ ]);
8
+ export type HttpMethod = z.infer<typeof HttpMethodSchema>;
9
+
10
+ // --- Key-Value Pair ---
11
+ export const KeyValueSchema = z.object({
12
+ key: z.string().min(1, 'Key is required'),
13
+ value: z.string(),
14
+ enabled: z.boolean().default(true),
15
+ description: z.string().optional(),
16
+ });
17
+ export type KeyValue = z.infer<typeof KeyValueSchema>;
18
+
19
+ // --- Body Types ---
20
+ export const BodyTypeSchema = z.enum([
21
+ 'none', 'json', 'form', 'urlencoded', 'raw', 'graphql', 'binary',
22
+ ]);
23
+ export type BodyType = z.infer<typeof BodyTypeSchema>;
24
+
25
+ // --- Request Body ---
26
+ export const RequestBodySchema = z.object({
27
+ raw: z.string().optional(),
28
+ form: z.array(KeyValueSchema).optional(),
29
+ graphql: z.object({
30
+ query: z.string(),
31
+ variables: z.string().optional(),
32
+ }).optional(),
33
+ binary: z.string().optional(),
34
+ });
35
+ export type RequestBody = z.infer<typeof RequestBodySchema>;
36
+
37
+ // --- Auth Types ---
38
+ export const AuthTypeSchema = z.enum([
39
+ 'none', 'basic', 'bearer', 'apikey', 'oauth2', 'aws', 'digest', 'ntlm',
40
+ ]);
41
+ export type AuthType = z.infer<typeof AuthTypeSchema>;
42
+
43
+ export const BasicAuthSchema = z.object({
44
+ type: z.literal('basic'),
45
+ username: z.string(),
46
+ password: z.string(),
47
+ });
48
+
49
+ export const BearerAuthSchema = z.object({
50
+ type: z.literal('bearer'),
51
+ token: z.string(),
52
+ prefix: z.string().default('Bearer'),
53
+ });
54
+
55
+ export const ApiKeyAuthSchema = z.object({
56
+ type: z.literal('apikey'),
57
+ key: z.string(),
58
+ value: z.string(),
59
+ addTo: z.enum(['header', 'query', 'cookie']),
60
+ });
61
+
62
+ export const OAuth2AuthSchema = z.object({
63
+ type: z.literal('oauth2'),
64
+ grantType: z.enum(['authorization_code', 'client_credentials', 'implicit', 'password']),
65
+ accessTokenUrl: z.string().url().optional().or(z.literal('')),
66
+ authorizationUrl: z.string().url().optional().or(z.literal('')),
67
+ clientId: z.string().optional(),
68
+ clientSecret: z.string().optional(),
69
+ scope: z.string().optional(),
70
+ token: z.string().optional(),
71
+ refreshToken: z.string().optional(),
72
+ tokenType: z.string().default('Bearer'),
73
+ });
74
+
75
+ export const AwsAuthSchema = z.object({
76
+ type: z.literal('aws'),
77
+ accessKey: z.string(),
78
+ secretKey: z.string(),
79
+ region: z.string(),
80
+ service: z.string(),
81
+ sessionToken: z.string().optional(),
82
+ });
83
+
84
+ export const DigestAuthSchema = z.object({
85
+ type: z.literal('digest'),
86
+ username: z.string(),
87
+ password: z.string(),
88
+ realm: z.string().optional(),
89
+ nonce: z.string().optional(),
90
+ qop: z.enum(['auth', 'auth-int']).optional(),
91
+ });
92
+
93
+ export const AuthConfigSchema = z.discriminatedUnion('type', [
94
+ z.object({ type: z.literal('none') }),
95
+ BasicAuthSchema,
96
+ BearerAuthSchema,
97
+ ApiKeyAuthSchema,
98
+ OAuth2AuthSchema,
99
+ AwsAuthSchema,
100
+ DigestAuthSchema,
101
+ ]);
102
+ export type AuthConfig = z.infer<typeof AuthConfigSchema>;
103
+
104
+ // --- Request Schema ---
105
+ export const CreateRequestSchema = z.object({
106
+ name: z.string().min(1, 'Name is required').max(200),
107
+ description: z.string().optional(),
108
+ collectionId: z.string().min(1, 'Collection ID is required'),
109
+ method: HttpMethodSchema.default('GET'),
110
+ url: z.string().min(1, 'URL is required'),
111
+ headers: z.array(KeyValueSchema).default([]),
112
+ params: z.array(KeyValueSchema).default([]),
113
+ body: RequestBodySchema.optional(),
114
+ bodyType: BodyTypeSchema.default('none'),
115
+ auth: AuthConfigSchema.optional(),
116
+ preRequestScript: z.string().optional(),
117
+ tests: z.string().optional(),
118
+ sortOrder: z.number().default(0),
119
+ });
120
+
121
+ export const UpdateRequestSchema = CreateRequestSchema.partial().extend({
122
+ id: z.string(),
123
+ });
124
+
125
+ // --- Collection Schema ---
126
+ export const CreateCollectionSchema = z.object({
127
+ name: z.string().min(1, 'Name is required').max(200),
128
+ description: z.string().optional(),
129
+ workspaceId: z.string().min(1, 'Workspace ID is required'),
130
+ parentId: z.string().optional(),
131
+ variables: z.record(z.string()).default({}),
132
+ preScript: z.string().optional(),
133
+ postScript: z.string().optional(),
134
+ sortOrder: z.number().default(0),
135
+ });
136
+
137
+ export const UpdateCollectionSchema = CreateCollectionSchema.partial().extend({
138
+ id: z.string(),
139
+ });
140
+
141
+ // --- Workspace Schema ---
142
+ export const CreateWorkspaceSchema = z.object({
143
+ name: z.string().min(1, 'Name is required').max(200),
144
+ description: z.string().optional(),
145
+ userId: z.string().default('demo-user'),
146
+ });
147
+
148
+ export const UpdateWorkspaceSchema = CreateWorkspaceSchema.partial().extend({
149
+ id: z.string(),
150
+ });
151
+
152
+ // --- Environment Schema ---
153
+ export const EnvironmentVariableSchema = z.object({
154
+ key: z.string().min(1),
155
+ value: z.string(),
156
+ type: z.enum(['string', 'number', 'boolean', 'secret']).default('string'),
157
+ enabled: z.boolean().default(true),
158
+ });
159
+ export type EnvironmentVariable = z.infer<typeof EnvironmentVariableSchema>;
160
+
161
+ export const CreateEnvironmentSchema = z.object({
162
+ name: z.string().min(1, 'Name is required').max(200),
163
+ workspaceId: z.string().min(1, 'Workspace ID is required'),
164
+ variables: z.array(EnvironmentVariableSchema).default([]),
165
+ isDefault: z.boolean().default(false),
166
+ });
167
+
168
+ export const UpdateEnvironmentSchema = CreateEnvironmentSchema.partial().extend({
169
+ id: z.string(),
170
+ });
171
+
172
+ // --- Execute Request Schema ---
173
+ export const ExecuteRequestSchema = z.object({
174
+ method: HttpMethodSchema,
175
+ url: z.string().min(1, 'URL is required'),
176
+ headers: z.array(KeyValueSchema).default([]),
177
+ params: z.array(KeyValueSchema).default([]),
178
+ body: RequestBodySchema.optional(),
179
+ bodyType: BodyTypeSchema.default('none'),
180
+ auth: AuthConfigSchema.optional(),
181
+ timeout: z.number().min(1).max(300000).default(30000),
182
+ followRedirects: z.boolean().default(true),
183
+ maxRedirects: z.number().min(0).max(20).default(5),
184
+ verifySSL: z.boolean().default(true),
185
+ });
186
+
187
+ // --- Import Schemas ---
188
+ export const ImportOpenApiSchema = z.object({
189
+ spec: z.string().min(1, 'OpenAPI spec is required'),
190
+ workspaceId: z.string().min(1),
191
+ collectionName: z.string().optional(),
192
+ });
193
+
194
+ export const ImportPostmanSchema = z.object({
195
+ collection: z.string().min(1, 'Postman collection is required'),
196
+ workspaceId: z.string().min(1),
197
+ });
198
+
199
+ export const ImportCurlSchema = z.object({
200
+ command: z.string().min(1, 'cURL command is required'),
201
+ collectionId: z.string().min(1),
202
+ name: z.string().optional(),
203
+ });
204
+
205
+ export const ImportHarSchema = z.object({
206
+ har: z.string().min(1, 'HAR data is required'),
207
+ collectionId: z.string().min(1),
208
+ });
209
+
210
+ // --- AI Schemas ---
211
+ export const AiGenerateTestsSchema = z.object({
212
+ requestId: z.string().min(1),
213
+ response: z.unknown(),
214
+ prompt: z.string().optional(),
215
+ });
216
+
217
+ export const AiDiscoverSchema = z.object({
218
+ code: z.string().min(1),
219
+ framework: z.enum(['express', 'nextjs', 'fastapi', 'django', 'flask', 'spring', 'auto']).default('auto'),
220
+ workspaceId: z.string().min(1),
221
+ });
222
+
223
+ export const AiExplainSchema = z.object({
224
+ response: z.unknown(),
225
+ question: z.string().optional(),
226
+ });
227
+
228
+ export const AiSuggestSchema = z.object({
229
+ request: z.unknown(),
230
+ response: z.unknown().optional(),
231
+ });
@@ -0,0 +1,182 @@
1
+ /**
2
+ * WebSocket Client — manages WebSocket connections for testing.
3
+ *
4
+ * In a browser context, this uses the native WebSocket API.
5
+ * In Node.js test environments, the caller provides a mock or ws library.
6
+ */
7
+
8
+ import type { WSConfig, WSConnectionState, WSMessage, WSClientEvents } from './types';
9
+ import { detectMessageType } from './frames';
10
+
11
+ type MessageHandler = WSClientEvents['message'];
12
+ type OpenHandler = WSClientEvents['open'];
13
+ type CloseHandler = WSClientEvents['close'];
14
+ type ErrorHandler = WSClientEvents['error'];
15
+ type StateChangeHandler = WSClientEvents['stateChange'];
16
+
17
+ export class WebSocketClient {
18
+ private ws: WebSocket | null = null;
19
+ private config: WSConfig;
20
+ private state: WSConnectionState = 'disconnected';
21
+ private reconnectAttempts = 0;
22
+
23
+ private messageHandlers: Set<MessageHandler> = new Set();
24
+ private openHandlers: Set<OpenHandler> = new Set();
25
+ private closeHandlers: Set<CloseHandler> = new Set();
26
+ private errorHandlers: Set<ErrorHandler> = new Set();
27
+ private stateChangeHandlers: Set<StateChangeHandler> = new Set();
28
+
29
+ constructor(config: WSConfig) {
30
+ this.config = config;
31
+ }
32
+
33
+ /**
34
+ * Establish a WebSocket connection.
35
+ */
36
+ connect(): Promise<void> {
37
+ return new Promise((resolve, reject) => {
38
+ if (this.state === 'connected' || this.state === 'connecting') {
39
+ resolve();
40
+ return;
41
+ }
42
+
43
+ this.setState('connecting');
44
+
45
+ try {
46
+ const protocols = this.config.protocols?.length ? this.config.protocols : undefined;
47
+ this.ws = new WebSocket(this.config.url, protocols);
48
+
49
+ this.ws.onopen = () => {
50
+ this.setState('connected');
51
+ this.reconnectAttempts = 0;
52
+ this.openHandlers.forEach((h) => h());
53
+ resolve();
54
+ };
55
+
56
+ this.ws.onmessage = (event: MessageEvent) => {
57
+ const data = typeof event.data === 'string' ? event.data : '[binary data]';
58
+ const message = this.createMessage('received', data);
59
+ this.messageHandlers.forEach((h) => h(message));
60
+ };
61
+
62
+ this.ws.onclose = (event: CloseEvent) => {
63
+ this.setState('disconnected');
64
+ this.closeHandlers.forEach((h) => h(event.code, event.reason));
65
+
66
+ if (this.config.reconnect && this.reconnectAttempts < (this.config.maxReconnectAttempts ?? 5)) {
67
+ this.reconnectAttempts++;
68
+ setTimeout(() => this.connect(), this.config.reconnectDelay ?? 1000);
69
+ }
70
+ };
71
+
72
+ this.ws.onerror = () => {
73
+ const error = new Error(`WebSocket error connecting to ${this.config.url}`);
74
+ this.errorHandlers.forEach((h) => h(error));
75
+ if (this.state === 'connecting') {
76
+ reject(error);
77
+ }
78
+ };
79
+ } catch (err) {
80
+ this.setState('disconnected');
81
+ reject(err);
82
+ }
83
+ });
84
+ }
85
+
86
+ /**
87
+ * Send a text or binary message.
88
+ */
89
+ send(message: string): void {
90
+ if (!this.ws || this.state !== 'connected') {
91
+ throw new Error('WebSocket is not connected');
92
+ }
93
+
94
+ this.ws.send(message);
95
+ const msg = this.createMessage('sent', message);
96
+ this.messageHandlers.forEach((h) => h(msg));
97
+ }
98
+
99
+ /**
100
+ * Register a message handler.
101
+ */
102
+ onMessage(handler: MessageHandler): () => void {
103
+ this.messageHandlers.add(handler);
104
+ return () => this.messageHandlers.delete(handler);
105
+ }
106
+
107
+ /**
108
+ * Register an open handler.
109
+ */
110
+ onOpen(handler: OpenHandler): () => void {
111
+ this.openHandlers.add(handler);
112
+ return () => this.openHandlers.delete(handler);
113
+ }
114
+
115
+ /**
116
+ * Register a close handler.
117
+ */
118
+ onClose(handler: CloseHandler): () => void {
119
+ this.closeHandlers.add(handler);
120
+ return () => this.closeHandlers.delete(handler);
121
+ }
122
+
123
+ /**
124
+ * Register an error handler.
125
+ */
126
+ onError(handler: ErrorHandler): () => void {
127
+ this.errorHandlers.add(handler);
128
+ return () => this.errorHandlers.delete(handler);
129
+ }
130
+
131
+ /**
132
+ * Register a state change handler.
133
+ */
134
+ onStateChange(handler: StateChangeHandler): () => void {
135
+ this.stateChangeHandlers.add(handler);
136
+ return () => this.stateChangeHandlers.delete(handler);
137
+ }
138
+
139
+ /**
140
+ * Close the connection.
141
+ */
142
+ close(code: number = 1000, reason: string = ''): void {
143
+ this.setState('disconnecting');
144
+ this.ws?.close(code, reason);
145
+ this.ws = null;
146
+ }
147
+
148
+ /**
149
+ * Get the current connection state.
150
+ */
151
+ getState(): WSConnectionState {
152
+ return this.state;
153
+ }
154
+
155
+ /**
156
+ * Get the URL this client is connected to.
157
+ */
158
+ getUrl(): string {
159
+ return this.config.url;
160
+ }
161
+
162
+ private setState(newState: WSConnectionState): void {
163
+ this.state = newState;
164
+ this.stateChangeHandlers.forEach((h) => h(newState));
165
+ }
166
+
167
+ private createMessage(direction: 'sent' | 'received', data: string): WSMessage {
168
+ return {
169
+ id: generateId(),
170
+ direction,
171
+ data,
172
+ type: detectMessageType(data),
173
+ timestamp: Date.now(),
174
+ size: new TextEncoder().encode(data).length,
175
+ };
176
+ }
177
+ }
178
+
179
+ let idCounter = 0;
180
+ function generateId(): string {
181
+ return `ws_${Date.now()}_${++idCounter}`;
182
+ }