@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.
- package/dist/cli/commands/deploy.js +83 -3
- package/dist/cli/commands/domain.js +27 -109
- package/package.json +1 -1
- package/renderer/app/api/collections/route.js +34 -10
- package/renderer/app/api/deploy/route.js +10 -3
- package/renderer/app/api/domains/add/route.js +71 -107
- package/renderer/app/api/domains/remove/route.js +21 -13
- package/renderer/app/api/domains/status/route.js +17 -19
- package/renderer/app/api/domains/verify/route.js +65 -141
- package/renderer/app/api/upload/spec/route.js +101 -0
- package/renderer/lib/docs/config/domain-schema.js +0 -38
- package/renderer/lib/docs/config/index.js +1 -1
- package/renderer/lib/storage/blob.js +8 -12
|
@@ -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
|
|
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
|
-
*
|
|
12
|
-
* -
|
|
13
|
-
* -
|
|
14
|
-
* -
|
|
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",
|
|
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
|
-
//
|
|
81
|
-
if (isVercelIntegrationEnabled()) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
status:
|
|
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
|
-
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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:
|
|
159
|
-
verified:
|
|
160
|
-
|
|
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
|
-
*
|
|
11
|
-
* -
|
|
12
|
-
* -
|
|
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
|
-
//
|
|
61
|
-
if (isVercelIntegrationEnabled()) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
|
|
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
|
|
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.
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
*
|
|
15
|
-
* -
|
|
16
|
-
* -
|
|
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
|
-
//
|
|
88
|
-
if (isVercelIntegrationEnabled()) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
//
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
183
|
-
|
|
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
|
-
//
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
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;
|