@brainfish-ai/devdoc 0.1.30 → 0.1.32
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/ai-agents/CLAUDE.md +1 -1
- package/dist/cli/commands/deploy.js +65 -1
- package/dist/cli/commands/domain.d.ts +21 -0
- package/dist/cli/commands/domain.js +407 -0
- package/dist/cli/index.js +30 -1
- package/package.json +1 -1
- package/renderer/app/api/domains/add/route.ts +132 -0
- package/renderer/app/api/domains/lookup/route.ts +43 -0
- package/renderer/app/api/domains/remove/route.ts +100 -0
- package/renderer/app/api/domains/status/route.ts +158 -0
- package/renderer/app/api/domains/verify/route.ts +181 -0
- package/renderer/app/api/local-assets/[...path]/route.ts +122 -0
- package/renderer/app/globals.css +9 -0
- package/renderer/app/layout.tsx +8 -1
- package/renderer/components/docs/mdx/cards.tsx +35 -7
- package/renderer/components/docs/navigation/sidebar.tsx +5 -5
- package/renderer/components/docs-header.tsx +9 -5
- package/renderer/components/docs-viewer/sidebar/right-sidebar.tsx +5 -5
- package/renderer/lib/docs/config/domain-schema.ts +260 -0
- package/renderer/lib/docs/config/index.ts +14 -0
- package/renderer/lib/storage/blob.ts +242 -4
- package/renderer/public/file.svg +0 -1
- package/renderer/public/globe.svg +0 -1
- package/renderer/public/logo.svg +0 -9
- package/renderer/public/window.svg +0 -1
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server'
|
|
2
|
+
import {
|
|
3
|
+
validateApiKey,
|
|
4
|
+
addCustomDomain,
|
|
5
|
+
isCustomDomainRegistered,
|
|
6
|
+
} from '@/lib/storage/blob'
|
|
7
|
+
import { isValidDomain, normalizeDomain, getDnsInstructions } from '@/lib/docs/config'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* POST /api/domains/add
|
|
11
|
+
*
|
|
12
|
+
* Add a custom domain to a project.
|
|
13
|
+
* Each project can have ONE custom domain (free).
|
|
14
|
+
*
|
|
15
|
+
* Headers:
|
|
16
|
+
* Authorization: Bearer <api_key>
|
|
17
|
+
*
|
|
18
|
+
* Body:
|
|
19
|
+
* { customDomain: "docs.example.com" }
|
|
20
|
+
*
|
|
21
|
+
* Response:
|
|
22
|
+
* {
|
|
23
|
+
* success: true,
|
|
24
|
+
* domain: "docs.example.com",
|
|
25
|
+
* status: "pending",
|
|
26
|
+
* verification: {
|
|
27
|
+
* cname: { name: "docs", value: "cname.devdoc-dns.com" },
|
|
28
|
+
* txt: { name: "_devdoc-verify.docs.example.com", value: "devdoc-verify=xxx" }
|
|
29
|
+
* }
|
|
30
|
+
* }
|
|
31
|
+
*/
|
|
32
|
+
export async function POST(request: NextRequest) {
|
|
33
|
+
try {
|
|
34
|
+
// Validate API key
|
|
35
|
+
const authHeader = request.headers.get('Authorization')
|
|
36
|
+
const apiKey = authHeader?.replace('Bearer ', '')
|
|
37
|
+
|
|
38
|
+
if (!apiKey) {
|
|
39
|
+
return NextResponse.json(
|
|
40
|
+
{ error: 'API key required. Provide via Authorization header.' },
|
|
41
|
+
{ status: 401 }
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const projectSlug = await validateApiKey(apiKey)
|
|
46
|
+
if (!projectSlug) {
|
|
47
|
+
return NextResponse.json(
|
|
48
|
+
{ error: 'Invalid API key' },
|
|
49
|
+
{ status: 403 }
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Parse request body
|
|
54
|
+
const body = await request.json()
|
|
55
|
+
const { customDomain: rawDomain } = body
|
|
56
|
+
|
|
57
|
+
if (!rawDomain || typeof rawDomain !== 'string') {
|
|
58
|
+
return NextResponse.json(
|
|
59
|
+
{ error: 'Missing customDomain in request body' },
|
|
60
|
+
{ status: 400 }
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Normalize and validate domain
|
|
65
|
+
const customDomain = normalizeDomain(rawDomain)
|
|
66
|
+
const validation = isValidDomain(customDomain)
|
|
67
|
+
|
|
68
|
+
if (!validation.valid) {
|
|
69
|
+
return NextResponse.json(
|
|
70
|
+
{ error: validation.error },
|
|
71
|
+
{ status: 400 }
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Check if domain is already registered
|
|
76
|
+
const isRegistered = await isCustomDomainRegistered(customDomain)
|
|
77
|
+
if (isRegistered) {
|
|
78
|
+
return NextResponse.json(
|
|
79
|
+
{ error: `Domain ${customDomain} is already registered to another project.` },
|
|
80
|
+
{ status: 409 }
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Add the custom domain
|
|
85
|
+
const result = await addCustomDomain(projectSlug, customDomain)
|
|
86
|
+
|
|
87
|
+
if (!result.success) {
|
|
88
|
+
return NextResponse.json(
|
|
89
|
+
{ error: result.error },
|
|
90
|
+
{ status: 400 }
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Get DNS instructions
|
|
95
|
+
const dnsInstructions = getDnsInstructions(customDomain)
|
|
96
|
+
|
|
97
|
+
// Add verification token to TXT record
|
|
98
|
+
dnsInstructions.txt.value = result.entry!.verificationToken || ''
|
|
99
|
+
|
|
100
|
+
return NextResponse.json({
|
|
101
|
+
success: true,
|
|
102
|
+
domain: customDomain,
|
|
103
|
+
projectSlug,
|
|
104
|
+
status: result.entry!.status,
|
|
105
|
+
verification: {
|
|
106
|
+
cname: dnsInstructions.cname,
|
|
107
|
+
txt: dnsInstructions.txt,
|
|
108
|
+
},
|
|
109
|
+
instructions: [
|
|
110
|
+
'Add the following DNS records to your domain:',
|
|
111
|
+
'',
|
|
112
|
+
`1. CNAME Record:`,
|
|
113
|
+
` Name: ${dnsInstructions.cname.name}`,
|
|
114
|
+
` Value: ${dnsInstructions.cname.value}`,
|
|
115
|
+
'',
|
|
116
|
+
`2. TXT Record (for verification):`,
|
|
117
|
+
` Name: ${dnsInstructions.txt.name}`,
|
|
118
|
+
` Value: ${dnsInstructions.txt.value}`,
|
|
119
|
+
'',
|
|
120
|
+
'After adding DNS records, run "devdoc domain verify" to verify.',
|
|
121
|
+
],
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
} catch (error) {
|
|
125
|
+
console.error('[Domains API] Error adding domain:', error)
|
|
126
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
127
|
+
return NextResponse.json(
|
|
128
|
+
{ error: 'Failed to add domain', details: message },
|
|
129
|
+
{ status: 500 }
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server'
|
|
2
|
+
import { lookupCustomDomain } from '@/lib/storage/blob'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* GET /api/domains/lookup
|
|
6
|
+
*
|
|
7
|
+
* Internal API for middleware to look up custom domains.
|
|
8
|
+
* Returns the project slug for an active custom domain.
|
|
9
|
+
*
|
|
10
|
+
* Query:
|
|
11
|
+
* ?domain=docs.example.com
|
|
12
|
+
*
|
|
13
|
+
* Response:
|
|
14
|
+
* { found: true, projectSlug: "my-project" }
|
|
15
|
+
* or
|
|
16
|
+
* { found: false }
|
|
17
|
+
*/
|
|
18
|
+
export async function GET(request: NextRequest) {
|
|
19
|
+
try {
|
|
20
|
+
const { searchParams } = new URL(request.url)
|
|
21
|
+
const domain = searchParams.get('domain')
|
|
22
|
+
|
|
23
|
+
if (!domain) {
|
|
24
|
+
return NextResponse.json({ found: false, error: 'Missing domain parameter' })
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const entry = await lookupCustomDomain(domain)
|
|
28
|
+
|
|
29
|
+
if (entry) {
|
|
30
|
+
return NextResponse.json({
|
|
31
|
+
found: true,
|
|
32
|
+
projectSlug: entry.projectSlug,
|
|
33
|
+
status: entry.status,
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return NextResponse.json({ found: false })
|
|
38
|
+
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.error('[Domains Lookup] Error:', error)
|
|
41
|
+
return NextResponse.json({ found: false })
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server'
|
|
2
|
+
import {
|
|
3
|
+
validateApiKey,
|
|
4
|
+
getProjectCustomDomain,
|
|
5
|
+
removeCustomDomain,
|
|
6
|
+
} from '@/lib/storage/blob'
|
|
7
|
+
import { normalizeDomain } from '@/lib/docs/config'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* DELETE /api/domains/remove
|
|
11
|
+
*
|
|
12
|
+
* Remove a custom domain from a project.
|
|
13
|
+
*
|
|
14
|
+
* Headers:
|
|
15
|
+
* Authorization: Bearer <api_key>
|
|
16
|
+
*
|
|
17
|
+
* Body:
|
|
18
|
+
* { customDomain: "docs.example.com" } // Optional, removes project's domain if not specified
|
|
19
|
+
*
|
|
20
|
+
* Response:
|
|
21
|
+
* {
|
|
22
|
+
* success: true,
|
|
23
|
+
* message: "Domain docs.example.com removed successfully"
|
|
24
|
+
* }
|
|
25
|
+
*/
|
|
26
|
+
export async function DELETE(request: NextRequest) {
|
|
27
|
+
try {
|
|
28
|
+
// Validate API key
|
|
29
|
+
const authHeader = request.headers.get('Authorization')
|
|
30
|
+
const apiKey = authHeader?.replace('Bearer ', '')
|
|
31
|
+
|
|
32
|
+
if (!apiKey) {
|
|
33
|
+
return NextResponse.json(
|
|
34
|
+
{ error: 'API key required. Provide via Authorization header.' },
|
|
35
|
+
{ status: 401 }
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const projectSlug = await validateApiKey(apiKey)
|
|
40
|
+
if (!projectSlug) {
|
|
41
|
+
return NextResponse.json(
|
|
42
|
+
{ error: 'Invalid API key' },
|
|
43
|
+
{ status: 403 }
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Parse request body
|
|
48
|
+
const body = await request.json().catch(() => ({}))
|
|
49
|
+
let customDomain = body.customDomain ? normalizeDomain(body.customDomain) : null
|
|
50
|
+
|
|
51
|
+
// If no domain specified, get the project's custom domain
|
|
52
|
+
if (!customDomain) {
|
|
53
|
+
const projectDomain = await getProjectCustomDomain(projectSlug)
|
|
54
|
+
if (!projectDomain) {
|
|
55
|
+
return NextResponse.json(
|
|
56
|
+
{ error: 'No custom domain configured for this project' },
|
|
57
|
+
{ status: 404 }
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
customDomain = projectDomain.customDomain
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Remove the domain
|
|
64
|
+
const result = await removeCustomDomain(customDomain, projectSlug)
|
|
65
|
+
|
|
66
|
+
if (!result.success) {
|
|
67
|
+
return NextResponse.json(
|
|
68
|
+
{ error: result.error },
|
|
69
|
+
{ status: 400 }
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// TODO: In production, also remove from Vercel via API
|
|
74
|
+
// await vercelApi.removeDomain(customDomain)
|
|
75
|
+
|
|
76
|
+
return NextResponse.json({
|
|
77
|
+
success: true,
|
|
78
|
+
message: `Domain ${customDomain} removed successfully`,
|
|
79
|
+
domain: customDomain,
|
|
80
|
+
projectSlug,
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
} catch (error) {
|
|
84
|
+
console.error('[Domains API] Error removing domain:', error)
|
|
85
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
86
|
+
return NextResponse.json(
|
|
87
|
+
{ error: 'Failed to remove domain', details: message },
|
|
88
|
+
{ status: 500 }
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* POST /api/domains/remove
|
|
95
|
+
*
|
|
96
|
+
* Alternative method for removing domain (some clients don't support DELETE with body)
|
|
97
|
+
*/
|
|
98
|
+
export async function POST(request: NextRequest) {
|
|
99
|
+
return DELETE(request)
|
|
100
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server'
|
|
2
|
+
import {
|
|
3
|
+
validateApiKey,
|
|
4
|
+
getProjectCustomDomain,
|
|
5
|
+
getCustomDomainEntry,
|
|
6
|
+
} from '@/lib/storage/blob'
|
|
7
|
+
import { normalizeDomain, getDnsInstructions } from '@/lib/docs/config'
|
|
8
|
+
import { getProjectUrl } from '@/lib/multi-tenant/context'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* GET /api/domains/status
|
|
12
|
+
*
|
|
13
|
+
* Get the status of a custom domain.
|
|
14
|
+
*
|
|
15
|
+
* Headers:
|
|
16
|
+
* Authorization: Bearer <api_key>
|
|
17
|
+
*
|
|
18
|
+
* Query:
|
|
19
|
+
* ?domain=docs.example.com // Optional, uses project's domain if not specified
|
|
20
|
+
*
|
|
21
|
+
* Response:
|
|
22
|
+
* {
|
|
23
|
+
* domain: "docs.example.com",
|
|
24
|
+
* status: "active",
|
|
25
|
+
* projectSlug: "my-project",
|
|
26
|
+
* projectUrl: "https://my-project.devdoc.sh",
|
|
27
|
+
* customUrl: "https://docs.example.com",
|
|
28
|
+
* createdAt: "2026-01-24T...",
|
|
29
|
+
* verifiedAt: "2026-01-24T..."
|
|
30
|
+
* }
|
|
31
|
+
*/
|
|
32
|
+
export async function GET(request: NextRequest) {
|
|
33
|
+
try {
|
|
34
|
+
// Validate API key
|
|
35
|
+
const authHeader = request.headers.get('Authorization')
|
|
36
|
+
const apiKey = authHeader?.replace('Bearer ', '')
|
|
37
|
+
|
|
38
|
+
if (!apiKey) {
|
|
39
|
+
return NextResponse.json(
|
|
40
|
+
{ error: 'API key required. Provide via Authorization header.' },
|
|
41
|
+
{ status: 401 }
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const projectSlug = await validateApiKey(apiKey)
|
|
46
|
+
if (!projectSlug) {
|
|
47
|
+
return NextResponse.json(
|
|
48
|
+
{ error: 'Invalid API key' },
|
|
49
|
+
{ status: 403 }
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Get domain from query or use project's domain
|
|
54
|
+
const { searchParams } = new URL(request.url)
|
|
55
|
+
let customDomain = searchParams.get('domain')
|
|
56
|
+
|
|
57
|
+
if (customDomain) {
|
|
58
|
+
customDomain = normalizeDomain(customDomain)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let domainEntry
|
|
62
|
+
|
|
63
|
+
if (customDomain) {
|
|
64
|
+
// Get specific domain
|
|
65
|
+
domainEntry = await getCustomDomainEntry(customDomain)
|
|
66
|
+
|
|
67
|
+
if (!domainEntry) {
|
|
68
|
+
return NextResponse.json(
|
|
69
|
+
{ error: `Domain ${customDomain} not found` },
|
|
70
|
+
{ status: 404 }
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Verify domain belongs to this project
|
|
75
|
+
if (domainEntry.projectSlug !== projectSlug) {
|
|
76
|
+
return NextResponse.json(
|
|
77
|
+
{ error: `Domain ${customDomain} does not belong to this project` },
|
|
78
|
+
{ status: 403 }
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
} else {
|
|
82
|
+
// Get project's custom domain
|
|
83
|
+
domainEntry = await getProjectCustomDomain(projectSlug)
|
|
84
|
+
|
|
85
|
+
if (!domainEntry) {
|
|
86
|
+
return NextResponse.json({
|
|
87
|
+
hasCustomDomain: false,
|
|
88
|
+
projectSlug,
|
|
89
|
+
projectUrl: getProjectUrl(projectSlug),
|
|
90
|
+
message: 'No custom domain configured. Use /api/domains/add to add one.',
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Build response based on status
|
|
96
|
+
const projectUrl = getProjectUrl(projectSlug)
|
|
97
|
+
const response: Record<string, unknown> = {
|
|
98
|
+
hasCustomDomain: true,
|
|
99
|
+
domain: domainEntry.customDomain,
|
|
100
|
+
status: domainEntry.status,
|
|
101
|
+
projectSlug,
|
|
102
|
+
projectUrl,
|
|
103
|
+
createdAt: domainEntry.createdAt,
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Add status-specific information
|
|
107
|
+
switch (domainEntry.status) {
|
|
108
|
+
case 'pending': {
|
|
109
|
+
const dnsInstructions = getDnsInstructions(domainEntry.customDomain)
|
|
110
|
+
dnsInstructions.txt.value = domainEntry.verificationToken || ''
|
|
111
|
+
|
|
112
|
+
response.message = 'Waiting for DNS configuration'
|
|
113
|
+
response.dnsRecords = {
|
|
114
|
+
cname: dnsInstructions.cname,
|
|
115
|
+
txt: dnsInstructions.txt,
|
|
116
|
+
}
|
|
117
|
+
response.nextStep = 'Add the DNS records above, then run "devdoc domain verify"'
|
|
118
|
+
break
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
case 'dns_verified':
|
|
122
|
+
response.message = 'DNS verified, SSL certificate provisioning in progress'
|
|
123
|
+
response.verifiedAt = domainEntry.verifiedAt
|
|
124
|
+
response.nextStep = 'SSL certificate should be ready within 1-24 hours'
|
|
125
|
+
break
|
|
126
|
+
|
|
127
|
+
case 'ssl_provisioning':
|
|
128
|
+
response.message = 'SSL certificate being provisioned'
|
|
129
|
+
response.verifiedAt = domainEntry.verifiedAt
|
|
130
|
+
response.nextStep = 'Almost ready! Check back in a few minutes'
|
|
131
|
+
break
|
|
132
|
+
|
|
133
|
+
case 'active':
|
|
134
|
+
response.message = 'Domain is active and working'
|
|
135
|
+
response.customUrl = `https://${domainEntry.customDomain}`
|
|
136
|
+
response.verifiedAt = domainEntry.verifiedAt
|
|
137
|
+
break
|
|
138
|
+
|
|
139
|
+
case 'error':
|
|
140
|
+
response.message = 'Domain configuration error'
|
|
141
|
+
response.error = domainEntry.errorMessage
|
|
142
|
+
response.nextStep = 'Check DNS configuration and try verifying again'
|
|
143
|
+
break
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
response.lastCheckedAt = domainEntry.lastCheckedAt
|
|
147
|
+
|
|
148
|
+
return NextResponse.json(response)
|
|
149
|
+
|
|
150
|
+
} catch (error) {
|
|
151
|
+
console.error('[Domains API] Error getting status:', error)
|
|
152
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
153
|
+
return NextResponse.json(
|
|
154
|
+
{ error: 'Failed to get domain status', details: message },
|
|
155
|
+
{ status: 500 }
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server'
|
|
2
|
+
import {
|
|
3
|
+
validateApiKey,
|
|
4
|
+
getProjectCustomDomain,
|
|
5
|
+
getCustomDomainEntry,
|
|
6
|
+
updateCustomDomainStatus,
|
|
7
|
+
} from '@/lib/storage/blob'
|
|
8
|
+
import { normalizeDomain } from '@/lib/docs/config'
|
|
9
|
+
import dns from 'dns'
|
|
10
|
+
import { promisify } from 'util'
|
|
11
|
+
|
|
12
|
+
const resolveCname = promisify(dns.resolveCname)
|
|
13
|
+
const resolveTxt = promisify(dns.resolveTxt)
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* POST /api/domains/verify
|
|
17
|
+
*
|
|
18
|
+
* Verify DNS configuration for a custom domain.
|
|
19
|
+
* Checks CNAME and TXT records, then triggers SSL provisioning.
|
|
20
|
+
*
|
|
21
|
+
* Headers:
|
|
22
|
+
* Authorization: Bearer <api_key>
|
|
23
|
+
*
|
|
24
|
+
* Body:
|
|
25
|
+
* { customDomain: "docs.example.com" } // Optional, uses project's domain if not specified
|
|
26
|
+
*
|
|
27
|
+
* Response:
|
|
28
|
+
* {
|
|
29
|
+
* success: true,
|
|
30
|
+
* domain: "docs.example.com",
|
|
31
|
+
* status: "dns_verified",
|
|
32
|
+
* checks: {
|
|
33
|
+
* cname: { found: true, value: "cname.devdoc-dns.com" },
|
|
34
|
+
* txt: { found: true, verified: true }
|
|
35
|
+
* }
|
|
36
|
+
* }
|
|
37
|
+
*/
|
|
38
|
+
export async function POST(request: NextRequest) {
|
|
39
|
+
try {
|
|
40
|
+
// Validate API key
|
|
41
|
+
const authHeader = request.headers.get('Authorization')
|
|
42
|
+
const apiKey = authHeader?.replace('Bearer ', '')
|
|
43
|
+
|
|
44
|
+
if (!apiKey) {
|
|
45
|
+
return NextResponse.json(
|
|
46
|
+
{ error: 'API key required. Provide via Authorization header.' },
|
|
47
|
+
{ status: 401 }
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const projectSlug = await validateApiKey(apiKey)
|
|
52
|
+
if (!projectSlug) {
|
|
53
|
+
return NextResponse.json(
|
|
54
|
+
{ error: 'Invalid API key' },
|
|
55
|
+
{ status: 403 }
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Parse request body
|
|
60
|
+
const body = await request.json().catch(() => ({}))
|
|
61
|
+
let customDomain = body.customDomain ? normalizeDomain(body.customDomain) : null
|
|
62
|
+
|
|
63
|
+
// If no domain specified, get the project's custom domain
|
|
64
|
+
if (!customDomain) {
|
|
65
|
+
const projectDomain = await getProjectCustomDomain(projectSlug)
|
|
66
|
+
if (!projectDomain) {
|
|
67
|
+
return NextResponse.json(
|
|
68
|
+
{ error: 'No custom domain configured for this project. Add one first with /api/domains/add' },
|
|
69
|
+
{ status: 404 }
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
customDomain = projectDomain.customDomain
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Get domain entry
|
|
76
|
+
const domainEntry = await getCustomDomainEntry(customDomain)
|
|
77
|
+
if (!domainEntry) {
|
|
78
|
+
return NextResponse.json(
|
|
79
|
+
{ error: `Domain ${customDomain} not found` },
|
|
80
|
+
{ status: 404 }
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Verify domain belongs to this project
|
|
85
|
+
if (domainEntry.projectSlug !== projectSlug) {
|
|
86
|
+
return NextResponse.json(
|
|
87
|
+
{ error: `Domain ${customDomain} does not belong to this project` },
|
|
88
|
+
{ status: 403 }
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Check DNS records
|
|
93
|
+
const checks = {
|
|
94
|
+
cname: { found: false, value: null as string | null, expected: 'cname.devdoc-dns.com' },
|
|
95
|
+
txt: { found: false, verified: false, expected: domainEntry.verificationToken },
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Check CNAME record
|
|
99
|
+
try {
|
|
100
|
+
const cnameRecords = await resolveCname(customDomain)
|
|
101
|
+
if (cnameRecords && cnameRecords.length > 0) {
|
|
102
|
+
checks.cname.found = true
|
|
103
|
+
checks.cname.value = cnameRecords[0]
|
|
104
|
+
}
|
|
105
|
+
} catch {
|
|
106
|
+
// CNAME not found or DNS error
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Check TXT record for verification
|
|
110
|
+
const txtRecordName = `_devdoc-verify.${customDomain}`
|
|
111
|
+
try {
|
|
112
|
+
const txtRecords = await resolveTxt(txtRecordName)
|
|
113
|
+
if (txtRecords && txtRecords.length > 0) {
|
|
114
|
+
checks.txt.found = true
|
|
115
|
+
// TXT records can be arrays, flatten and check
|
|
116
|
+
const allTxtValues = txtRecords.flat()
|
|
117
|
+
checks.txt.verified = allTxtValues.some(
|
|
118
|
+
val => val === domainEntry.verificationToken
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
} catch {
|
|
122
|
+
// TXT not found or DNS error
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Determine overall status
|
|
126
|
+
const cnameValid = checks.cname.found &&
|
|
127
|
+
checks.cname.value?.toLowerCase().includes('devdoc')
|
|
128
|
+
const txtValid = checks.txt.found && checks.txt.verified
|
|
129
|
+
|
|
130
|
+
let newStatus = domainEntry.status
|
|
131
|
+
let message = ''
|
|
132
|
+
|
|
133
|
+
if (cnameValid && txtValid) {
|
|
134
|
+
// DNS is verified, update status
|
|
135
|
+
newStatus = 'dns_verified'
|
|
136
|
+
message = 'DNS verified! SSL certificate will be provisioned automatically (1-24 hours).'
|
|
137
|
+
|
|
138
|
+
await updateCustomDomainStatus(customDomain, 'dns_verified')
|
|
139
|
+
|
|
140
|
+
// In production, this would trigger Vercel API to add domain
|
|
141
|
+
// For now, we simulate SSL provisioning by updating status
|
|
142
|
+
// TODO: Integrate with Vercel Domains API
|
|
143
|
+
|
|
144
|
+
} else if (cnameValid && !txtValid) {
|
|
145
|
+
message = 'CNAME record found, but TXT verification record is missing or incorrect.'
|
|
146
|
+
} else if (!cnameValid && txtValid) {
|
|
147
|
+
message = 'TXT verification found, but CNAME record is missing or incorrect.'
|
|
148
|
+
} else {
|
|
149
|
+
message = 'DNS records not found. Please add the required CNAME and TXT records.'
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return NextResponse.json({
|
|
153
|
+
success: cnameValid && txtValid,
|
|
154
|
+
domain: customDomain,
|
|
155
|
+
projectSlug,
|
|
156
|
+
status: newStatus,
|
|
157
|
+
message,
|
|
158
|
+
checks: {
|
|
159
|
+
cname: {
|
|
160
|
+
found: checks.cname.found,
|
|
161
|
+
value: checks.cname.value,
|
|
162
|
+
valid: cnameValid,
|
|
163
|
+
expected: checks.cname.expected,
|
|
164
|
+
},
|
|
165
|
+
txt: {
|
|
166
|
+
found: checks.txt.found,
|
|
167
|
+
verified: checks.txt.verified,
|
|
168
|
+
expected: checks.txt.expected,
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
} catch (error) {
|
|
174
|
+
console.error('[Domains API] Error verifying domain:', error)
|
|
175
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
176
|
+
return NextResponse.json(
|
|
177
|
+
{ error: 'Failed to verify domain', details: message },
|
|
178
|
+
{ status: 500 }
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
}
|