@brainfish-ai/devdoc 0.1.50 → 0.1.52

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.
@@ -1,17 +1,17 @@
1
1
  import { NextResponse } from 'next/server';
2
2
  import { validateApiKey, addCustomDomain, isCustomDomainRegistered, updateCustomDomainStatus } from '@/lib/storage/blob';
3
3
  import { isValidDomain, normalizeDomain } from '@/lib/docs/config';
4
- import { addDomainToProject, isVercelIntegrationEnabled, formatVerificationInstructions, getDomainType } from '@/lib/vercel/domains';
4
+ import { addDomainToProject, isVercelIntegrationEnabled, formatVerificationInstructions } from '@/lib/vercel/domains';
5
5
  /**
6
6
  * POST /api/domains/add
7
7
  *
8
- * Add a custom domain to a project.
8
+ * Add a custom domain to a project via Vercel Domains API.
9
9
  * Each project can have ONE custom domain (free).
10
10
  *
11
- * When Vercel integration is enabled (VERCEL_API_TOKEN + VERCEL_PROJECT_ID):
12
- * - Domain is added to Vercel project
13
- * - Real DNS verification instructions from Vercel are returned
14
- * - SSL is provisioned automatically by Vercel after verification
11
+ * Requires environment variables:
12
+ * - VERCEL_API_TOKEN (or VERCEL_TOKEN)
13
+ * - VERCEL_PROJECT_ID
14
+ * - VERCEL_TEAM_ID (optional)
15
15
  *
16
16
  * Headers:
17
17
  * Authorization: Bearer <api_key>
@@ -25,7 +25,7 @@ import { addDomainToProject, isVercelIntegrationEnabled, formatVerificationInstr
25
25
  * domain: "docs.example.com",
26
26
  * status: "pending",
27
27
  * verification: [
28
- * { type: "TXT", domain: "_vercel.docs.example.com", value: "vc-domain-verify=..." }
28
+ * { type: "TXT", name: "_vercel.example.com", value: "vc-domain-verify=..." }
29
29
  * ]
30
30
  * }
31
31
  */ export async function POST(request) {
@@ -77,115 +77,79 @@ import { addDomainToProject, isVercelIntegrationEnabled, formatVerificationInstr
77
77
  status: 409
78
78
  });
79
79
  }
80
- // Check if Vercel integration is enabled
81
- if (isVercelIntegrationEnabled()) {
82
- // Add domain to Vercel project
83
- const vercelResult = await addDomainToProject(customDomain);
84
- if (!vercelResult.success) {
85
- return NextResponse.json({
86
- error: vercelResult.error,
87
- vercelError: vercelResult.vercelError
88
- }, {
89
- status: 400
90
- });
91
- }
92
- const vercelDomain = vercelResult.domain;
93
- // Add to our internal registry with Vercel data
94
- const result = await addCustomDomain(projectSlug, customDomain);
95
- if (!result.success) {
96
- // Rollback: Try to remove from Vercel if we couldn't add to registry
97
- console.error('[Domains API] Failed to add to registry, domain added to Vercel:', customDomain);
98
- return NextResponse.json({
99
- error: result.error
100
- }, {
101
- status: 400
102
- });
103
- }
104
- // Update with Vercel domain ID
105
- await updateCustomDomainStatus(customDomain, vercelDomain.verified ? 'active' : 'pending', {
106
- vercelDomainId: vercelDomain.projectId
80
+ // Require Vercel integration
81
+ if (!isVercelIntegrationEnabled()) {
82
+ return NextResponse.json({
83
+ error: 'Vercel integration not configured. Set VERCEL_API_TOKEN and VERCEL_PROJECT_ID environment variables.'
84
+ }, {
85
+ status: 503
107
86
  });
108
- // If Vercel already verified the domain (e.g., previously configured DNS)
109
- if (vercelDomain.verified) {
110
- return NextResponse.json({
111
- success: true,
112
- domain: customDomain,
113
- projectSlug,
114
- status: 'active',
115
- verified: true,
116
- message: 'Domain is already verified and active!'
117
- });
118
- }
119
- // Format Vercel's verification instructions
120
- const verification = vercelDomain.verification || [];
121
- const { records, instructions } = formatVerificationInstructions(verification);
122
- // Add CNAME instruction for subdomains (Vercel may not always include it)
123
- const domainType = getDomainType(customDomain);
124
- const parts = customDomain.split('.');
125
- const subdomain = parts.length > 2 ? parts[0] : '@';
126
- const cnameInstruction = domainType === 'subdomain' ? `\nAlso add a CNAME record:\n Name: ${subdomain}\n Value: cname.vercel-dns.com` : `\nFor apex domains, add an A record:\n Name: @\n Value: 76.76.21.21`;
87
+ }
88
+ // Add domain to Vercel project
89
+ const vercelResult = await addDomainToProject(customDomain);
90
+ if (!vercelResult.success) {
127
91
  return NextResponse.json({
128
- success: true,
129
- domain: customDomain,
130
- projectSlug,
131
- status: 'pending',
132
- verified: false,
133
- verification: records,
134
- instructions: [
135
- ...instructions,
136
- cnameInstruction
137
- ],
138
- vercelVerification: verification
92
+ error: vercelResult.error,
93
+ vercelError: vercelResult.vercelError
94
+ }, {
95
+ status: 400
139
96
  });
140
- } else {
141
- // Fallback: No Vercel integration, use legacy behavior
142
- console.warn('[Domains API] Vercel integration not configured, using legacy mode');
143
- const result = await addCustomDomain(projectSlug, customDomain);
144
- if (!result.success) {
145
- return NextResponse.json({
146
- error: result.error
147
- }, {
148
- status: 400
149
- });
150
- }
151
- // Legacy hardcoded DNS instructions
152
- const parts = customDomain.split('.');
153
- const subdomain = parts.length > 2 ? parts[0] : '@';
97
+ }
98
+ const vercelDomain = vercelResult.domain;
99
+ // Add to our internal registry with Vercel data
100
+ const result = await addCustomDomain(projectSlug, customDomain);
101
+ if (!result.success) {
102
+ // Rollback: Try to remove from Vercel if we couldn't add to registry
103
+ console.error('[Domains API] Failed to add to registry, domain added to Vercel:', customDomain);
104
+ return NextResponse.json({
105
+ error: result.error
106
+ }, {
107
+ status: 400
108
+ });
109
+ }
110
+ // Update with Vercel domain ID
111
+ await updateCustomDomainStatus(customDomain, vercelDomain.verified ? 'active' : 'pending', {
112
+ vercelDomainId: vercelDomain.projectId
113
+ });
114
+ // If Vercel already verified the domain (e.g., previously configured DNS)
115
+ if (vercelDomain.verified) {
154
116
  return NextResponse.json({
155
117
  success: true,
156
118
  domain: customDomain,
157
119
  projectSlug,
158
- status: result.entry.status,
159
- verified: false,
160
- verification: [
161
- {
162
- type: 'CNAME',
163
- name: subdomain,
164
- value: 'cname.vercel-dns.com'
165
- },
166
- {
167
- type: 'TXT',
168
- name: `_devdoc-verify.${customDomain}`,
169
- value: result.entry.verificationToken || ''
170
- }
171
- ],
172
- instructions: [
173
- 'Add the following DNS records to your domain:',
174
- '',
175
- '1. CNAME Record:',
176
- ` Name: ${subdomain}`,
177
- ' Value: cname.vercel-dns.com',
178
- '',
179
- '2. TXT Record (for verification):',
180
- ` Name: _devdoc-verify.${customDomain}`,
181
- ` Value: ${result.entry.verificationToken || ''}`,
182
- '',
183
- 'After adding DNS records, run "devdoc domain verify" to verify.',
184
- '',
185
- 'Note: Vercel integration not configured. Set VERCEL_API_TOKEN and VERCEL_PROJECT_ID for automatic SSL.'
186
- ]
120
+ status: 'active',
121
+ verified: true,
122
+ message: 'Domain is already verified and active!'
187
123
  });
188
124
  }
125
+ // Format Vercel's verification instructions
126
+ const verification = vercelDomain.verification || [];
127
+ const { records } = formatVerificationInstructions(verification);
128
+ // Add CNAME/A record for routing (Vercel only returns TXT for verification)
129
+ const parts = customDomain.split('.');
130
+ const isApexDomain = parts.length <= 2;
131
+ const subdomain = parts.length > 2 ? parts[0] : '@';
132
+ // Add routing record first, then verification records
133
+ const allRecords = [
134
+ isApexDomain ? {
135
+ type: 'A',
136
+ name: '@',
137
+ value: '76.76.21.21'
138
+ } : {
139
+ type: 'CNAME',
140
+ name: subdomain,
141
+ value: 'cname.vercel-dns.com'
142
+ },
143
+ ...records
144
+ ];
145
+ return NextResponse.json({
146
+ success: true,
147
+ domain: customDomain,
148
+ projectSlug,
149
+ status: 'pending',
150
+ verified: false,
151
+ verification: allRecords
152
+ });
189
153
  } catch (error) {
190
154
  console.error('[Domains API] Error adding domain:', error);
191
155
  const message = error instanceof Error ? error.message : String(error);
@@ -5,11 +5,13 @@ import { removeDomain as vercelRemoveDomain, isVercelIntegrationEnabled } from '
5
5
  /**
6
6
  * DELETE /api/domains/remove
7
7
  *
8
- * Remove a custom domain from a project.
8
+ * Remove a custom domain from a project via Vercel API.
9
+ * Removes from both Vercel and internal registry.
9
10
  *
10
- * When Vercel integration is enabled:
11
- * - Removes domain from Vercel project
12
- * - Removes from internal registry
11
+ * Requires environment variables:
12
+ * - VERCEL_API_TOKEN (or VERCEL_TOKEN)
13
+ * - VERCEL_PROJECT_ID
14
+ * - VERCEL_TEAM_ID (optional)
13
15
  *
14
16
  * Headers:
15
17
  * Authorization: Bearer <api_key>
@@ -57,15 +59,21 @@ import { removeDomain as vercelRemoveDomain, isVercelIntegrationEnabled } from '
57
59
  }
58
60
  customDomain = projectDomain.customDomain;
59
61
  }
60
- // Remove from Vercel if integration is enabled
61
- if (isVercelIntegrationEnabled()) {
62
- const vercelResult = await vercelRemoveDomain(customDomain);
63
- if (!vercelResult.success) {
64
- // Log warning but continue - domain might not exist in Vercel
65
- console.warn('[Domains API] Failed to remove from Vercel:', vercelResult.error);
66
- } else {
67
- console.log('[Domains API] Domain removed from Vercel:', customDomain);
68
- }
62
+ // Require Vercel integration
63
+ if (!isVercelIntegrationEnabled()) {
64
+ return NextResponse.json({
65
+ error: 'Vercel integration not configured. Set VERCEL_API_TOKEN and VERCEL_PROJECT_ID environment variables.'
66
+ }, {
67
+ status: 503
68
+ });
69
+ }
70
+ // Remove from Vercel
71
+ const vercelResult = await vercelRemoveDomain(customDomain);
72
+ if (!vercelResult.success) {
73
+ // Log warning but continue - domain might not exist in Vercel
74
+ console.warn('[Domains API] Failed to remove from Vercel:', vercelResult.error);
75
+ } else {
76
+ console.log('[Domains API] Domain removed from Vercel:', customDomain);
69
77
  }
70
78
  // Remove from internal registry
71
79
  const result = await removeCustomDomain(customDomain, projectSlug);
@@ -1,11 +1,17 @@
1
1
  import { NextResponse } from 'next/server';
2
2
  import { validateApiKey, getProjectCustomDomain, getCustomDomainEntry } from '@/lib/storage/blob';
3
- import { normalizeDomain, getDnsInstructions } from '@/lib/docs/config';
3
+ import { normalizeDomain } from '@/lib/docs/config';
4
4
  import { getProjectUrl } from '@/lib/multi-tenant/context';
5
+ import { getDomainConfig, isVercelIntegrationEnabled, formatVerificationInstructions } from '@/lib/vercel/domains';
5
6
  /**
6
7
  * GET /api/domains/status
7
8
  *
8
- * Get the status of a custom domain.
9
+ * Get the status of a custom domain via Vercel API.
10
+ *
11
+ * Requires environment variables:
12
+ * - VERCEL_API_TOKEN (or VERCEL_TOKEN)
13
+ * - VERCEL_PROJECT_ID
14
+ * - VERCEL_TEAM_ID (optional)
9
15
  *
10
16
  * Headers:
11
17
  * Authorization: Bearer <api_key>
@@ -94,26 +100,18 @@ import { getProjectUrl } from '@/lib/multi-tenant/context';
94
100
  switch(domainEntry.status){
95
101
  case 'pending':
96
102
  {
97
- const dnsInstructions = getDnsInstructions(domainEntry.customDomain);
98
- dnsInstructions.txt.value = domainEntry.verificationToken || '';
99
103
  response.message = 'Waiting for DNS configuration';
100
- response.dnsRecords = {
101
- cname: dnsInstructions.cname,
102
- txt: dnsInstructions.txt
103
- };
104
- response.nextStep = 'Add the DNS records above, then run "devdoc domain verify"';
104
+ response.nextStep = 'Add the DNS records below, then run "devdoc domain verify"';
105
+ // Get verification instructions from Vercel if available
106
+ if (isVercelIntegrationEnabled()) {
107
+ const configResult = await getDomainConfig(domainEntry.customDomain);
108
+ if (configResult.success && configResult.domain?.verification) {
109
+ const { records } = formatVerificationInstructions(configResult.domain.verification);
110
+ response.verification = records;
111
+ }
112
+ }
105
113
  break;
106
114
  }
107
- case 'dns_verified':
108
- response.message = 'DNS verified, SSL certificate provisioning in progress';
109
- response.verifiedAt = domainEntry.verifiedAt;
110
- response.nextStep = 'SSL certificate should be ready within 1-24 hours';
111
- break;
112
- case 'ssl_provisioning':
113
- response.message = 'SSL certificate being provisioned';
114
- response.verifiedAt = domainEntry.verifiedAt;
115
- response.nextStep = 'Almost ready! Check back in a few minutes';
116
- break;
117
115
  case 'active':
118
116
  response.message = 'Domain is active and working';
119
117
  response.customUrl = `https://${domainEntry.customDomain}`;
@@ -2,21 +2,16 @@ import { NextResponse } from 'next/server';
2
2
  import { validateApiKey, getProjectCustomDomain, getCustomDomainEntry, updateCustomDomainStatus } from '@/lib/storage/blob';
3
3
  import { normalizeDomain } from '@/lib/docs/config';
4
4
  import { verifyDomain as vercelVerifyDomain, getDomainConfig, isVercelIntegrationEnabled, formatVerificationInstructions } from '@/lib/vercel/domains';
5
- import dns from 'dns';
6
- import { promisify } from 'util';
7
- const resolveCname = promisify(dns.resolveCname);
8
- const resolveTxt = promisify(dns.resolveTxt);
9
5
  /**
10
6
  * POST /api/domains/verify
11
7
  *
12
- * Verify DNS configuration for a custom domain.
8
+ * Verify DNS configuration for a custom domain via Vercel API.
9
+ * Vercel handles DNS verification and automatic SSL provisioning.
13
10
  *
14
- * When Vercel integration is enabled:
15
- * - Calls Vercel's verify endpoint directly
16
- * - Vercel handles DNS verification and SSL provisioning
17
- *
18
- * Legacy mode (no Vercel integration):
19
- * - Checks CNAME and TXT records manually
11
+ * Requires environment variables:
12
+ * - VERCEL_API_TOKEN (or VERCEL_TOKEN)
13
+ * - VERCEL_PROJECT_ID
14
+ * - VERCEL_TEAM_ID (optional)
20
15
  *
21
16
  * Headers:
22
17
  * Authorization: Bearer <api_key>
@@ -84,142 +79,71 @@ const resolveTxt = promisify(dns.resolveTxt);
84
79
  status: 403
85
80
  });
86
81
  }
87
- // Use Vercel integration if available
88
- if (isVercelIntegrationEnabled()) {
89
- // Call Vercel's verify endpoint
90
- const verifyResult = await vercelVerifyDomain(customDomain);
91
- if (!verifyResult.success) {
92
- // Get current domain config to show what's needed
93
- const configResult = await getDomainConfig(customDomain);
94
- const verification = configResult.domain?.verification || [];
95
- let instructions = [];
96
- if (verification.length > 0) {
97
- const formatted = formatVerificationInstructions(verification);
98
- instructions = formatted.instructions;
99
- }
100
- return NextResponse.json({
101
- success: false,
102
- domain: customDomain,
103
- projectSlug,
104
- status: domainEntry.status,
105
- verified: false,
106
- message: verifyResult.error || 'DNS verification failed. Please check your DNS records.',
107
- verification: verification.map((v)=>({
108
- type: v.type,
109
- name: v.domain,
110
- value: v.value
111
- })),
112
- instructions
113
- });
114
- }
115
- // Domain verified successfully
116
- if (verifyResult.verified) {
117
- // Update our registry to mark as active
118
- await updateCustomDomainStatus(customDomain, 'active');
119
- return NextResponse.json({
120
- success: true,
121
- domain: customDomain,
122
- projectSlug,
123
- status: 'active',
124
- verified: true,
125
- message: 'Domain verified! SSL certificate will be provisioned automatically by Vercel.'
126
- });
127
- } else {
128
- // Vercel didn't verify yet, return what's needed
129
- const verification = verifyResult.domain?.verification || [];
130
- const { instructions } = formatVerificationInstructions(verification);
131
- return NextResponse.json({
132
- success: false,
133
- domain: customDomain,
134
- projectSlug,
135
- status: 'pending',
136
- verified: false,
137
- message: 'DNS records not yet propagated. This can take up to 48 hours.',
138
- verification: verification.map((v)=>({
139
- type: v.type,
140
- name: v.domain,
141
- value: v.value
142
- })),
143
- instructions
144
- });
145
- }
146
- }
147
- // Legacy verification (no Vercel integration)
148
- console.warn('[Domains API] Vercel integration not configured, using legacy DNS verification');
149
- // Check DNS records manually
150
- const checks = {
151
- cname: {
152
- found: false,
153
- value: null,
154
- expected: 'cname.vercel-dns.com'
155
- },
156
- txt: {
157
- found: false,
158
- verified: false,
159
- expected: domainEntry.verificationToken
160
- }
161
- };
162
- // Check CNAME record
163
- try {
164
- const cnameRecords = await resolveCname(customDomain);
165
- if (cnameRecords && cnameRecords.length > 0) {
166
- checks.cname.found = true;
167
- checks.cname.value = cnameRecords[0];
168
- }
169
- } catch {
170
- // CNAME not found or DNS error
82
+ // Require Vercel integration
83
+ if (!isVercelIntegrationEnabled()) {
84
+ return NextResponse.json({
85
+ error: 'Vercel integration not configured. Set VERCEL_API_TOKEN and VERCEL_PROJECT_ID environment variables.'
86
+ }, {
87
+ status: 503
88
+ });
171
89
  }
172
- // Check TXT record for verification
173
- const txtRecordName = `_devdoc-verify.${customDomain}`;
174
- try {
175
- const txtRecords = await resolveTxt(txtRecordName);
176
- if (txtRecords && txtRecords.length > 0) {
177
- checks.txt.found = true;
178
- // TXT records can be arrays, flatten and check
179
- const allTxtValues = txtRecords.flat();
180
- checks.txt.verified = allTxtValues.some((val)=>val === domainEntry.verificationToken);
90
+ // Call Vercel's verify endpoint
91
+ const verifyResult = await vercelVerifyDomain(customDomain);
92
+ if (!verifyResult.success) {
93
+ // Get current domain config to show what's needed
94
+ const configResult = await getDomainConfig(customDomain);
95
+ const verification = configResult.domain?.verification || [];
96
+ let instructions = [];
97
+ if (verification.length > 0) {
98
+ const formatted = formatVerificationInstructions(verification);
99
+ instructions = formatted.instructions;
181
100
  }
182
- } catch {
183
- // TXT not found or DNS error
101
+ return NextResponse.json({
102
+ success: false,
103
+ domain: customDomain,
104
+ projectSlug,
105
+ status: domainEntry.status,
106
+ verified: false,
107
+ message: verifyResult.error || 'DNS verification failed. Please check your DNS records.',
108
+ verification: verification.map((v)=>({
109
+ type: v.type,
110
+ name: v.domain,
111
+ value: v.value
112
+ })),
113
+ instructions
114
+ });
184
115
  }
185
- // Determine overall status
186
- const cnameValid = checks.cname.found && checks.cname.value?.toLowerCase().includes('vercel');
187
- const txtValid = checks.txt.found && checks.txt.verified;
188
- let newStatus = domainEntry.status;
189
- let message = '';
190
- if (cnameValid && txtValid) {
191
- // DNS is verified
192
- newStatus = 'active';
193
- message = 'DNS verified! Domain is now active.';
116
+ // Domain verified successfully
117
+ if (verifyResult.verified) {
118
+ // Update our registry to mark as active
194
119
  await updateCustomDomainStatus(customDomain, 'active');
195
- } else if (cnameValid && !txtValid) {
196
- message = 'CNAME record found, but TXT verification record is missing or incorrect.';
197
- } else if (!cnameValid && txtValid) {
198
- message = 'TXT verification found, but CNAME record is missing or incorrect.';
120
+ return NextResponse.json({
121
+ success: true,
122
+ domain: customDomain,
123
+ projectSlug,
124
+ status: 'active',
125
+ verified: true,
126
+ message: 'Domain verified! SSL certificate will be provisioned automatically by Vercel.'
127
+ });
199
128
  } else {
200
- message = 'DNS records not found. Please add the required CNAME and TXT records.';
129
+ // Vercel didn't verify yet, return what's needed
130
+ const verification = verifyResult.domain?.verification || [];
131
+ const { instructions } = formatVerificationInstructions(verification);
132
+ return NextResponse.json({
133
+ success: false,
134
+ domain: customDomain,
135
+ projectSlug,
136
+ status: 'pending',
137
+ verified: false,
138
+ message: 'DNS records not yet propagated. This can take up to 48 hours.',
139
+ verification: verification.map((v)=>({
140
+ type: v.type,
141
+ name: v.domain,
142
+ value: v.value
143
+ })),
144
+ instructions
145
+ });
201
146
  }
202
- return NextResponse.json({
203
- success: cnameValid && txtValid,
204
- domain: customDomain,
205
- projectSlug,
206
- status: newStatus,
207
- verified: cnameValid && txtValid,
208
- message,
209
- checks: {
210
- cname: {
211
- found: checks.cname.found,
212
- value: checks.cname.value,
213
- valid: cnameValid,
214
- expected: checks.cname.expected
215
- },
216
- txt: {
217
- found: checks.txt.found,
218
- verified: checks.txt.verified,
219
- expected: checks.txt.expected
220
- }
221
- }
222
- });
223
147
  } catch (error) {
224
148
  console.error('[Domains API] Error verifying domain:', error);
225
149
  const message = error instanceof Error ? error.message : String(error);
@@ -0,0 +1,101 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { put } from '@vercel/blob';
3
+ import { validateApiKey } from '@/lib/storage/blob';
4
+ /**
5
+ * POST /api/upload/spec
6
+ *
7
+ * Upload OpenAPI specs or GraphQL schemas to blob storage using multipart form data.
8
+ * All specs/schemas are stored in blob storage for multi-tenant support.
9
+ *
10
+ * Headers:
11
+ * Authorization: Bearer <api_key>
12
+ *
13
+ * Form Data:
14
+ * slug: string - Project slug
15
+ * fileName: string - File name (without extension)
16
+ * type: 'openapi' | 'graphql' - Type of schema
17
+ * file: File - The spec/schema file content
18
+ *
19
+ * Response:
20
+ * { success: true, url: string, path: string, type: string }
21
+ */ export async function POST(request) {
22
+ try {
23
+ // Validate API key
24
+ const authHeader = request.headers.get('Authorization');
25
+ const apiKey = authHeader?.replace('Bearer ', '');
26
+ if (!apiKey) {
27
+ return NextResponse.json({
28
+ error: 'API key required'
29
+ }, {
30
+ status: 401
31
+ });
32
+ }
33
+ const projectSlug = await validateApiKey(apiKey);
34
+ if (!projectSlug) {
35
+ return NextResponse.json({
36
+ error: 'Invalid API key'
37
+ }, {
38
+ status: 403
39
+ });
40
+ }
41
+ // Parse multipart form data
42
+ const formData = await request.formData();
43
+ const slug = formData.get('slug');
44
+ const fileName = formData.get('fileName');
45
+ const type = formData.get('type') || 'openapi' // Default to openapi for backwards compatibility
46
+ ;
47
+ const file = formData.get('file');
48
+ if (!slug || !fileName || !file) {
49
+ return NextResponse.json({
50
+ error: 'Missing slug, fileName, or file'
51
+ }, {
52
+ status: 400
53
+ });
54
+ }
55
+ // Validate type
56
+ if (type !== 'openapi' && type !== 'graphql') {
57
+ return NextResponse.json({
58
+ error: 'Invalid type. Must be "openapi" or "graphql"'
59
+ }, {
60
+ status: 400
61
+ });
62
+ }
63
+ // Verify slug matches API key's project
64
+ if (slug !== projectSlug) {
65
+ return NextResponse.json({
66
+ error: 'API key does not match project slug'
67
+ }, {
68
+ status: 403
69
+ });
70
+ }
71
+ // Determine blob path and content type based on type
72
+ const isGraphQL = type === 'graphql';
73
+ const extension = isGraphQL ? 'graphql' : 'json';
74
+ const contentType = isGraphQL ? 'text/plain' : 'application/json';
75
+ const folder = isGraphQL ? 'schemas' : 'specs';
76
+ const blobPath = `projects/${slug}/${folder}/${fileName}.${extension}`;
77
+ const blob = await put(blobPath, file.stream(), {
78
+ access: 'public',
79
+ contentType,
80
+ addRandomSuffix: false
81
+ });
82
+ console.log(`[Upload Schema] Uploaded ${type} ${fileName} for ${slug}: ${blob.url}`);
83
+ return NextResponse.json({
84
+ success: true,
85
+ url: blob.url,
86
+ path: blobPath,
87
+ type
88
+ });
89
+ } catch (error) {
90
+ console.error('[Upload Schema] Error:', error);
91
+ const message = error instanceof Error ? error.message : String(error);
92
+ return NextResponse.json({
93
+ error: 'Failed to upload schema',
94
+ details: message
95
+ }, {
96
+ status: 500
97
+ });
98
+ }
99
+ }
100
+ // Allow longer duration for large uploads
101
+ export const maxDuration = 60;