@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,163 @@
1
+ /**
2
+ * Built-in assertion library for API tests.
3
+ */
4
+
5
+ export interface AssertionResult {
6
+ name: string;
7
+ passed: boolean;
8
+ error?: string;
9
+ duration: number;
10
+ }
11
+
12
+ /**
13
+ * Built-in assertion functions that can evaluate test expressions.
14
+ */
15
+ export const assertions = {
16
+ equals: (actual: unknown, expected: unknown): boolean => actual === expected,
17
+ notEquals: (actual: unknown, expected: unknown): boolean => actual !== expected,
18
+ greaterThan: (actual: number, expected: number): boolean => actual > expected,
19
+ lessThan: (actual: number, expected: number): boolean => actual < expected,
20
+ contains: (actual: string, expected: string): boolean => actual.includes(expected),
21
+ matches: (actual: string, pattern: RegExp): boolean => pattern.test(actual),
22
+ hasProperty: (obj: unknown, prop: string): boolean => typeof obj === 'object' && obj !== null && prop in obj,
23
+ isArray: (val: unknown): boolean => Array.isArray(val),
24
+ isObject: (val: unknown): boolean => typeof val === 'object' && val !== null && !Array.isArray(val),
25
+ isString: (val: unknown): boolean => typeof val === 'string',
26
+ isNumber: (val: unknown): boolean => typeof val === 'number',
27
+ isBoolean: (val: unknown): boolean => typeof val === 'boolean',
28
+ hasStatus: (status: number, expected: number): boolean => status === expected,
29
+ hasHeader: (headers: Record<string, string>, name: string): boolean => {
30
+ return headers[name.toLowerCase()] !== undefined;
31
+ },
32
+ hasHeaderWithValue: (headers: Record<string, string>, name: string, value: string): boolean => {
33
+ return headers[name.toLowerCase()] === value;
34
+ },
35
+ jsonPathEquals: (json: unknown, path: string, expected: unknown): boolean => {
36
+ const actual = getJsonPathValue(json, path);
37
+ return actual === expected;
38
+ },
39
+ jsonPathExists: (json: unknown, path: string): boolean => {
40
+ return getJsonPathValue(json, path) !== undefined;
41
+ },
42
+ };
43
+
44
+ function getJsonPathValue(data: unknown, path: string): unknown {
45
+ const parts = path.replace(/\[(\d+)]/g, '.$1').split('.');
46
+ let current: unknown = data;
47
+
48
+ for (const part of parts) {
49
+ if (current === null || current === undefined) return undefined;
50
+
51
+ if (Array.isArray(current)) {
52
+ const index = parseInt(part, 10);
53
+ if (isNaN(index)) return undefined;
54
+ current = current[index];
55
+ } else if (typeof current === 'object') {
56
+ current = (current as Record<string, unknown>)[part];
57
+ } else {
58
+ return undefined;
59
+ }
60
+ }
61
+
62
+ return current;
63
+ }
64
+
65
+ /**
66
+ * Run a set of simple assertions against a response.
67
+ */
68
+ /* eslint-disable @typescript-eslint/no-explicit-any */
69
+ export function runAssertions(
70
+ testCode: string,
71
+ response: {
72
+ status: number;
73
+ statusText: string;
74
+ headers: Record<string, string>;
75
+ body: string;
76
+ duration: number;
77
+ },
78
+ ): AssertionResult[] {
79
+ const results: AssertionResult[] = [];
80
+
81
+ try {
82
+ const json = (() => {
83
+ try { return JSON.parse(response.body); } catch { return null; }
84
+ })();
85
+
86
+ const pm: any = {
87
+ response: {
88
+ status: response.status,
89
+ statusText: response.statusText,
90
+ headers: response.headers,
91
+ body: response.body,
92
+ duration: response.duration,
93
+ json: () => json,
94
+ },
95
+ test: (name: string, fn: () => void) => {
96
+ const start = performance.now();
97
+ try {
98
+ fn();
99
+ const existing = results.find((r) => r.name === name);
100
+ if (!existing) {
101
+ results.push({
102
+ name,
103
+ passed: true,
104
+ duration: performance.now() - start,
105
+ });
106
+ }
107
+ } catch (e) {
108
+ results.push({
109
+ name,
110
+ passed: false,
111
+ error: e instanceof Error ? e.message : String(e),
112
+ duration: performance.now() - start,
113
+ });
114
+ }
115
+ },
116
+ expect: (val: any) => ({
117
+ to: {
118
+ equal: (expected: any) => {
119
+ results.push({ name: `Expect ${JSON.stringify(val)} to equal ${JSON.stringify(expected)}`, passed: val === expected, duration: 0 });
120
+ },
121
+ be: {
122
+ an: (type: string) => {
123
+ const typeMap: Record<string, (v: any) => boolean> = {
124
+ array: Array.isArray,
125
+ object: (v: any) => typeof v === 'object' && v !== null && !Array.isArray(v),
126
+ string: (v: any) => typeof v === 'string',
127
+ number: (v: any) => typeof v === 'number',
128
+ };
129
+ const check = typeMap[type];
130
+ results.push({ name: `Expect ${JSON.stringify(val)} to be ${type}`, passed: check ? check(val) : false, duration: 0 });
131
+ return { and: { greaterThan: (n: number) => { results.push({ name: `Expect ${JSON.stringify(val)} to be greater than ${n}`, passed: typeof val === 'number' && val > n, duration: 0 }); } } };
132
+ },
133
+ below: (n: number) => {
134
+ results.push({ name: `Expect ${JSON.stringify(val)} to be below ${n}`, passed: typeof val === 'number' && val < n, duration: 0 });
135
+ },
136
+ },
137
+ exist: () => {
138
+ results.push({ name: 'Expect value to exist', passed: val !== undefined && val !== null, duration: 0 });
139
+ },
140
+ have: {
141
+ status: (expected: number) => {
142
+ results.push({ name: `Status code is ${expected}`, passed: response.status === expected, duration: 0 });
143
+ },
144
+ },
145
+ },
146
+ }),
147
+ };
148
+
149
+ // Execute the test code in a sandboxed function
150
+ const fn = new Function('pm', 'console', testCode);
151
+ fn(pm, console);
152
+ } catch (e) {
153
+ results.push({
154
+ name: 'Test execution',
155
+ passed: false,
156
+ error: e instanceof Error ? e.message : String(e),
157
+ duration: 0,
158
+ });
159
+ }
160
+
161
+ return results;
162
+ }
163
+ /* eslint-enable @typescript-eslint/no-explicit-any */
@@ -0,0 +1,90 @@
1
+ import type { TestSuiteResult, TestRunResult } from './runner';
2
+
3
+ export interface TestReport {
4
+ summary: {
5
+ total: number;
6
+ passed: number;
7
+ failed: number;
8
+ duration: number;
9
+ timestamp: string;
10
+ };
11
+ results: Array<{
12
+ name: string;
13
+ method: string;
14
+ url: string;
15
+ status: number;
16
+ passed: boolean;
17
+ assertions: Array<{
18
+ name: string;
19
+ passed: boolean;
20
+ error?: string;
21
+ }>;
22
+ error?: string;
23
+ }>;
24
+ }
25
+
26
+ /**
27
+ * Generate a test report from suite results.
28
+ */
29
+ export function generateReport(suiteResult: TestSuiteResult): TestReport {
30
+ return {
31
+ summary: {
32
+ total: suiteResult.totalTests,
33
+ passed: suiteResult.passed,
34
+ failed: suiteResult.failed,
35
+ duration: suiteResult.duration,
36
+ timestamp: new Date().toISOString(),
37
+ },
38
+ results: suiteResult.results.map(formatResult),
39
+ };
40
+ }
41
+
42
+ function formatResult(result: TestRunResult): TestReport['results'][number] {
43
+ return {
44
+ name: result.requestName,
45
+ method: result.method,
46
+ url: result.url,
47
+ status: result.status,
48
+ passed: result.passed,
49
+ assertions: result.assertions.map((a) => ({
50
+ name: a.name,
51
+ passed: a.passed,
52
+ error: a.error,
53
+ })),
54
+ error: result.error,
55
+ };
56
+ }
57
+
58
+ /**
59
+ * Format a test report as Markdown.
60
+ */
61
+ export function reportToMarkdown(report: TestReport): string {
62
+ const lines: string[] = [
63
+ '# Test Report',
64
+ '',
65
+ `**Date:** ${report.summary.timestamp}`,
66
+ `**Total:** ${report.summary.total} | **Passed:** ${report.summary.passed} | **Failed:** ${report.summary.failed}`,
67
+ `**Duration:** ${report.summary.duration}ms`,
68
+ '',
69
+ ];
70
+
71
+ for (const result of report.results) {
72
+ const icon = result.passed ? 'PASS' : 'FAIL';
73
+ lines.push(`## [${icon}] ${result.name}`);
74
+ lines.push(`- **Method:** ${result.method}`);
75
+ lines.push(`- **URL:** ${result.url}`);
76
+ lines.push(`- **Status:** ${result.status}`);
77
+ lines.push('');
78
+
79
+ for (const assertion of result.assertions) {
80
+ const aIcon = assertion.passed ? 'PASS' : 'FAIL';
81
+ lines.push(` - [${aIcon}] ${assertion.name}`);
82
+ if (assertion.error) {
83
+ lines.push(` - Error: ${assertion.error}`);
84
+ }
85
+ }
86
+ lines.push('');
87
+ }
88
+
89
+ return lines.join('\n');
90
+ }
@@ -0,0 +1,69 @@
1
+ import { runAssertions, type AssertionResult } from './assertions';
2
+
3
+ export interface TestRunResult {
4
+ requestId: string;
5
+ requestName: string;
6
+ method: string;
7
+ url: string;
8
+ status: number;
9
+ duration: number;
10
+ assertions: AssertionResult[];
11
+ passed: boolean;
12
+ error?: string;
13
+ }
14
+
15
+ export interface TestSuiteResult {
16
+ totalTests: number;
17
+ passed: number;
18
+ failed: number;
19
+ duration: number;
20
+ results: TestRunResult[];
21
+ }
22
+
23
+ /**
24
+ * Run tests for a single request/response pair.
25
+ */
26
+ export function runRequestTests(
27
+ requestId: string,
28
+ requestName: string,
29
+ method: string,
30
+ url: string,
31
+ testCode: string,
32
+ response: {
33
+ status: number;
34
+ statusText: string;
35
+ headers: Record<string, string>;
36
+ body: string;
37
+ duration: number;
38
+ },
39
+ ): TestRunResult {
40
+ const assertionResults = runAssertions(testCode, response);
41
+ const allPassed = assertionResults.every((r) => r.passed);
42
+
43
+ return {
44
+ requestId,
45
+ requestName,
46
+ method,
47
+ url,
48
+ status: response.status,
49
+ duration: response.duration,
50
+ assertions: assertionResults,
51
+ passed: allPassed,
52
+ };
53
+ }
54
+
55
+ /**
56
+ * Aggregate results from multiple test runs into a suite.
57
+ */
58
+ export function aggregateTestResults(results: TestRunResult[]): TestSuiteResult {
59
+ const totalTests = results.reduce((sum, r) => sum + r.assertions.length, 0);
60
+ const passed = results.reduce((sum, r) => sum + r.assertions.filter((a) => a.passed).length, 0);
61
+
62
+ return {
63
+ totalTests,
64
+ passed,
65
+ failed: totalTests - passed,
66
+ duration: results.reduce((sum, r) => sum + r.duration, 0),
67
+ results,
68
+ };
69
+ }
@@ -0,0 +1,85 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { ZodError } from 'zod';
3
+ import { AppError, isAppError, toAppError } from './errors';
4
+
5
+ interface ApiSuccessResponse<T> {
6
+ success: true;
7
+ data: T;
8
+ meta?: {
9
+ page?: number;
10
+ limit?: number;
11
+ total?: number;
12
+ [key: string]: unknown;
13
+ };
14
+ }
15
+
16
+ interface ApiErrorResponse {
17
+ success: false;
18
+ error: {
19
+ message: string;
20
+ code: string;
21
+ details?: unknown;
22
+ };
23
+ }
24
+
25
+ export type ApiResponse<T> = ApiSuccessResponse<T> | ApiErrorResponse;
26
+
27
+ export function success<T>(data: T, meta?: ApiSuccessResponse<T>['meta']): NextResponse<ApiSuccessResponse<T>> {
28
+ return NextResponse.json(
29
+ { success: true, data, ...(meta ? { meta } : {}) },
30
+ { status: 200 },
31
+ );
32
+ }
33
+
34
+ export function created<T>(data: T): NextResponse<ApiSuccessResponse<T>> {
35
+ return NextResponse.json(
36
+ { success: true, data },
37
+ { status: 201 },
38
+ );
39
+ }
40
+
41
+ export function noContent(): NextResponse {
42
+ return new NextResponse(null, { status: 204 });
43
+ }
44
+
45
+ export function error(err: unknown): NextResponse<ApiErrorResponse> {
46
+ const appError = isAppError(err) ? err : toAppError(err);
47
+
48
+ if (err instanceof ZodError) {
49
+ return NextResponse.json(
50
+ {
51
+ success: false,
52
+ error: {
53
+ message: 'Validation failed',
54
+ code: 'VALIDATION_ERROR',
55
+ details: err.errors.map((e) => ({
56
+ path: e.path.join('.'),
57
+ message: e.message,
58
+ })),
59
+ },
60
+ },
61
+ { status: 400 },
62
+ );
63
+ }
64
+
65
+ return NextResponse.json(
66
+ {
67
+ success: false,
68
+ error: {
69
+ message: appError.message,
70
+ code: appError.code,
71
+ details: appError.details,
72
+ },
73
+ },
74
+ { status: appError.statusCode },
75
+ );
76
+ }
77
+
78
+ export function paginated<T>(
79
+ data: T[],
80
+ page: number,
81
+ limit: number,
82
+ total: number,
83
+ ): NextResponse<ApiSuccessResponse<T[]>> {
84
+ return success(data, { page, limit, total });
85
+ }
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from 'clsx';
2
+ import { twMerge } from 'tailwind-merge';
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }
@@ -0,0 +1,123 @@
1
+ export type ContentTypeCategory = 'json' | 'xml' | 'html' | 'text' | 'image' | 'binary' | 'form' | 'unknown';
2
+
3
+ export interface ContentTypeInfo {
4
+ category: ContentTypeCategory;
5
+ mimeType: string;
6
+ charset: string | null;
7
+ isText: boolean;
8
+ }
9
+
10
+ const MIME_CATEGORIES: Record<string, ContentTypeCategory> = {
11
+ 'application/json': 'json',
12
+ 'application/ld+json': 'json',
13
+ 'application/vnd.api+json': 'json',
14
+ 'application/hal+json': 'json',
15
+ 'application/problem+json': 'json',
16
+ 'text/json': 'json',
17
+ 'application/xml': 'xml',
18
+ 'text/xml': 'xml',
19
+ 'application/soap+xml': 'xml',
20
+ 'application/xhtml+xml': 'xml',
21
+ 'application/rss+xml': 'xml',
22
+ 'application/atom+xml': 'xml',
23
+ 'text/html': 'html',
24
+ 'text/plain': 'text',
25
+ 'text/csv': 'text',
26
+ 'text/markdown': 'text',
27
+ 'text/css': 'text',
28
+ 'text/javascript': 'text',
29
+ 'application/javascript': 'text',
30
+ 'application/x-www-form-urlencoded': 'form',
31
+ 'multipart/form-data': 'form',
32
+ 'image/png': 'image',
33
+ 'image/jpeg': 'image',
34
+ 'image/gif': 'image',
35
+ 'image/svg+xml': 'image',
36
+ 'image/webp': 'image',
37
+ 'image/bmp': 'image',
38
+ 'image/x-icon': 'image',
39
+ 'application/pdf': 'binary',
40
+ 'application/zip': 'binary',
41
+ 'application/gzip': 'binary',
42
+ 'application/octet-stream': 'binary',
43
+ };
44
+
45
+ const TEXT_CATEGORIES: Set<ContentTypeCategory> = new Set([
46
+ 'json', 'xml', 'html', 'text', 'form',
47
+ ]);
48
+
49
+ /**
50
+ * Parse content type header into structured info.
51
+ */
52
+ export function parseContentType(header: string | null): ContentTypeInfo {
53
+ if (!header) {
54
+ return { category: 'unknown', mimeType: 'application/octet-stream', charset: null, isText: false };
55
+ }
56
+
57
+ const parts = header.split(';').map((p) => p.trim());
58
+ const mimeType = (parts[0] ?? 'application/octet-stream').toLowerCase();
59
+
60
+ let charset: string | null = null;
61
+ for (let i = 1; i < parts.length; i++) {
62
+ const part = parts[i];
63
+ if (part?.startsWith('charset=')) {
64
+ charset = part.slice('charset='.length).replace(/"/g, '');
65
+ }
66
+ }
67
+
68
+ const category = MIME_CATEGORIES[mimeType] ?? 'binary';
69
+ const isText = TEXT_CATEGORIES.has(category) || mimeType.startsWith('text/');
70
+
71
+ return { category, mimeType, charset, isText };
72
+ }
73
+
74
+ /**
75
+ * Detect content type from response body content.
76
+ */
77
+ export function detectContentFromBody(body: string): ContentTypeCategory {
78
+ const trimmed = body.trim();
79
+
80
+ if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
81
+ try {
82
+ JSON.parse(trimmed);
83
+ return 'json';
84
+ } catch {
85
+ // Not valid JSON
86
+ }
87
+ }
88
+
89
+ if (trimmed.startsWith('<?xml') || trimmed.startsWith('<xml') || trimmed.startsWith('<rss')) {
90
+ return 'xml';
91
+ }
92
+
93
+ if (trimmed.startsWith('<!DOCTYPE') || trimmed.startsWith('<html') || trimmed.startsWith('<HTML')) {
94
+ return 'html';
95
+ }
96
+
97
+ return 'text';
98
+ }
99
+
100
+ /**
101
+ * Get file extension for a content type.
102
+ */
103
+ export function getExtensionForMimeType(mimeType: string): string {
104
+ const map: Record<string, string> = {
105
+ 'application/json': 'json',
106
+ 'application/xml': 'xml',
107
+ 'text/xml': 'xml',
108
+ 'text/html': 'html',
109
+ 'text/plain': 'txt',
110
+ 'text/csv': 'csv',
111
+ 'text/css': 'css',
112
+ 'application/javascript': 'js',
113
+ 'application/pdf': 'pdf',
114
+ 'image/png': 'png',
115
+ 'image/jpeg': 'jpg',
116
+ 'image/gif': 'gif',
117
+ 'image/svg+xml': 'svg',
118
+ 'image/webp': 'webp',
119
+ 'application/zip': 'zip',
120
+ 'application/gzip': 'gz',
121
+ };
122
+ return map[mimeType] ?? 'bin';
123
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Create a download trigger for files from the browser.
3
+ */
4
+ export function downloadFile(filename: string, content: string, mimeType: string = 'text/plain'): void {
5
+ const blob = new Blob([content], { type: mimeType });
6
+ const url = URL.createObjectURL(blob);
7
+ const link = document.createElement('a');
8
+ link.href = url;
9
+ link.download = filename;
10
+ document.body.appendChild(link);
11
+ link.click();
12
+ document.body.removeChild(link);
13
+ URL.revokeObjectURL(url);
14
+ }
15
+
16
+ /**
17
+ * Download binary data as a file.
18
+ */
19
+ export function downloadBinary(filename: string, data: Uint8Array, mimeType: string = 'application/octet-stream'): void {
20
+ const blob = new Blob([data as BlobPart], { type: mimeType });
21
+ const url = URL.createObjectURL(blob);
22
+ const link = document.createElement('a');
23
+ link.href = url;
24
+ link.download = filename;
25
+ document.body.appendChild(link);
26
+ link.click();
27
+ document.body.removeChild(link);
28
+ URL.revokeObjectURL(url);
29
+ }
30
+
31
+ /**
32
+ * Read a file as text.
33
+ */
34
+ export function readFileAsText(file: File): Promise<string> {
35
+ return new Promise((resolve, reject) => {
36
+ const reader = new FileReader();
37
+ reader.onload = () => resolve(reader.result as string);
38
+ reader.onerror = () => reject(new Error(`Failed to read file: ${file.name}`));
39
+ reader.readAsText(file);
40
+ });
41
+ }
42
+
43
+ /**
44
+ * Read a file as a data URL (for binary files).
45
+ */
46
+ export function readFileAsDataUrl(file: File): Promise<string> {
47
+ return new Promise((resolve, reject) => {
48
+ const reader = new FileReader();
49
+ reader.onload = () => resolve(reader.result as string);
50
+ reader.onerror = () => reject(new Error(`Failed to read file: ${file.name}`));
51
+ reader.readAsDataURL(file);
52
+ });
53
+ }
@@ -0,0 +1,92 @@
1
+ export class AppError extends Error {
2
+ constructor(
3
+ message: string,
4
+ public readonly statusCode: number = 500,
5
+ public readonly code: string = 'INTERNAL_ERROR',
6
+ public readonly details?: unknown,
7
+ ) {
8
+ super(message);
9
+ this.name = 'AppError';
10
+ Object.setPrototypeOf(this, AppError.prototype);
11
+ }
12
+ }
13
+
14
+ export class ValidationError extends AppError {
15
+ constructor(message: string, details?: unknown) {
16
+ super(message, 400, 'VALIDATION_ERROR', details);
17
+ this.name = 'ValidationError';
18
+ Object.setPrototypeOf(this, ValidationError.prototype);
19
+ }
20
+ }
21
+
22
+ export class NotFoundError extends AppError {
23
+ constructor(resource: string, id?: string) {
24
+ super(
25
+ id ? `${resource} with id '${id}' not found` : `${resource} not found`,
26
+ 404,
27
+ 'NOT_FOUND',
28
+ );
29
+ this.name = 'NotFoundError';
30
+ Object.setPrototypeOf(this, NotFoundError.prototype);
31
+ }
32
+ }
33
+
34
+ export class UnauthorizedError extends AppError {
35
+ constructor(message: string = 'Unauthorized') {
36
+ super(message, 401, 'UNAUTHORIZED');
37
+ this.name = 'UnauthorizedError';
38
+ Object.setPrototypeOf(this, UnauthorizedError.prototype);
39
+ }
40
+ }
41
+
42
+ export class ForbiddenError extends AppError {
43
+ constructor(message: string = 'Forbidden') {
44
+ super(message, 403, 'FORBIDDEN');
45
+ this.name = 'ForbiddenError';
46
+ Object.setPrototypeOf(this, ForbiddenError.prototype);
47
+ }
48
+ }
49
+
50
+ export class TimeoutError extends AppError {
51
+ constructor(timeout: number) {
52
+ super(`Request timed out after ${timeout}ms`, 408, 'TIMEOUT');
53
+ this.name = 'TimeoutError';
54
+ Object.setPrototypeOf(this, TimeoutError.prototype);
55
+ }
56
+ }
57
+
58
+ export class NetworkError extends AppError {
59
+ constructor(message: string) {
60
+ super(message, 502, 'NETWORK_ERROR');
61
+ this.name = 'NetworkError';
62
+ Object.setPrototypeOf(this, NetworkError.prototype);
63
+ }
64
+ }
65
+
66
+ export class SSLError extends AppError {
67
+ constructor(message: string) {
68
+ super(message, 502, 'SSL_ERROR');
69
+ this.name = 'SSLError';
70
+ Object.setPrototypeOf(this, SSLError.prototype);
71
+ }
72
+ }
73
+
74
+ export class RateLimitError extends AppError {
75
+ constructor(message: string = 'Rate limit exceeded') {
76
+ super(message, 429, 'RATE_LIMIT');
77
+ this.name = 'RateLimitError';
78
+ Object.setPrototypeOf(this, RateLimitError.prototype);
79
+ }
80
+ }
81
+
82
+ export function isAppError(error: unknown): error is AppError {
83
+ return error instanceof AppError;
84
+ }
85
+
86
+ export function toAppError(error: unknown): AppError {
87
+ if (isAppError(error)) return error;
88
+ if (error instanceof Error) {
89
+ return new AppError(error.message, 500, 'INTERNAL_ERROR');
90
+ }
91
+ return new AppError('An unknown error occurred', 500, 'UNKNOWN_ERROR');
92
+ }