@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,324 @@
1
+ import type { HttpMethod } from '@/lib/utils/validation';
2
+
3
+ export interface ImportedEndpoint {
4
+ name: string;
5
+ method: HttpMethod;
6
+ url: string;
7
+ description?: string;
8
+ headers: { key: string; value: string; enabled: boolean }[];
9
+ params: { key: string; value: string; enabled: boolean }[];
10
+ body?: {
11
+ raw?: string;
12
+ };
13
+ bodyType?: string;
14
+ auth?: unknown;
15
+ }
16
+
17
+ export interface ImportedCollection {
18
+ name: string;
19
+ description?: string;
20
+ endpoints: ImportedEndpoint[];
21
+ }
22
+
23
+ /**
24
+ * Import an OpenAPI 3.x specification into a collection.
25
+ * Supports both JSON and YAML formats.
26
+ */
27
+ export function importOpenApi(spec: string): ImportedCollection {
28
+ let parsed: unknown;
29
+ try {
30
+ parsed = JSON.parse(spec);
31
+ } catch {
32
+ // Try YAML (basic support)
33
+ try {
34
+ parsed = parseSimpleYaml(spec);
35
+ } catch {
36
+ throw new Error('Invalid OpenAPI spec: must be valid JSON or YAML');
37
+ }
38
+ }
39
+
40
+ const api = parsed as Record<string, unknown>;
41
+ const info = api['info'] as Record<string, string> | undefined;
42
+ const paths = api['paths'] as Record<string, Record<string, unknown>> | undefined;
43
+ const servers = api['servers'] as Array<{ url: string }> | undefined;
44
+ const components = api['components'] as Record<string, unknown> | undefined;
45
+
46
+ if (!paths || typeof paths !== 'object') {
47
+ throw new Error('Invalid OpenAPI spec: missing or invalid "paths" object');
48
+ }
49
+
50
+ const baseUrl = servers?.[0]?.url ?? '';
51
+
52
+ const collection: ImportedCollection = {
53
+ name: info?.['title'] ?? 'Imported API',
54
+ description: info?.['description'],
55
+ endpoints: [],
56
+ };
57
+
58
+ const httpMethods = new Set(['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace']);
59
+
60
+ for (const [path, methods] of Object.entries(paths)) {
61
+ if (!methods || typeof methods !== 'object') continue;
62
+
63
+ for (const [method, operation] of Object.entries(methods)) {
64
+ if (!httpMethods.has(method.toLowerCase())) continue;
65
+
66
+ const op = operation as Record<string, unknown>;
67
+ const parameters = (op['parameters'] ?? []) as Array<Record<string, unknown>>;
68
+ const requestBody = op['requestBody'] as Record<string, unknown> | undefined;
69
+ const security = op['security'] as Array<Record<string, unknown>> | undefined;
70
+
71
+ const headers: ImportedEndpoint['headers'] = [];
72
+ const params: ImportedEndpoint['params'] = [];
73
+
74
+ for (const param of parameters) {
75
+ const paramName = param['name'] as string;
76
+ const paramIn = param['in'] as string;
77
+ const paramValue = getExampleValue(param);
78
+
79
+ if (paramIn === 'query') {
80
+ params.push({ key: paramName, value: paramValue, enabled: true });
81
+ } else if (paramIn === 'header') {
82
+ headers.push({ key: paramName, value: paramValue, enabled: true });
83
+ }
84
+ }
85
+
86
+ let body: ImportedEndpoint['body'];
87
+ let bodyType: string | undefined;
88
+
89
+ if (requestBody) {
90
+ const content = requestBody['content'] as Record<string, Record<string, unknown>> | undefined;
91
+ if (content) {
92
+ const jsonContent = content['application/json'];
93
+ if (jsonContent) {
94
+ const schema = jsonContent['schema'] as Record<string, unknown> | undefined;
95
+ if (schema) {
96
+ const example = resolveSchemaExample(schema, components);
97
+ body = { raw: JSON.stringify(example, null, 2) };
98
+ bodyType = 'json';
99
+ headers.push({ key: 'Content-Type', value: 'application/json', enabled: true });
100
+ }
101
+ }
102
+
103
+ if (!body) {
104
+ const formContent = content['application/x-www-form-urlencoded'];
105
+ if (formContent) {
106
+ const schema = formContent['schema'] as Record<string, unknown> | undefined;
107
+ if (schema) {
108
+ const example = resolveSchemaExample(schema, components);
109
+ body = { raw: JSON.stringify(example, null, 2) };
110
+ bodyType = 'urlencoded';
111
+ }
112
+ }
113
+ }
114
+ }
115
+ }
116
+
117
+ let auth: unknown;
118
+ if (security && security.length > 0) {
119
+ const securitySchemes = (components?.['securitySchemes'] ?? {}) as Record<string, Record<string, unknown>>;
120
+ const schemeName = Object.keys(security[0] ?? {})[0];
121
+ if (schemeName && securitySchemes[schemeName]) {
122
+ auth = securitySchemeToAuth(securitySchemes[schemeName]!);
123
+ }
124
+ }
125
+
126
+ const operationId = (op['operationId'] as string) ?? `${method.toUpperCase()} ${path}`;
127
+ const summary = op['summary'] as string | undefined;
128
+
129
+ collection.endpoints.push({
130
+ name: summary ?? operationId,
131
+ method: method.toUpperCase() as HttpMethod,
132
+ url: baseUrl ? `${baseUrl}${path}` : path,
133
+ description: (op['description'] as string) ?? summary,
134
+ headers,
135
+ params,
136
+ body,
137
+ bodyType,
138
+ auth,
139
+ });
140
+ }
141
+ }
142
+
143
+ return collection;
144
+ }
145
+
146
+ function getExampleValue(param: Record<string, unknown>): string {
147
+ const example = param['example'];
148
+ if (example !== undefined) return String(example);
149
+
150
+ const schema = param['schema'] as Record<string, unknown> | undefined;
151
+ if (schema) {
152
+ if (schema['example'] !== undefined) return String(schema['example']);
153
+ if (schema['default'] !== undefined) return String(schema['default']);
154
+ if (schema['enum'] && Array.isArray(schema['enum']) && schema['enum'].length > 0) {
155
+ return String(schema['enum'][0]);
156
+ }
157
+ }
158
+
159
+ return '';
160
+ }
161
+
162
+ function resolveSchemaExample(
163
+ schema: Record<string, unknown>,
164
+ components?: Record<string, unknown>,
165
+ visited: Set<string> = new Set(),
166
+ ): unknown {
167
+ // Handle $ref
168
+ if (schema['$ref']) {
169
+ const refPath = (schema['$ref'] as string).replace('#/', '').split('/');
170
+ let resolved: unknown = { '#': components };
171
+
172
+ for (const segment of refPath) {
173
+ if (typeof resolved !== 'object' || resolved === null) return {};
174
+ const refKey = `${refPath.join('/')}`;
175
+ if (visited.has(refKey)) return {};
176
+ visited.add(refKey);
177
+ resolved = (resolved as Record<string, unknown>)[segment];
178
+ }
179
+
180
+ if (typeof resolved === 'object' && resolved !== null) {
181
+ return resolveSchemaExample(resolved as Record<string, unknown>, components, visited);
182
+ }
183
+ return resolved;
184
+ }
185
+
186
+ if (schema['example'] !== undefined) return schema['example'];
187
+ if (schema['default'] !== undefined) return schema['default'];
188
+
189
+ const type = schema['type'] as string;
190
+
191
+ if (type === 'object') {
192
+ const properties = schema['properties'] as Record<string, Record<string, unknown>> | undefined;
193
+ if (!properties) return {};
194
+
195
+ const result: Record<string, unknown> = {};
196
+ for (const [key, propSchema] of Object.entries(properties)) {
197
+ result[key] = resolveSchemaExample(propSchema, components, visited);
198
+ }
199
+ return result;
200
+ }
201
+
202
+ if (type === 'array') {
203
+ const items = schema['items'] as Record<string, unknown> | undefined;
204
+ if (items) {
205
+ return [resolveSchemaExample(items, components, visited)];
206
+ }
207
+ return [];
208
+ }
209
+
210
+ if (type === 'string') return '';
211
+ if (type === 'number' || type === 'integer') return 0;
212
+ if (type === 'boolean') return false;
213
+
214
+ return null;
215
+ }
216
+
217
+ function securitySchemeToAuth(scheme: Record<string, unknown>): unknown {
218
+ const type = scheme['type'] as string;
219
+
220
+ if (type === 'http') {
221
+ const schemeName = scheme['scheme'] as string;
222
+ if (schemeName === 'basic') {
223
+ return { type: 'basic', username: '', password: '' };
224
+ }
225
+ if (schemeName === 'bearer') {
226
+ return { type: 'bearer', token: '', prefix: 'Bearer' };
227
+ }
228
+ }
229
+
230
+ if (type === 'apiKey') {
231
+ return {
232
+ type: 'apikey',
233
+ key: scheme['name'] as string,
234
+ value: '',
235
+ addTo: (scheme['in'] as string) === 'query' ? 'query' : 'header',
236
+ };
237
+ }
238
+
239
+ if (type === 'oauth2') {
240
+ return { type: 'oauth2', grantType: 'authorization_code' };
241
+ }
242
+
243
+ return { type: 'none' };
244
+ }
245
+
246
+ /**
247
+ * Basic YAML parser for OpenAPI specs (no external dependency).
248
+ * Handles simple key-value pairs and nested objects.
249
+ */
250
+ function parseSimpleYaml(input: string): unknown {
251
+ const lines = input.split('\n');
252
+ const result: unknown = undefined;
253
+ const stack: Array<{ indent: number; obj: Record<string, unknown>; key: string | null }> = [];
254
+ let root: Record<string, unknown> = {};
255
+
256
+ stack.push({ indent: -1, obj: root, key: null });
257
+
258
+ for (const rawLine of lines) {
259
+ if (rawLine.trim() === '' || rawLine.trim().startsWith('#')) continue;
260
+
261
+ const indent = rawLine.search(/\S/);
262
+ const line = rawLine.trim();
263
+
264
+ // Pop stack to find parent
265
+ while (stack.length > 1 && stack[stack.length - 1]!.indent >= indent) {
266
+ stack.pop();
267
+ }
268
+
269
+ const parent = stack[stack.length - 1]!;
270
+
271
+ // Array item
272
+ if (line.startsWith('- ')) {
273
+ const value = parseYamlValue(line.slice(2));
274
+ const parentObj = parent.obj;
275
+ if (parent.key && Array.isArray(parentObj[parent.key])) {
276
+ (parentObj[parent.key] as unknown[]).push(value);
277
+ }
278
+ continue;
279
+ }
280
+
281
+ // Key-value pair
282
+ const colonIndex = line.indexOf(':');
283
+ if (colonIndex === -1) continue;
284
+
285
+ const key = line.slice(0, colonIndex).trim();
286
+ const valueStr = line.slice(colonIndex + 1).trim();
287
+
288
+ if (valueStr === '') {
289
+ // New nested object
290
+ const newObj: Record<string, unknown> = {};
291
+ if (parent.key && Array.isArray(parent.obj[parent.key])) {
292
+ (parent.obj[parent.key] as unknown[]).push(newObj);
293
+ } else {
294
+ parent.obj[key] = newObj;
295
+ }
296
+ stack.push({ indent, obj: newObj, key: null });
297
+ } else {
298
+ const value = parseYamlValue(valueStr);
299
+ if (parent.key && Array.isArray(parent.obj[parent.key])) {
300
+ // Not common, skip
301
+ } else {
302
+ parent.obj[key] = value;
303
+ }
304
+ }
305
+
306
+ root = stack[0]!.obj;
307
+ }
308
+
309
+ return root || result;
310
+ }
311
+
312
+ function parseYamlValue(value: string): unknown {
313
+ if (value === 'null' || value === '~') return null;
314
+ if (value === 'true') return true;
315
+ if (value === 'false') return false;
316
+ if (value.startsWith('"') && value.endsWith('"')) return value.slice(1, -1);
317
+ if (value.startsWith("'") && value.endsWith("'")) return value.slice(1, -1);
318
+ if (/^-?\d+$/.test(value)) return parseInt(value, 10);
319
+ if (/^-?\d+\.\d+$/.test(value)) return parseFloat(value);
320
+ if (value.startsWith('[') && value.endsWith(']')) {
321
+ return value.slice(1, -1).split(',').map((v) => parseYamlValue(v.trim()));
322
+ }
323
+ return value;
324
+ }
@@ -0,0 +1,312 @@
1
+ import type { HttpMethod } from '@/lib/utils/validation';
2
+
3
+ export interface ImportedPostmanCollection {
4
+ name: string;
5
+ description?: string;
6
+ folders: ImportedFolder[];
7
+ requests: ImportedPostmanRequest[];
8
+ variables: Record<string, string>;
9
+ }
10
+
11
+ export interface ImportedFolder {
12
+ name: string;
13
+ description?: string;
14
+ requests: ImportedPostmanRequest[];
15
+ }
16
+
17
+ export interface ImportedPostmanRequest {
18
+ name: string;
19
+ method: HttpMethod;
20
+ url: string;
21
+ headers: { key: string; value: string; enabled: boolean }[];
22
+ params: { key: string; value: string; enabled: boolean }[];
23
+ body?: {
24
+ raw?: string;
25
+ form?: { key: string; value: string; enabled: boolean }[];
26
+ };
27
+ bodyType?: string;
28
+ auth?: unknown;
29
+ preRequestScript?: string;
30
+ tests?: string;
31
+ }
32
+
33
+ /**
34
+ * Import a Postman Collection v2.1 JSON.
35
+ */
36
+ export function importPostman(collectionJson: string): ImportedPostmanCollection {
37
+ let parsed: unknown;
38
+ try {
39
+ parsed = JSON.parse(collectionJson);
40
+ } catch {
41
+ throw new Error('Invalid Postman collection: must be valid JSON');
42
+ }
43
+
44
+ const collection = parsed as Record<string, unknown>;
45
+
46
+ // Handle wrapped format: { info: { schema: ... }, item: [...] }
47
+ const info = collection['info'] as Record<string, unknown> | undefined;
48
+ const items = collection['item'] as PostmanItem[] | undefined;
49
+
50
+ if (!items || !Array.isArray(items)) {
51
+ throw new Error('Invalid Postman collection: missing "item" array');
52
+ }
53
+
54
+ const variables = collection['variable'] as Array<{ key: string; value: string }> | undefined;
55
+ const vars: Record<string, string> = {};
56
+ if (variables && Array.isArray(variables)) {
57
+ for (const v of variables) {
58
+ if (v.key) vars[v.key] = v.value ?? '';
59
+ }
60
+ }
61
+
62
+ const result: ImportedPostmanCollection = {
63
+ name: (info?.['name'] as string) ?? 'Imported Collection',
64
+ description: info?.['description'] as string,
65
+ folders: [],
66
+ requests: [],
67
+ variables: vars,
68
+ };
69
+
70
+ // Process items recursively
71
+ processItems(items, result);
72
+
73
+ return result;
74
+ }
75
+
76
+ interface PostmanItem {
77
+ name?: string;
78
+ item?: PostmanItem[];
79
+ request?: PostmanRequest;
80
+ description?: string;
81
+ event?: Array<{
82
+ listen: string;
83
+ script: { exec: string[] | string };
84
+ }>;
85
+ }
86
+
87
+ interface PostmanRequest {
88
+ method?: string;
89
+ url?: string | {
90
+ raw?: string;
91
+ protocol?: string;
92
+ host?: string[];
93
+ path?: string[];
94
+ query?: Array<{ key: string; value: string; disabled?: boolean }>;
95
+ variable?: Array<{ key: string; value: string }>;
96
+ };
97
+ header?: Array<{ key: string; value: string; disabled?: boolean }>;
98
+ body?: {
99
+ mode?: string;
100
+ raw?: string;
101
+ urlencoded?: Array<{ key: string; value: string; disabled?: boolean }>;
102
+ formdata?: Array<{ key: string; value: string; type?: string; disabled?: boolean }>;
103
+ graphql?: { query: string; variables?: string };
104
+ };
105
+ auth?: {
106
+ type?: string;
107
+ basic?: Array<{ key: string; value: string }>;
108
+ bearer?: Array<{ key: string; value: string }>;
109
+ apikey?: Array<{ key: string; value: string }>;
110
+ oauth2?: Array<{ key: string; value: string }>;
111
+ awsv4?: Array<{ key: string; value: string }>;
112
+ };
113
+ }
114
+
115
+ function processItems(items: PostmanItem[], result: ImportedPostmanCollection, folderName?: string): void {
116
+ for (const item of items) {
117
+ if (item.item && Array.isArray(item.item)) {
118
+ // This is a folder
119
+ const folder: ImportedFolder = {
120
+ name: item.name ?? 'Unnamed Folder',
121
+ description: item.description,
122
+ requests: [],
123
+ };
124
+
125
+ processFolderItems(item.item, folder);
126
+ result.folders.push(folder);
127
+ } else if (item.request) {
128
+ const request = parsePostmanRequest(item);
129
+ if (folderName) {
130
+ // Will be added to folder by caller
131
+ }
132
+ result.requests.push(request);
133
+ }
134
+ }
135
+ }
136
+
137
+ function processFolderItems(items: PostmanItem[], folder: ImportedFolder): void {
138
+ for (const item of items) {
139
+ if (item.request) {
140
+ folder.requests.push(parsePostmanRequest(item));
141
+ }
142
+ }
143
+ }
144
+
145
+ function parsePostmanRequest(item: PostmanItem): ImportedPostmanRequest {
146
+ const req = item.request!;
147
+ const method = (req.method?.toUpperCase() ?? 'GET') as HttpMethod;
148
+
149
+ // Parse URL
150
+ let url = '';
151
+ const params: ImportedPostmanRequest['params'] = [];
152
+
153
+ if (typeof req.url === 'string') {
154
+ url = req.url;
155
+ } else if (req.url) {
156
+ if (req.url.raw) {
157
+ url = req.url.raw;
158
+ } else if (req.url.host && req.url.path) {
159
+ url = `${req.url.protocol ?? 'https'}://${req.url.host.join('.')}/${req.url.path.join('/')}`;
160
+ }
161
+
162
+ if (req.url.query) {
163
+ for (const q of req.url.query) {
164
+ params.push({ key: q.key, value: q.value ?? '', enabled: !q.disabled });
165
+ }
166
+ }
167
+ }
168
+
169
+ // Parse headers
170
+ const headers: ImportedPostmanRequest['headers'] = [];
171
+ if (req.header) {
172
+ for (const h of req.header) {
173
+ headers.push({ key: h.key, value: h.value, enabled: !h.disabled });
174
+ }
175
+ }
176
+
177
+ // Parse body
178
+ let body: ImportedPostmanRequest['body'];
179
+ let bodyType: string | undefined;
180
+
181
+ if (req.body) {
182
+ switch (req.body.mode) {
183
+ case 'raw':
184
+ body = { raw: req.body.raw ?? '' };
185
+ bodyType = 'raw';
186
+ // Check if it's JSON
187
+ try {
188
+ JSON.parse(req.body.raw ?? '');
189
+ bodyType = 'json';
190
+ } catch { /* not json */ }
191
+ break;
192
+ case 'urlencoded':
193
+ body = {
194
+ form: (req.body.urlencoded ?? []).map((f) => ({
195
+ key: f.key,
196
+ value: f.value,
197
+ enabled: !f.disabled,
198
+ })),
199
+ };
200
+ bodyType = 'urlencoded';
201
+ break;
202
+ case 'formdata':
203
+ body = {
204
+ form: (req.body.formdata ?? []).map((f) => ({
205
+ key: f.key,
206
+ value: f.value,
207
+ enabled: !f.disabled,
208
+ })),
209
+ };
210
+ bodyType = 'form';
211
+ break;
212
+ case 'graphql':
213
+ if (req.body.graphql) {
214
+ body = {
215
+ raw: JSON.stringify({
216
+ query: req.body.graphql.query,
217
+ variables: req.body.graphql.variables
218
+ ? JSON.parse(req.body.graphql.variables)
219
+ : undefined,
220
+ }, null, 2),
221
+ };
222
+ bodyType = 'graphql';
223
+ }
224
+ break;
225
+ }
226
+ }
227
+
228
+ // Parse auth
229
+ let auth: unknown;
230
+ if (req.auth) {
231
+ auth = parsePostmanAuth(req.auth);
232
+ }
233
+
234
+ // Parse scripts
235
+ let preRequestScript: string | undefined;
236
+ let tests: string | undefined;
237
+
238
+ if (item.event) {
239
+ for (const event of item.event) {
240
+ const script = typeof event.script?.exec === 'string'
241
+ ? event.script.exec
242
+ : (event.script?.exec ?? []).join('\n');
243
+
244
+ if (event.listen === 'prerequest') {
245
+ preRequestScript = script;
246
+ } else if (event.listen === 'test') {
247
+ tests = script;
248
+ }
249
+ }
250
+ }
251
+
252
+ return {
253
+ name: item.name ?? 'Unnamed Request',
254
+ method,
255
+ url,
256
+ headers,
257
+ params,
258
+ body,
259
+ bodyType,
260
+ auth,
261
+ preRequestScript,
262
+ tests,
263
+ };
264
+ }
265
+
266
+ function parsePostmanAuth(auth: PostmanRequest['auth']): unknown {
267
+ if (!auth) return undefined;
268
+
269
+ const getAuthValue = (items: Array<{ key: string; value: string }> | undefined, key: string): string => {
270
+ const item = items?.find((i) => i.key === key);
271
+ return item?.value ?? '';
272
+ };
273
+
274
+ switch (auth.type) {
275
+ case 'basic':
276
+ return {
277
+ type: 'basic',
278
+ username: getAuthValue(auth.basic, 'username'),
279
+ password: getAuthValue(auth.basic, 'password'),
280
+ };
281
+ case 'bearer':
282
+ return {
283
+ type: 'bearer',
284
+ token: getAuthValue(auth.bearer, 'token'),
285
+ };
286
+ case 'apikey':
287
+ return {
288
+ type: 'apikey',
289
+ key: getAuthValue(auth.apikey, 'key'),
290
+ value: getAuthValue(auth.apikey, 'value'),
291
+ addTo: getAuthValue(auth.apikey, 'in') === 'query' ? 'query' : 'header',
292
+ };
293
+ case 'oauth2':
294
+ return {
295
+ type: 'oauth2',
296
+ accessTokenUrl: getAuthValue(auth.oauth2, 'accessTokenUrl'),
297
+ clientId: getAuthValue(auth.oauth2, 'clientId'),
298
+ clientSecret: getAuthValue(auth.oauth2, 'clientSecret'),
299
+ token: getAuthValue(auth.oauth2, 'accessToken'),
300
+ };
301
+ case 'awsv4':
302
+ return {
303
+ type: 'aws',
304
+ accessKey: getAuthValue(auth.awsv4, 'accessKey'),
305
+ secretKey: getAuthValue(auth.awsv4, 'secretKey'),
306
+ region: getAuthValue(auth.awsv4, 'region'),
307
+ service: getAuthValue(auth.awsv4, 'service'),
308
+ };
309
+ default:
310
+ return undefined;
311
+ }
312
+ }