@theihtisham/devtools-with-cloud 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +15 -0
- package/LICENSE +21 -0
- package/README.md +73 -0
- package/docker-compose.yml +23 -0
- package/jest.config.js +7 -0
- package/next-env.d.ts +5 -0
- package/next.config.mjs +22 -0
- package/package.json +82 -0
- package/postcss.config.js +6 -0
- package/prisma/schema.prisma +105 -0
- package/prisma/seed.ts +211 -0
- package/src/app/(app)/ai/page.tsx +122 -0
- package/src/app/(app)/collections/page.tsx +155 -0
- package/src/app/(app)/environments/page.tsx +96 -0
- package/src/app/(app)/history/page.tsx +107 -0
- package/src/app/(app)/import/page.tsx +102 -0
- package/src/app/(app)/layout.tsx +60 -0
- package/src/app/(app)/settings/page.tsx +79 -0
- package/src/app/(app)/workspace/page.tsx +284 -0
- package/src/app/api/ai/discover/route.ts +17 -0
- package/src/app/api/ai/explain/route.ts +29 -0
- package/src/app/api/ai/generate-tests/route.ts +37 -0
- package/src/app/api/ai/suggest/route.ts +29 -0
- package/src/app/api/collections/[id]/route.ts +66 -0
- package/src/app/api/collections/route.ts +48 -0
- package/src/app/api/environments/route.ts +40 -0
- package/src/app/api/export/openapi/route.ts +17 -0
- package/src/app/api/export/postman/route.ts +18 -0
- package/src/app/api/import/curl/route.ts +18 -0
- package/src/app/api/import/har/route.ts +20 -0
- package/src/app/api/import/openapi/route.ts +21 -0
- package/src/app/api/import/postman/route.ts +21 -0
- package/src/app/api/proxy/route.ts +35 -0
- package/src/app/api/requests/[id]/execute/route.ts +85 -0
- package/src/app/api/requests/[id]/history/route.ts +23 -0
- package/src/app/api/requests/[id]/route.ts +66 -0
- package/src/app/api/requests/route.ts +49 -0
- package/src/app/api/workspaces/route.ts +38 -0
- package/src/app/globals.css +99 -0
- package/src/app/layout.tsx +24 -0
- package/src/app/page.tsx +182 -0
- package/src/components/ai/ai-panel.tsx +65 -0
- package/src/components/ai/code-explainer.tsx +51 -0
- package/src/components/ai/endpoint-discovery.tsx +62 -0
- package/src/components/ai/test-generator.tsx +49 -0
- package/src/components/collections/collection-actions.tsx +36 -0
- package/src/components/collections/collection-tree.tsx +55 -0
- package/src/components/collections/folder-creator.tsx +54 -0
- package/src/components/landing/comparison.tsx +43 -0
- package/src/components/landing/cta.tsx +16 -0
- package/src/components/landing/features.tsx +24 -0
- package/src/components/landing/hero.tsx +23 -0
- package/src/components/response/body-viewer.tsx +33 -0
- package/src/components/response/headers-viewer.tsx +23 -0
- package/src/components/response/status-badge.tsx +25 -0
- package/src/components/response/test-results.tsx +50 -0
- package/src/components/response/timing-chart.tsx +39 -0
- package/src/components/ui/badge.tsx +24 -0
- package/src/components/ui/button.tsx +32 -0
- package/src/components/ui/code-editor.tsx +51 -0
- package/src/components/ui/dialog.tsx +56 -0
- package/src/components/ui/dropdown.tsx +63 -0
- package/src/components/ui/input.tsx +22 -0
- package/src/components/ui/key-value-editor.tsx +75 -0
- package/src/components/ui/select.tsx +24 -0
- package/src/components/ui/tabs.tsx +85 -0
- package/src/components/ui/textarea.tsx +22 -0
- package/src/components/ui/toast.tsx +54 -0
- package/src/components/workspace/request-panel.tsx +38 -0
- package/src/components/workspace/response-panel.tsx +81 -0
- package/src/components/workspace/sidebar.tsx +52 -0
- package/src/components/workspace/split-pane.tsx +49 -0
- package/src/components/workspace/tabs/auth-tab.tsx +94 -0
- package/src/components/workspace/tabs/body-tab.tsx +41 -0
- package/src/components/workspace/tabs/headers-tab.tsx +23 -0
- package/src/components/workspace/tabs/params-tab.tsx +23 -0
- package/src/components/workspace/tabs/pre-request-tab.tsx +26 -0
- package/src/components/workspace/url-bar.tsx +53 -0
- package/src/hooks/use-ai.ts +115 -0
- package/src/hooks/use-collection.ts +71 -0
- package/src/hooks/use-environment.ts +73 -0
- package/src/hooks/use-request.ts +111 -0
- package/src/lib/ai/endpoint-discovery.ts +158 -0
- package/src/lib/ai/explainer.ts +127 -0
- package/src/lib/ai/suggester.ts +164 -0
- package/src/lib/ai/test-generator.ts +161 -0
- package/src/lib/auth/api-key.ts +28 -0
- package/src/lib/auth/aws-sig.ts +131 -0
- package/src/lib/auth/basic.ts +17 -0
- package/src/lib/auth/bearer.ts +15 -0
- package/src/lib/auth/oauth2.ts +155 -0
- package/src/lib/auth/types.ts +16 -0
- package/src/lib/db/client.ts +15 -0
- package/src/lib/env/manager.ts +32 -0
- package/src/lib/env/resolver.ts +30 -0
- package/src/lib/exporters/openapi.ts +193 -0
- package/src/lib/exporters/postman.ts +140 -0
- package/src/lib/graphql/builder.ts +249 -0
- package/src/lib/graphql/formatter.ts +147 -0
- package/src/lib/graphql/index.ts +43 -0
- package/src/lib/graphql/introspection.ts +175 -0
- package/src/lib/graphql/types.ts +99 -0
- package/src/lib/graphql/validator.ts +216 -0
- package/src/lib/http/client.ts +112 -0
- package/src/lib/http/proxy.ts +83 -0
- package/src/lib/http/request-builder.ts +214 -0
- package/src/lib/http/response-parser.ts +106 -0
- package/src/lib/http/timing.ts +63 -0
- package/src/lib/importers/curl-parser.ts +346 -0
- package/src/lib/importers/har-parser.ts +128 -0
- package/src/lib/importers/openapi.ts +324 -0
- package/src/lib/importers/postman.ts +312 -0
- package/src/lib/test-runner/assertions.ts +163 -0
- package/src/lib/test-runner/reporter.ts +90 -0
- package/src/lib/test-runner/runner.ts +69 -0
- package/src/lib/utils/api-response.ts +85 -0
- package/src/lib/utils/cn.ts +6 -0
- package/src/lib/utils/content-type.ts +123 -0
- package/src/lib/utils/download.ts +53 -0
- package/src/lib/utils/errors.ts +92 -0
- package/src/lib/utils/format.ts +142 -0
- package/src/lib/utils/syntax-highlight.ts +108 -0
- package/src/lib/utils/validation.ts +231 -0
- package/src/lib/websocket/client.ts +182 -0
- package/src/lib/websocket/frames.ts +96 -0
- package/src/lib/websocket/history.ts +121 -0
- package/src/lib/websocket/index.ts +25 -0
- package/src/lib/websocket/types.ts +57 -0
- package/src/types/ai.ts +28 -0
- package/src/types/collection.ts +24 -0
- package/src/types/environment.ts +16 -0
- package/src/types/request.ts +54 -0
- package/src/types/response.ts +37 -0
- package/tailwind.config.ts +82 -0
- package/tests/lib/env/resolver.test.ts +108 -0
- package/tests/lib/graphql/builder.test.ts +349 -0
- package/tests/lib/graphql/formatter.test.ts +99 -0
- package/tests/lib/http/request-builder.test.ts +160 -0
- package/tests/lib/http/response-parser.test.ts +150 -0
- package/tests/lib/http/timing.test.ts +188 -0
- package/tests/lib/importers/curl-parser.test.ts +245 -0
- package/tests/lib/test-runner/assertions.test.ts +342 -0
- package/tests/lib/utils/cn.test.ts +46 -0
- package/tests/lib/utils/content-type.test.ts +175 -0
- package/tests/lib/utils/format.test.ts +188 -0
- package/tests/lib/utils/validation.test.ts +237 -0
- package/tests/lib/websocket/history.test.ts +186 -0
- package/tsconfig.json +29 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/vitest.config.ts +21 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI test generator - generates test assertions from request/response data.
|
|
3
|
+
* Uses OpenAI-compatible API.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
interface RequestData {
|
|
7
|
+
method: string;
|
|
8
|
+
url: string;
|
|
9
|
+
headers: Record<string, string>;
|
|
10
|
+
body?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface ResponseData {
|
|
14
|
+
status: number;
|
|
15
|
+
statusText: string;
|
|
16
|
+
headers: Record<string, string>;
|
|
17
|
+
body: string;
|
|
18
|
+
duration: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Generate test assertions from a request/response pair.
|
|
23
|
+
* Returns JavaScript test code as a string.
|
|
24
|
+
*/
|
|
25
|
+
export async function generateTests(
|
|
26
|
+
request: RequestData,
|
|
27
|
+
response: ResponseData,
|
|
28
|
+
apiKey?: string,
|
|
29
|
+
baseUrl?: string,
|
|
30
|
+
model?: string,
|
|
31
|
+
): Promise<string> {
|
|
32
|
+
// If no API key, generate basic tests locally
|
|
33
|
+
if (!apiKey) {
|
|
34
|
+
return generateLocalTests(request, response);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const openaiUrl = `${baseUrl ?? 'https://api.openai.com/v1'}/chat/completions`;
|
|
39
|
+
const responseJson = parseBody(response.body);
|
|
40
|
+
|
|
41
|
+
const prompt = buildTestGenerationPrompt(request, response, responseJson);
|
|
42
|
+
|
|
43
|
+
const res = await fetch(openaiUrl, {
|
|
44
|
+
method: 'POST',
|
|
45
|
+
headers: {
|
|
46
|
+
'Content-Type': 'application/json',
|
|
47
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
48
|
+
},
|
|
49
|
+
body: JSON.stringify({
|
|
50
|
+
model: model ?? 'gpt-4o',
|
|
51
|
+
messages: [
|
|
52
|
+
{ role: 'system', content: 'You are an API testing expert. Generate test assertions in JavaScript. Use pm.test(), pm.expect(), and pm.response APIs similar to Postman.' },
|
|
53
|
+
{ role: 'user', content: prompt },
|
|
54
|
+
],
|
|
55
|
+
temperature: 0.3,
|
|
56
|
+
max_tokens: 2000,
|
|
57
|
+
}),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
if (!res.ok) {
|
|
61
|
+
return generateLocalTests(request, response);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const data = await res.json() as { choices: Array<{ message: { content: string } }> };
|
|
65
|
+
const content = data.choices?.[0]?.message?.content;
|
|
66
|
+
if (content) {
|
|
67
|
+
// Extract code from markdown code blocks if present
|
|
68
|
+
const codeMatch = content.match(/```(?:javascript|js)?\n([\s\S]*?)```/);
|
|
69
|
+
return codeMatch?.[1]?.trim() ?? content.trim();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return generateLocalTests(request, response);
|
|
73
|
+
} catch {
|
|
74
|
+
return generateLocalTests(request, response);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function parseBody(body: string): unknown {
|
|
79
|
+
try {
|
|
80
|
+
return JSON.parse(body);
|
|
81
|
+
} catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function buildTestGenerationPrompt(request: RequestData, response: ResponseData, responseJson: unknown): string {
|
|
87
|
+
return `Generate 5-10 API test assertions for this request/response pair:
|
|
88
|
+
|
|
89
|
+
Request:
|
|
90
|
+
- Method: ${request.method}
|
|
91
|
+
- URL: ${request.url}
|
|
92
|
+
- Headers: ${JSON.stringify(request.headers)}
|
|
93
|
+
|
|
94
|
+
Response:
|
|
95
|
+
- Status: ${response.status} ${response.statusText}
|
|
96
|
+
- Duration: ${response.duration}ms
|
|
97
|
+
- Body: ${typeof responseJson === 'object' ? JSON.stringify(responseJson, null, 2).slice(0, 2000) : response.body.slice(0, 1000)}
|
|
98
|
+
|
|
99
|
+
Generate tests using pm.test() syntax:
|
|
100
|
+
- Status code checks
|
|
101
|
+
- Response body structure validation
|
|
102
|
+
- Field type/value assertions
|
|
103
|
+
- Performance checks
|
|
104
|
+
- Header checks
|
|
105
|
+
|
|
106
|
+
Output ONLY the JavaScript test code, no explanations.`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function generateLocalTests(request: RequestData, response: ResponseData): string {
|
|
110
|
+
const tests: string[] = [];
|
|
111
|
+
|
|
112
|
+
// Status code test
|
|
113
|
+
tests.push(`pm.test("Status code is ${response.status}", () => {
|
|
114
|
+
pm.expect(pm.response.status).to.equal(${response.status});
|
|
115
|
+
});`);
|
|
116
|
+
|
|
117
|
+
// Response time test
|
|
118
|
+
tests.push(`pm.test("Response time is less than 5000ms", () => {
|
|
119
|
+
pm.expect(pm.response.duration).to.be.below(5000);
|
|
120
|
+
});`);
|
|
121
|
+
|
|
122
|
+
// Content type test
|
|
123
|
+
const contentType = response.headers['content-type'];
|
|
124
|
+
if (contentType) {
|
|
125
|
+
tests.push(`pm.test("Has Content-Type header", () => {
|
|
126
|
+
pm.expect(pm.response.headers['content-type']).to.exist;
|
|
127
|
+
});`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// JSON body tests
|
|
131
|
+
try {
|
|
132
|
+
const json = JSON.parse(response.body);
|
|
133
|
+
if (Array.isArray(json)) {
|
|
134
|
+
tests.push(`pm.test("Response is an array", () => {
|
|
135
|
+
const json = pm.response.json();
|
|
136
|
+
pm.expect(json).to.be.an('array');
|
|
137
|
+
});`);
|
|
138
|
+
} else if (typeof json === 'object' && json !== null) {
|
|
139
|
+
const keys = Object.keys(json);
|
|
140
|
+
if (keys.length > 0) {
|
|
141
|
+
tests.push(`pm.test("Response is an object", () => {
|
|
142
|
+
const json = pm.response.json();
|
|
143
|
+
pm.expect(json).to.be.an('object');
|
|
144
|
+
});`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
} catch {
|
|
148
|
+
tests.push(`pm.test("Response has body", () => {
|
|
149
|
+
pm.expect(pm.response.body).to.exist;
|
|
150
|
+
});`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Method-specific tests
|
|
154
|
+
if (request.method === 'POST' && response.status === 201) {
|
|
155
|
+
tests.push(`pm.test("POST returns 201 Created", () => {
|
|
156
|
+
pm.expect(pm.response.status).to.equal(201);
|
|
157
|
+
});`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return tests.join('\n\n');
|
|
161
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ApiKeyAuthSchema } from '@/lib/utils/validation';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
type ApiKeyAuthConfig = z.infer<typeof ApiKeyAuthSchema>;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Apply API Key authentication to request headers, query params, or cookies.
|
|
8
|
+
*/
|
|
9
|
+
export function applyApiKeyAuth(
|
|
10
|
+
headers: Record<string, string>,
|
|
11
|
+
params: Record<string, string>,
|
|
12
|
+
config: ApiKeyAuthConfig,
|
|
13
|
+
): void {
|
|
14
|
+
switch (config.addTo) {
|
|
15
|
+
case 'header':
|
|
16
|
+
headers[config.key] = config.value;
|
|
17
|
+
break;
|
|
18
|
+
case 'query':
|
|
19
|
+
params[config.key] = config.value;
|
|
20
|
+
break;
|
|
21
|
+
case 'cookie': {
|
|
22
|
+
const existing = headers['Cookie'] ?? '';
|
|
23
|
+
const separator = existing ? '; ' : '';
|
|
24
|
+
headers['Cookie'] = `${existing}${separator}${config.key}=${config.value}`;
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { createHmac, createHash } from 'crypto';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AWS Signature Version 4 signing implementation.
|
|
5
|
+
* Signs HTTP requests for AWS services (S3, API Gateway, Lambda, etc.).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
interface AwsSignConfig {
|
|
9
|
+
accessKey: string;
|
|
10
|
+
secretKey: string;
|
|
11
|
+
region: string;
|
|
12
|
+
service: string;
|
|
13
|
+
sessionToken?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const AWS4_ALGORITHM = 'AWS4-HMAC-SHA256';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Apply AWS Signature v4 to request headers.
|
|
20
|
+
*/
|
|
21
|
+
export function applyAwsAuth(
|
|
22
|
+
headers: Record<string, string>,
|
|
23
|
+
method: string,
|
|
24
|
+
path: string,
|
|
25
|
+
accessKey: string,
|
|
26
|
+
secretKey: string,
|
|
27
|
+
region: string,
|
|
28
|
+
service: string,
|
|
29
|
+
body: string,
|
|
30
|
+
sessionToken?: string,
|
|
31
|
+
): void {
|
|
32
|
+
const now = new Date();
|
|
33
|
+
const dateStamp = formatDate(now);
|
|
34
|
+
const amzDate = formatAmzDate(now);
|
|
35
|
+
const host = headers['Host'] ?? headers['host'] ?? '';
|
|
36
|
+
|
|
37
|
+
// Create canonical request
|
|
38
|
+
const payloadHash = sha256Hex(body);
|
|
39
|
+
headers['x-amz-content-sha256'] = payloadHash;
|
|
40
|
+
headers['x-amz-date'] = amzDate;
|
|
41
|
+
if (sessionToken) {
|
|
42
|
+
headers['x-amz-security-token'] = sessionToken;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const signedHeaders = getSignedHeaders(headers, sessionToken !== undefined);
|
|
46
|
+
const canonicalHeaders = buildCanonicalHeaders(headers, signedHeaders);
|
|
47
|
+
const canonicalQueryString = ''; // Would need query string parsing for full support
|
|
48
|
+
|
|
49
|
+
const canonicalRequest = [
|
|
50
|
+
method,
|
|
51
|
+
path,
|
|
52
|
+
canonicalQueryString,
|
|
53
|
+
canonicalHeaders,
|
|
54
|
+
'',
|
|
55
|
+
signedHeaders,
|
|
56
|
+
payloadHash,
|
|
57
|
+
].join('\n');
|
|
58
|
+
|
|
59
|
+
// Create string to sign
|
|
60
|
+
const credentialScope = `${dateStamp}/${region}/${service}/aws4_request`;
|
|
61
|
+
const stringToSign = [
|
|
62
|
+
AWS4_ALGORITHM,
|
|
63
|
+
amzDate,
|
|
64
|
+
credentialScope,
|
|
65
|
+
sha256Hex(canonicalRequest),
|
|
66
|
+
].join('\n');
|
|
67
|
+
|
|
68
|
+
// Calculate signature
|
|
69
|
+
const signingKey = getSigningKey(secretKey, dateStamp, region, service);
|
|
70
|
+
const signature = hmacHex(signingKey, stringToSign);
|
|
71
|
+
|
|
72
|
+
// Build authorization header
|
|
73
|
+
headers['Authorization'] = `${AWS4_ALGORITHM} Credential=${accessKey}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function formatDate(date: Date): string {
|
|
77
|
+
return date.toISOString().replace(/[-:]/g, '').slice(0, 8);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function formatAmzDate(date: Date): string {
|
|
81
|
+
return date.toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, '');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function sha256Hex(data: string): string {
|
|
85
|
+
return createHash('sha256').update(data).digest('hex');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function hmacHex(key: Buffer, data: string): string {
|
|
89
|
+
return createHmac('sha256', key).update(data).digest('hex');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function hmacBuffer(key: Buffer | string, data: string): Buffer {
|
|
93
|
+
return createHmac('sha256', key).update(data).digest();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function getSigningKey(
|
|
97
|
+
secretKey: string,
|
|
98
|
+
dateStamp: string,
|
|
99
|
+
region: string,
|
|
100
|
+
service: string,
|
|
101
|
+
): Buffer {
|
|
102
|
+
const kDate = hmacBuffer(`AWS4${secretKey}`, dateStamp);
|
|
103
|
+
const kRegion = hmacBuffer(kDate, region);
|
|
104
|
+
const kService = hmacBuffer(kRegion, service);
|
|
105
|
+
return hmacBuffer(kService, 'aws4_request');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function getSignedHeaders(headers: Record<string, string>, hasSessionToken: boolean): string {
|
|
109
|
+
const headerNames = Object.keys(headers)
|
|
110
|
+
.map((k) => k.toLowerCase())
|
|
111
|
+
.filter((name) => {
|
|
112
|
+
// Sign specific headers
|
|
113
|
+
const signable = ['host', 'content-type', 'x-amz-date', 'x-amz-content-sha256'];
|
|
114
|
+
if (hasSessionToken) signable.push('x-amz-security-token');
|
|
115
|
+
return signable.includes(name);
|
|
116
|
+
})
|
|
117
|
+
.sort();
|
|
118
|
+
return headerNames.join(';');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function buildCanonicalHeaders(headers: Record<string, string>, signedHeaders: string): string {
|
|
122
|
+
const headerNames = signedHeaders.split(';');
|
|
123
|
+
return headerNames
|
|
124
|
+
.map((name) => {
|
|
125
|
+
const value = Object.entries(headers).find(
|
|
126
|
+
([k]) => k.toLowerCase() === name,
|
|
127
|
+
)?.[1] ?? '';
|
|
128
|
+
return `${name}:${value.trim()}`;
|
|
129
|
+
})
|
|
130
|
+
.join('\n');
|
|
131
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { BasicAuthSchema } from '@/lib/utils/validation';
|
|
2
|
+
|
|
3
|
+
type BasicAuthConfig = z.infer<typeof BasicAuthSchema>;
|
|
4
|
+
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Apply Basic authentication to request headers.
|
|
9
|
+
* Encodes username:password as base64 and adds Authorization header.
|
|
10
|
+
*/
|
|
11
|
+
export function applyBasicAuth(
|
|
12
|
+
headers: Record<string, string>,
|
|
13
|
+
config: BasicAuthConfig,
|
|
14
|
+
): void {
|
|
15
|
+
const credentials = btoa(`${config.username}:${config.password}`);
|
|
16
|
+
headers['Authorization'] = `Basic ${credentials}`;
|
|
17
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { BearerAuthSchema } from '@/lib/utils/validation';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
type BearerAuthConfig = z.infer<typeof BearerAuthSchema>;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Apply Bearer token authentication to request headers.
|
|
8
|
+
*/
|
|
9
|
+
export function applyBearerAuth(
|
|
10
|
+
headers: Record<string, string>,
|
|
11
|
+
config: BearerAuthConfig,
|
|
12
|
+
): void {
|
|
13
|
+
const prefix = config.prefix ?? 'Bearer';
|
|
14
|
+
headers['Authorization'] = `${prefix} ${config.token}`;
|
|
15
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import type { OAuth2AuthSchema } from '@/lib/utils/validation';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
type OAuth2AuthConfig = z.infer<typeof OAuth2AuthSchema>;
|
|
5
|
+
|
|
6
|
+
export interface OAuth2TokenResponse {
|
|
7
|
+
access_token: string;
|
|
8
|
+
token_type: string;
|
|
9
|
+
expires_in?: number;
|
|
10
|
+
refresh_token?: string;
|
|
11
|
+
scope?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get OAuth2 access token using client credentials flow.
|
|
16
|
+
*/
|
|
17
|
+
export async function getClientCredentialsToken(
|
|
18
|
+
config: OAuth2AuthConfig,
|
|
19
|
+
): Promise<OAuth2TokenResponse> {
|
|
20
|
+
if (!config.accessTokenUrl) {
|
|
21
|
+
throw new Error('Access token URL is required for client credentials flow');
|
|
22
|
+
}
|
|
23
|
+
if (!config.clientId || !config.clientSecret) {
|
|
24
|
+
throw new Error('Client ID and secret are required for client credentials flow');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const body = new URLSearchParams({
|
|
28
|
+
grant_type: 'client_credentials',
|
|
29
|
+
client_id: config.clientId,
|
|
30
|
+
client_secret: config.clientSecret,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
if (config.scope) {
|
|
34
|
+
body.set('scope', config.scope);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const response = await fetch(config.accessTokenUrl, {
|
|
38
|
+
method: 'POST',
|
|
39
|
+
headers: {
|
|
40
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
41
|
+
},
|
|
42
|
+
body: body.toString(),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (!response.ok) {
|
|
46
|
+
const errorText = await response.text();
|
|
47
|
+
throw new Error(`OAuth2 token request failed: ${response.status} ${errorText}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return response.json() as Promise<OAuth2TokenResponse>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get OAuth2 access token using password flow.
|
|
55
|
+
*/
|
|
56
|
+
export async function getPasswordToken(
|
|
57
|
+
config: OAuth2AuthConfig,
|
|
58
|
+
username: string,
|
|
59
|
+
password: string,
|
|
60
|
+
): Promise<OAuth2TokenResponse> {
|
|
61
|
+
if (!config.accessTokenUrl) {
|
|
62
|
+
throw new Error('Access token URL is required for password flow');
|
|
63
|
+
}
|
|
64
|
+
if (!config.clientId) {
|
|
65
|
+
throw new Error('Client ID is required for password flow');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const body = new URLSearchParams({
|
|
69
|
+
grant_type: 'password',
|
|
70
|
+
client_id: config.clientId,
|
|
71
|
+
username,
|
|
72
|
+
password,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
if (config.clientSecret) {
|
|
76
|
+
body.set('client_secret', config.clientSecret);
|
|
77
|
+
}
|
|
78
|
+
if (config.scope) {
|
|
79
|
+
body.set('scope', config.scope);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const response = await fetch(config.accessTokenUrl, {
|
|
83
|
+
method: 'POST',
|
|
84
|
+
headers: {
|
|
85
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
86
|
+
},
|
|
87
|
+
body: body.toString(),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if (!response.ok) {
|
|
91
|
+
const errorText = await response.text();
|
|
92
|
+
throw new Error(`OAuth2 token request failed: ${response.status} ${errorText}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return response.json() as Promise<OAuth2TokenResponse>;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Refresh an expired OAuth2 access token.
|
|
100
|
+
*/
|
|
101
|
+
export async function refreshOAuth2Token(
|
|
102
|
+
config: OAuth2AuthConfig,
|
|
103
|
+
refreshToken: string,
|
|
104
|
+
): Promise<OAuth2TokenResponse> {
|
|
105
|
+
if (!config.accessTokenUrl || !config.clientId) {
|
|
106
|
+
throw new Error('Access token URL and client ID are required for token refresh');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const body = new URLSearchParams({
|
|
110
|
+
grant_type: 'refresh_token',
|
|
111
|
+
client_id: config.clientId,
|
|
112
|
+
refresh_token: refreshToken,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
if (config.clientSecret) {
|
|
116
|
+
body.set('client_secret', config.clientSecret);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const response = await fetch(config.accessTokenUrl, {
|
|
120
|
+
method: 'POST',
|
|
121
|
+
headers: {
|
|
122
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
123
|
+
},
|
|
124
|
+
body: body.toString(),
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
if (!response.ok) {
|
|
128
|
+
const errorText = await response.text();
|
|
129
|
+
throw new Error(`OAuth2 token refresh failed: ${response.status} ${errorText}`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return response.json() as Promise<OAuth2TokenResponse>;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Build the authorization URL for the authorization code flow.
|
|
137
|
+
*/
|
|
138
|
+
export function buildAuthorizationUrl(config: OAuth2AuthConfig, state: string): string {
|
|
139
|
+
if (!config.authorizationUrl || !config.clientId) {
|
|
140
|
+
throw new Error('Authorization URL and client ID are required');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const params = new URLSearchParams({
|
|
144
|
+
response_type: 'code',
|
|
145
|
+
client_id: config.clientId,
|
|
146
|
+
redirect_uri: `${typeof window !== 'undefined' ? window.location.origin : ''}/oauth/callback`,
|
|
147
|
+
state,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
if (config.scope) {
|
|
151
|
+
params.set('scope', config.scope);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return `${config.authorizationUrl}?${params.toString()}`;
|
|
155
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { AuthConfig } from '@/lib/utils/validation';
|
|
2
|
+
|
|
3
|
+
export type { AuthConfig };
|
|
4
|
+
|
|
5
|
+
export interface AuthApplyResult {
|
|
6
|
+
headers: Record<string, string>;
|
|
7
|
+
params: Record<string, string>;
|
|
8
|
+
cookies: Record<string, string>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Base interface for auth handlers.
|
|
13
|
+
*/
|
|
14
|
+
export interface AuthHandler<T extends AuthConfig> {
|
|
15
|
+
apply(headers: Record<string, string>, config: T): void;
|
|
16
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { PrismaClient } from '@prisma/client';
|
|
2
|
+
|
|
3
|
+
const globalForPrisma = globalThis as unknown as {
|
|
4
|
+
prisma: PrismaClient | undefined;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export const prisma = globalForPrisma.prisma ?? new PrismaClient({
|
|
8
|
+
log: process.env['NODE_ENV'] === 'development' ? ['query', 'error', 'warn'] : ['error'],
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
if (process.env['NODE_ENV'] !== 'production') {
|
|
12
|
+
globalForPrisma.prisma = prisma;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default prisma;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
interface EnvironmentVariable {
|
|
2
|
+
key: string;
|
|
3
|
+
value: string;
|
|
4
|
+
type: string;
|
|
5
|
+
enabled: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface ResolvedEnvironment {
|
|
9
|
+
id: string;
|
|
10
|
+
name: string;
|
|
11
|
+
variables: Record<string, string>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Resolve environment variables into a simple key-value map.
|
|
16
|
+
*/
|
|
17
|
+
export function resolveEnvironmentVariables(vars: EnvironmentVariable[]): Record<string, string> {
|
|
18
|
+
const result: Record<string, string> = {};
|
|
19
|
+
for (const v of vars) {
|
|
20
|
+
if (v.enabled) {
|
|
21
|
+
result[v.key] = v.value;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Merge multiple environment variable maps (later ones override earlier ones).
|
|
29
|
+
*/
|
|
30
|
+
export function mergeEnvironments(...envMaps: Record<string, string>[]): Record<string, string> {
|
|
31
|
+
return envMaps.reduce((acc, map) => ({ ...acc, ...map }), {});
|
|
32
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve {{variable_name}} placeholders in strings.
|
|
3
|
+
* Replaces all occurrences of {{key}} with the corresponding value from the variables map.
|
|
4
|
+
*/
|
|
5
|
+
export function resolveVariables(input: string, variables: Record<string, string>): string {
|
|
6
|
+
return input.replace(/\{\{(\w+)\}\}/g, (match, key: string) => {
|
|
7
|
+
const value = variables[key];
|
|
8
|
+
if (value !== undefined) return value;
|
|
9
|
+
return match; // Leave unresolved variables as-is
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Extract all variable names from a string (e.g., {{base_url}} -> ['base_url']).
|
|
15
|
+
*/
|
|
16
|
+
export function extractVariables(input: string): string[] {
|
|
17
|
+
const matches = input.matchAll(/\{\{(\w+)\}\}/g);
|
|
18
|
+
const vars = new Set<string>();
|
|
19
|
+
for (const match of matches) {
|
|
20
|
+
if (match[1]) vars.add(match[1]);
|
|
21
|
+
}
|
|
22
|
+
return Array.from(vars);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Check if a string contains any unresolved variables.
|
|
27
|
+
*/
|
|
28
|
+
export function hasUnresolvedVariables(input: string): boolean {
|
|
29
|
+
return /\{\{\w+\}\}/.test(input);
|
|
30
|
+
}
|