@brainfish-ai/devdoc 0.1.48 → 0.1.50
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 -10
- package/dist/cli/commands/domain.js +134 -59
- package/package.json +1 -1
- package/renderer/app/[...slug]/client.js +17 -0
- package/renderer/app/[...slug]/page.js +125 -0
- package/renderer/app/api/assets/[...path]/route.js +23 -4
- package/renderer/app/api/chat/route.js +188 -25
- package/renderer/app/api/collections/route.js +105 -3
- package/renderer/app/api/deploy/route.js +7 -3
- package/renderer/app/api/domains/add/route.js +118 -40
- package/renderer/app/api/domains/remove/route.js +16 -3
- package/renderer/app/api/domains/verify/route.js +82 -17
- package/renderer/app/api/schema/route.js +12 -11
- package/renderer/app/api/suggestions/route.js +98 -10
- package/renderer/app/globals.css +33 -0
- package/renderer/app/layout.js +83 -8
- package/renderer/components/docs/mdx/cards.js +16 -45
- package/renderer/components/docs/mdx/file-tree.js +102 -0
- package/renderer/components/docs/mdx/index.js +7 -0
- package/renderer/components/docs-header.js +11 -11
- package/renderer/components/docs-viewer/agent/agent-chat.js +75 -11
- package/renderer/components/docs-viewer/agent/messages/assistant-message.js +67 -3
- package/renderer/components/docs-viewer/agent/messages/tool-call-display.js +49 -4
- package/renderer/components/docs-viewer/content/content-router.js +1 -1
- package/renderer/components/docs-viewer/content/doc-page.js +36 -28
- package/renderer/components/docs-viewer/index.js +223 -58
- package/renderer/components/docs-viewer/playground/graphql-playground.js +131 -33
- package/renderer/components/docs-viewer/shared/method-badge.js +11 -2
- package/renderer/components/docs-viewer/sidebar/collection-tree.js +44 -6
- package/renderer/components/docs-viewer/sidebar/index.js +2 -1
- package/renderer/components/docs-viewer/sidebar/right-sidebar.js +3 -1
- package/renderer/components/docs-viewer/sidebar/sidebar-item.js +5 -7
- package/renderer/hooks/use-route-state.js +44 -56
- package/renderer/lib/api-docs/agent/indexer.js +73 -12
- package/renderer/lib/api-docs/agent/use-suggestions.js +26 -16
- package/renderer/lib/api-docs/code-editor/mode-context.js +16 -18
- package/renderer/lib/api-docs/parsers/openapi/transformer.js +8 -1
- package/renderer/lib/cache/purge.js +98 -0
- package/renderer/lib/docs/config/domain-schema.js +23 -1
- package/renderer/lib/docs/config/index.js +1 -1
- package/renderer/lib/docs-link-utils.js +146 -0
- package/renderer/lib/docs-navigation-context.js +3 -2
- package/renderer/lib/docs-navigation.js +50 -41
- package/renderer/lib/rate-limit.js +203 -0
- package/renderer/lib/storage/blob.js +4 -2
- package/renderer/lib/vercel/domains.js +275 -0
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server';
|
|
2
|
-
import { validateApiKey, addCustomDomain, isCustomDomainRegistered } from '@/lib/storage/blob';
|
|
3
|
-
import { isValidDomain, normalizeDomain
|
|
2
|
+
import { validateApiKey, addCustomDomain, isCustomDomainRegistered, updateCustomDomainStatus } from '@/lib/storage/blob';
|
|
3
|
+
import { isValidDomain, normalizeDomain } from '@/lib/docs/config';
|
|
4
|
+
import { addDomainToProject, isVercelIntegrationEnabled, formatVerificationInstructions, getDomainType } from '@/lib/vercel/domains';
|
|
4
5
|
/**
|
|
5
6
|
* POST /api/domains/add
|
|
6
7
|
*
|
|
7
8
|
* Add a custom domain to a project.
|
|
8
9
|
* Each project can have ONE custom domain (free).
|
|
9
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
|
|
15
|
+
*
|
|
10
16
|
* Headers:
|
|
11
17
|
* Authorization: Bearer <api_key>
|
|
12
18
|
*
|
|
@@ -18,10 +24,9 @@ import { isValidDomain, normalizeDomain, getDnsInstructions } from '@/lib/docs/c
|
|
|
18
24
|
* success: true,
|
|
19
25
|
* domain: "docs.example.com",
|
|
20
26
|
* status: "pending",
|
|
21
|
-
* verification:
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* }
|
|
27
|
+
* verification: [
|
|
28
|
+
* { type: "TXT", domain: "_vercel.docs.example.com", value: "vc-domain-verify=..." }
|
|
29
|
+
* ]
|
|
25
30
|
* }
|
|
26
31
|
*/ export async function POST(request) {
|
|
27
32
|
try {
|
|
@@ -63,7 +68,7 @@ import { isValidDomain, normalizeDomain, getDnsInstructions } from '@/lib/docs/c
|
|
|
63
68
|
status: 400
|
|
64
69
|
});
|
|
65
70
|
}
|
|
66
|
-
// Check if domain is already registered
|
|
71
|
+
// Check if domain is already registered in our registry
|
|
67
72
|
const isRegistered = await isCustomDomainRegistered(customDomain);
|
|
68
73
|
if (isRegistered) {
|
|
69
74
|
return NextResponse.json({
|
|
@@ -72,42 +77,115 @@ import { isValidDomain, normalizeDomain, getDnsInstructions } from '@/lib/docs/c
|
|
|
72
77
|
status: 409
|
|
73
78
|
});
|
|
74
79
|
}
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
|
|
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
|
|
107
|
+
});
|
|
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`;
|
|
78
127
|
return NextResponse.json({
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
|
139
|
+
});
|
|
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] : '@';
|
|
154
|
+
return NextResponse.json({
|
|
155
|
+
success: true,
|
|
156
|
+
domain: customDomain,
|
|
157
|
+
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
|
+
]
|
|
82
187
|
});
|
|
83
188
|
}
|
|
84
|
-
// Get DNS instructions
|
|
85
|
-
const dnsInstructions = getDnsInstructions(customDomain);
|
|
86
|
-
// Add verification token to TXT record
|
|
87
|
-
dnsInstructions.txt.value = result.entry.verificationToken || '';
|
|
88
|
-
return NextResponse.json({
|
|
89
|
-
success: true,
|
|
90
|
-
domain: customDomain,
|
|
91
|
-
projectSlug,
|
|
92
|
-
status: result.entry.status,
|
|
93
|
-
verification: {
|
|
94
|
-
cname: dnsInstructions.cname,
|
|
95
|
-
txt: dnsInstructions.txt
|
|
96
|
-
},
|
|
97
|
-
instructions: [
|
|
98
|
-
'Add the following DNS records to your domain:',
|
|
99
|
-
'',
|
|
100
|
-
`1. CNAME Record:`,
|
|
101
|
-
` Name: ${dnsInstructions.cname.name}`,
|
|
102
|
-
` Value: ${dnsInstructions.cname.value}`,
|
|
103
|
-
'',
|
|
104
|
-
`2. TXT Record (for verification):`,
|
|
105
|
-
` Name: ${dnsInstructions.txt.name}`,
|
|
106
|
-
` Value: ${dnsInstructions.txt.value}`,
|
|
107
|
-
'',
|
|
108
|
-
'After adding DNS records, run "devdoc domain verify" to verify.'
|
|
109
|
-
]
|
|
110
|
-
});
|
|
111
189
|
} catch (error) {
|
|
112
190
|
console.error('[Domains API] Error adding domain:', error);
|
|
113
191
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server';
|
|
2
2
|
import { validateApiKey, getProjectCustomDomain, removeCustomDomain } from '@/lib/storage/blob';
|
|
3
3
|
import { normalizeDomain } from '@/lib/docs/config';
|
|
4
|
+
import { removeDomain as vercelRemoveDomain, isVercelIntegrationEnabled } from '@/lib/vercel/domains';
|
|
4
5
|
/**
|
|
5
6
|
* DELETE /api/domains/remove
|
|
6
7
|
*
|
|
7
8
|
* Remove a custom domain from a project.
|
|
8
9
|
*
|
|
10
|
+
* When Vercel integration is enabled:
|
|
11
|
+
* - Removes domain from Vercel project
|
|
12
|
+
* - Removes from internal registry
|
|
13
|
+
*
|
|
9
14
|
* Headers:
|
|
10
15
|
* Authorization: Bearer <api_key>
|
|
11
16
|
*
|
|
@@ -52,7 +57,17 @@ import { normalizeDomain } from '@/lib/docs/config';
|
|
|
52
57
|
}
|
|
53
58
|
customDomain = projectDomain.customDomain;
|
|
54
59
|
}
|
|
55
|
-
// Remove
|
|
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
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// Remove from internal registry
|
|
56
71
|
const result = await removeCustomDomain(customDomain, projectSlug);
|
|
57
72
|
if (!result.success) {
|
|
58
73
|
return NextResponse.json({
|
|
@@ -61,8 +76,6 @@ import { normalizeDomain } from '@/lib/docs/config';
|
|
|
61
76
|
status: 400
|
|
62
77
|
});
|
|
63
78
|
}
|
|
64
|
-
// TODO: In production, also remove from Vercel via API
|
|
65
|
-
// await vercelApi.removeDomain(customDomain)
|
|
66
79
|
return NextResponse.json({
|
|
67
80
|
success: true,
|
|
68
81
|
message: `Domain ${customDomain} removed successfully`,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
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
|
+
import { verifyDomain as vercelVerifyDomain, getDomainConfig, isVercelIntegrationEnabled, formatVerificationInstructions } from '@/lib/vercel/domains';
|
|
4
5
|
import dns from 'dns';
|
|
5
6
|
import { promisify } from 'util';
|
|
6
7
|
const resolveCname = promisify(dns.resolveCname);
|
|
@@ -9,7 +10,13 @@ const resolveTxt = promisify(dns.resolveTxt);
|
|
|
9
10
|
* POST /api/domains/verify
|
|
10
11
|
*
|
|
11
12
|
* Verify DNS configuration for a custom domain.
|
|
12
|
-
*
|
|
13
|
+
*
|
|
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
|
|
13
20
|
*
|
|
14
21
|
* Headers:
|
|
15
22
|
* Authorization: Bearer <api_key>
|
|
@@ -21,11 +28,9 @@ const resolveTxt = promisify(dns.resolveTxt);
|
|
|
21
28
|
* {
|
|
22
29
|
* success: true,
|
|
23
30
|
* domain: "docs.example.com",
|
|
24
|
-
* status: "
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* txt: { found: true, verified: true }
|
|
28
|
-
* }
|
|
31
|
+
* status: "active",
|
|
32
|
+
* verified: true,
|
|
33
|
+
* message: "Domain verified! SSL will be provisioned automatically."
|
|
29
34
|
* }
|
|
30
35
|
*/ export async function POST(request) {
|
|
31
36
|
try {
|
|
@@ -62,7 +67,7 @@ const resolveTxt = promisify(dns.resolveTxt);
|
|
|
62
67
|
}
|
|
63
68
|
customDomain = projectDomain.customDomain;
|
|
64
69
|
}
|
|
65
|
-
// Get domain entry
|
|
70
|
+
// Get domain entry from our registry
|
|
66
71
|
const domainEntry = await getCustomDomainEntry(customDomain);
|
|
67
72
|
if (!domainEntry) {
|
|
68
73
|
return NextResponse.json({
|
|
@@ -79,12 +84,74 @@ const resolveTxt = promisify(dns.resolveTxt);
|
|
|
79
84
|
status: 403
|
|
80
85
|
});
|
|
81
86
|
}
|
|
82
|
-
//
|
|
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
|
|
83
150
|
const checks = {
|
|
84
151
|
cname: {
|
|
85
152
|
found: false,
|
|
86
153
|
value: null,
|
|
87
|
-
expected: 'cname.
|
|
154
|
+
expected: 'cname.vercel-dns.com'
|
|
88
155
|
},
|
|
89
156
|
txt: {
|
|
90
157
|
found: false,
|
|
@@ -116,18 +183,15 @@ const resolveTxt = promisify(dns.resolveTxt);
|
|
|
116
183
|
// TXT not found or DNS error
|
|
117
184
|
}
|
|
118
185
|
// Determine overall status
|
|
119
|
-
const cnameValid = checks.cname.found && checks.cname.value?.toLowerCase().includes('
|
|
186
|
+
const cnameValid = checks.cname.found && checks.cname.value?.toLowerCase().includes('vercel');
|
|
120
187
|
const txtValid = checks.txt.found && checks.txt.verified;
|
|
121
188
|
let newStatus = domainEntry.status;
|
|
122
189
|
let message = '';
|
|
123
190
|
if (cnameValid && txtValid) {
|
|
124
|
-
// DNS is verified
|
|
125
|
-
newStatus = '
|
|
126
|
-
message = 'DNS verified!
|
|
127
|
-
await updateCustomDomainStatus(customDomain, '
|
|
128
|
-
// In production, this would trigger Vercel API to add domain
|
|
129
|
-
// For now, we simulate SSL provisioning by updating status
|
|
130
|
-
// TODO: Integrate with Vercel Domains API
|
|
191
|
+
// DNS is verified
|
|
192
|
+
newStatus = 'active';
|
|
193
|
+
message = 'DNS verified! Domain is now active.';
|
|
194
|
+
await updateCustomDomainStatus(customDomain, 'active');
|
|
131
195
|
} else if (cnameValid && !txtValid) {
|
|
132
196
|
message = 'CNAME record found, but TXT verification record is missing or incorrect.';
|
|
133
197
|
} else if (!cnameValid && txtValid) {
|
|
@@ -140,6 +204,7 @@ const resolveTxt = promisify(dns.resolveTxt);
|
|
|
140
204
|
domain: customDomain,
|
|
141
205
|
projectSlug,
|
|
142
206
|
status: newStatus,
|
|
207
|
+
verified: cnameValid && txtValid,
|
|
143
208
|
message,
|
|
144
209
|
checks: {
|
|
145
210
|
cname: {
|
|
@@ -6,10 +6,6 @@ import { getProjectContent } from '@/lib/storage/blob';
|
|
|
6
6
|
const STARTER_PATH = process.env.STARTER_PATH || 'devdoc-docs';
|
|
7
7
|
// Get the docs directory - respects STARTER_PATH for local development
|
|
8
8
|
function getDocsDir() {
|
|
9
|
-
const projectSlug = process.env.BRAINFISH_PROJECT_SLUG;
|
|
10
|
-
if (projectSlug) {
|
|
11
|
-
return join(process.cwd(), '.devdoc', projectSlug);
|
|
12
|
-
}
|
|
13
9
|
// Use STARTER_PATH (can be absolute or relative)
|
|
14
10
|
if (isAbsolute(STARTER_PATH)) {
|
|
15
11
|
return STARTER_PATH;
|
|
@@ -43,22 +39,27 @@ export async function GET(request) {
|
|
|
43
39
|
});
|
|
44
40
|
}
|
|
45
41
|
try {
|
|
46
|
-
//
|
|
47
|
-
const projectSlug = process.env.BRAINFISH_PROJECT_SLUG;
|
|
42
|
+
// Get project slug from middleware header (multi-tenant) or env var (single-tenant)
|
|
43
|
+
const projectSlug = request.headers.get('x-devdoc-project') || process.env.BRAINFISH_PROJECT_SLUG;
|
|
44
|
+
// Try blob storage first (for deployed/multi-tenant projects)
|
|
48
45
|
if (projectSlug) {
|
|
49
46
|
const projectContent = await getProjectContent(projectSlug);
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
47
|
+
// Check dedicated graphqlSchemas map first (like OpenAPI specs)
|
|
48
|
+
if (projectContent?.graphqlSchemas) {
|
|
49
|
+
const schemaContent = projectContent.graphqlSchemas[path] || projectContent.graphqlSchemas[path.replace(/^\//, '')] // Try without leading slash
|
|
50
|
+
;
|
|
51
|
+
if (schemaContent) {
|
|
52
|
+
return new NextResponse(schemaContent, {
|
|
54
53
|
headers: {
|
|
55
54
|
'Content-Type': 'text/plain'
|
|
56
55
|
}
|
|
57
56
|
});
|
|
58
57
|
}
|
|
58
|
+
// Log for debugging - schema not found
|
|
59
|
+
console.log('[Schema API] Schema not found in graphqlSchemas:', path, 'Available:', Object.keys(projectContent.graphqlSchemas));
|
|
59
60
|
}
|
|
60
61
|
}
|
|
61
|
-
// Try local filesystem
|
|
62
|
+
// Try local filesystem (for local development)
|
|
62
63
|
const docsDir = getDocsDir();
|
|
63
64
|
const fullPath = join(docsDir, path);
|
|
64
65
|
if (!existsSync(fullPath)) {
|
|
@@ -3,10 +3,22 @@ import { anthropic } from '@ai-sdk/anthropic';
|
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import crypto from 'crypto';
|
|
5
5
|
import { CacheUtils } from '@/lib/cache';
|
|
6
|
+
import { RateLimiter } from '@/lib/rate-limit';
|
|
6
7
|
export const runtime = 'nodejs';
|
|
7
8
|
// Cache TTL: 1 week in seconds
|
|
8
9
|
const CACHE_TTL_SECONDS = 60 * 60 * 24 * 7 // 7 days
|
|
9
10
|
;
|
|
11
|
+
// Rate limit configuration (can be overridden via env vars)
|
|
12
|
+
// Set SUGGESTIONS_RATE_LIMIT_ENABLED=false to disable rate limiting
|
|
13
|
+
const RATE_LIMIT_ENABLED = process.env.SUGGESTIONS_RATE_LIMIT_ENABLED !== 'false';
|
|
14
|
+
const RATE_LIMIT_MAX = parseInt(process.env.SUGGESTIONS_RATE_LIMIT_MAX || '30', 10);
|
|
15
|
+
const RATE_LIMIT_WINDOW = parseInt(process.env.SUGGESTIONS_RATE_LIMIT_WINDOW || '60', 10);
|
|
16
|
+
// Create rate limiter with configurable values
|
|
17
|
+
const suggestionsRateLimiter = new RateLimiter({
|
|
18
|
+
prefix: 'suggestions',
|
|
19
|
+
limit: RATE_LIMIT_MAX,
|
|
20
|
+
windowSeconds: RATE_LIMIT_WINDOW
|
|
21
|
+
});
|
|
10
22
|
const SuggestionsSchema = z.object({
|
|
11
23
|
suggestions: z.array(z.object({
|
|
12
24
|
title: z.string().describe('Short action phrase (2-4 words) like "Find endpoints" or "How do I"'),
|
|
@@ -22,10 +34,20 @@ const SuggestionsSchema = z.object({
|
|
|
22
34
|
if (currentEndpointId) {
|
|
23
35
|
// For specific endpoint: hash the endpoint details
|
|
24
36
|
const endpoint = endpointIndex.find((e)=>e.id === currentEndpointId);
|
|
25
|
-
|
|
37
|
+
if (endpoint) {
|
|
38
|
+
// Include GraphQL operation type in the cache key for better differentiation
|
|
39
|
+
const opType = endpoint.operationType || endpoint.method;
|
|
40
|
+
content = `endpoint:${endpoint.id}:${endpoint.name}:${opType}:${endpoint.path}`;
|
|
41
|
+
} else {
|
|
42
|
+
content = `endpoint:${currentEndpointId}`;
|
|
43
|
+
}
|
|
26
44
|
} else {
|
|
27
45
|
// For general API: hash based on endpoint count and first few endpoint signatures
|
|
28
|
-
const signature = endpointIndex.slice(0, 10).map((e)
|
|
46
|
+
const signature = endpointIndex.slice(0, 10).map((e)=>{
|
|
47
|
+
// Use operation type for GraphQL, method for REST
|
|
48
|
+
const opType = e.operationType || e.method;
|
|
49
|
+
return `${opType}:${e.name}`;
|
|
50
|
+
}).join('|');
|
|
29
51
|
content = `general:${endpointIndex.length}:${signature}`;
|
|
30
52
|
}
|
|
31
53
|
const hash = crypto.createHash('sha256').update(content).digest('hex').slice(0, 16);
|
|
@@ -34,6 +56,30 @@ const SuggestionsSchema = z.object({
|
|
|
34
56
|
function buildSuggestionPrompt(endpoints, currentEndpointId) {
|
|
35
57
|
const currentEndpoint = currentEndpointId ? endpoints.find((e)=>e.id === currentEndpointId) : null;
|
|
36
58
|
if (currentEndpoint) {
|
|
59
|
+
// Check if this is a GraphQL operation
|
|
60
|
+
const isGraphQL = currentEndpoint.type === 'graphql' || [
|
|
61
|
+
'query',
|
|
62
|
+
'mutation',
|
|
63
|
+
'subscription'
|
|
64
|
+
].includes(currentEndpoint.operationType || '');
|
|
65
|
+
if (isGraphQL) {
|
|
66
|
+
const opType = currentEndpoint.operationType || 'operation';
|
|
67
|
+
return `You are helping users explore a GraphQL API. Generate 4 helpful question suggestions for the "${currentEndpoint.name}" ${opType}.
|
|
68
|
+
|
|
69
|
+
GraphQL Operation details:
|
|
70
|
+
- Name: ${currentEndpoint.name}
|
|
71
|
+
- Type: ${opType}
|
|
72
|
+
- Description: ${currentEndpoint.description || 'No description'}
|
|
73
|
+
- Variables: ${currentEndpoint.parameters.join(', ') || 'None'}
|
|
74
|
+
|
|
75
|
+
Generate questions that help users:
|
|
76
|
+
1. Understand what this ${opType} does and when to use it
|
|
77
|
+
2. Know what variables are required and their types
|
|
78
|
+
3. See example GraphQL queries and responses
|
|
79
|
+
4. Understand how to handle errors or edge cases
|
|
80
|
+
|
|
81
|
+
Make questions specific to this GraphQL ${opType}, not generic.`;
|
|
82
|
+
}
|
|
37
83
|
return `You are helping users explore an API. Generate 4 helpful question suggestions for the "${currentEndpoint.name}" endpoint.
|
|
38
84
|
|
|
39
85
|
Endpoint details:
|
|
@@ -53,33 +99,75 @@ Generate questions that help users:
|
|
|
53
99
|
Make questions specific to this endpoint, not generic.`;
|
|
54
100
|
}
|
|
55
101
|
// General suggestions based on the API
|
|
56
|
-
|
|
102
|
+
// Separate GraphQL and REST endpoints
|
|
103
|
+
const graphqlEndpoints = endpoints.filter((e)=>e.type === 'graphql' || [
|
|
104
|
+
'query',
|
|
105
|
+
'mutation',
|
|
106
|
+
'subscription'
|
|
107
|
+
].includes(e.operationType || ''));
|
|
108
|
+
const restEndpoints = endpoints.filter((e)=>e.type === 'rest' && ![
|
|
109
|
+
'query',
|
|
110
|
+
'mutation',
|
|
111
|
+
'subscription'
|
|
112
|
+
].includes(e.operationType || ''));
|
|
113
|
+
const methodCounts = restEndpoints.reduce((acc, e)=>{
|
|
57
114
|
acc[e.method] = (acc[e.method] || 0) + 1;
|
|
58
115
|
return acc;
|
|
59
116
|
}, {});
|
|
117
|
+
const graphqlCounts = graphqlEndpoints.reduce((acc, e)=>{
|
|
118
|
+
const opType = e.operationType || 'operation';
|
|
119
|
+
acc[opType] = (acc[opType] || 0) + 1;
|
|
120
|
+
return acc;
|
|
121
|
+
}, {});
|
|
60
122
|
const categories = [
|
|
61
123
|
...new Set(endpoints.flatMap((e)=>e.tags))
|
|
62
124
|
].slice(0, 5);
|
|
63
|
-
|
|
125
|
+
// Build API type description
|
|
126
|
+
const apiTypes = [];
|
|
127
|
+
if (restEndpoints.length > 0) apiTypes.push('REST');
|
|
128
|
+
if (graphqlEndpoints.length > 0) apiTypes.push('GraphQL');
|
|
129
|
+
return `You are helping users explore ${apiTypes.join(' and ')} API documentation. Generate 4 helpful starter question suggestions.
|
|
64
130
|
|
|
65
131
|
API Overview:
|
|
66
|
-
- Total
|
|
67
|
-
|
|
132
|
+
- Total operations: ${endpoints.length}
|
|
133
|
+
${restEndpoints.length > 0 ? `- REST Methods: ${Object.entries(methodCounts).map(([m, c])=>`${m}: ${c}`).join(', ')}` : ''}
|
|
134
|
+
${graphqlEndpoints.length > 0 ? `- GraphQL Operations: ${Object.entries(graphqlCounts).map(([m, c])=>`${m}: ${c}`).join(', ')}` : ''}
|
|
68
135
|
- Categories: ${categories.join(', ') || 'General'}
|
|
69
136
|
|
|
70
|
-
Sample
|
|
71
|
-
${endpoints.slice(0, 8).map((e)
|
|
137
|
+
Sample operations:
|
|
138
|
+
${endpoints.slice(0, 8).map((e)=>{
|
|
139
|
+
if (e.type === 'graphql' || [
|
|
140
|
+
'query',
|
|
141
|
+
'mutation',
|
|
142
|
+
'subscription'
|
|
143
|
+
].includes(e.operationType || '')) {
|
|
144
|
+
return `- [${e.operationType?.toUpperCase() || 'GRAPHQL'}] ${e.name}: ${e.description || 'No description'}`;
|
|
145
|
+
}
|
|
146
|
+
return `- [${e.method}] ${e.name}: ${e.description || 'No description'}`;
|
|
147
|
+
}).join('\n')}
|
|
72
148
|
|
|
73
149
|
Generate questions that help users:
|
|
74
|
-
1. Find the right
|
|
150
|
+
1. Find the right operation for their use case
|
|
75
151
|
2. Understand authentication/authorization
|
|
76
|
-
3. Explore common operations (create, read, update, delete)
|
|
152
|
+
3. Explore common operations (${graphqlEndpoints.length > 0 ? 'queries, mutations' : 'create, read, update, delete'})
|
|
77
153
|
4. Get started quickly with the API
|
|
78
154
|
|
|
79
155
|
Make questions specific to this API's capabilities.`;
|
|
80
156
|
}
|
|
81
157
|
export async function POST(req) {
|
|
82
158
|
try {
|
|
159
|
+
// Rate limiting check (configurable via env vars)
|
|
160
|
+
if (RATE_LIMIT_ENABLED) {
|
|
161
|
+
const rateLimitResult = await suggestionsRateLimiter.check(req);
|
|
162
|
+
if (!rateLimitResult.success) {
|
|
163
|
+
console.log(`[Suggestions API] Rate limit exceeded for IP, count: ${rateLimitResult.count}/${rateLimitResult.limit}`);
|
|
164
|
+
return RateLimiter.tooManyRequestsResponse(rateLimitResult, 'Too many suggestion requests. Please wait before trying again.');
|
|
165
|
+
}
|
|
166
|
+
// Log rate limit status for monitoring (only on cache miss to reduce noise)
|
|
167
|
+
if (rateLimitResult.remaining < 5) {
|
|
168
|
+
console.log(`[Suggestions API] Rate limit warning: ${rateLimitResult.remaining} requests remaining`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
83
171
|
const body = await req.json();
|
|
84
172
|
const { endpointIndex, currentEndpointId } = body;
|
|
85
173
|
// Generate hash-based cache key
|
package/renderer/app/globals.css
CHANGED
|
@@ -1266,4 +1266,37 @@ code[data-line-numbers] > [data-line]::before {
|
|
|
1266
1266
|
.agent-suggestion-item {
|
|
1267
1267
|
animation: suggestion-fade-in 0.2s ease-out forwards;
|
|
1268
1268
|
animation-fill-mode: both;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
/* Sandbox tabs styling */
|
|
1272
|
+
.sandbox-tabs {
|
|
1273
|
+
background-color: hsl(240 10% 20%);
|
|
1274
|
+
border-bottom: 1px solid hsl(240 10% 25%);
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
.sandbox-tab-active {
|
|
1278
|
+
background-color: hsl(240 10% 14%);
|
|
1279
|
+
color: hsl(0 0% 95%);
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
.sandbox-tab-inactive {
|
|
1283
|
+
color: hsl(240 5% 65%);
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
.sandbox-tab-inactive:hover {
|
|
1287
|
+
color: hsl(0 0% 90%);
|
|
1288
|
+
background-color: hsl(240 10% 18%);
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
.sandbox-tab-indicator {
|
|
1292
|
+
background-color: hsl(var(--primary));
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
.sandbox-tab-close {
|
|
1296
|
+
color: hsl(240 5% 55%);
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
.sandbox-tab-close:hover {
|
|
1300
|
+
color: hsl(0 0% 95%);
|
|
1301
|
+
background-color: hsl(240 10% 25%);
|
|
1269
1302
|
}
|