@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.
Files changed (46) hide show
  1. package/dist/cli/commands/deploy.js +83 -10
  2. package/dist/cli/commands/domain.js +134 -59
  3. package/package.json +1 -1
  4. package/renderer/app/[...slug]/client.js +17 -0
  5. package/renderer/app/[...slug]/page.js +125 -0
  6. package/renderer/app/api/assets/[...path]/route.js +23 -4
  7. package/renderer/app/api/chat/route.js +188 -25
  8. package/renderer/app/api/collections/route.js +105 -3
  9. package/renderer/app/api/deploy/route.js +7 -3
  10. package/renderer/app/api/domains/add/route.js +118 -40
  11. package/renderer/app/api/domains/remove/route.js +16 -3
  12. package/renderer/app/api/domains/verify/route.js +82 -17
  13. package/renderer/app/api/schema/route.js +12 -11
  14. package/renderer/app/api/suggestions/route.js +98 -10
  15. package/renderer/app/globals.css +33 -0
  16. package/renderer/app/layout.js +83 -8
  17. package/renderer/components/docs/mdx/cards.js +16 -45
  18. package/renderer/components/docs/mdx/file-tree.js +102 -0
  19. package/renderer/components/docs/mdx/index.js +7 -0
  20. package/renderer/components/docs-header.js +11 -11
  21. package/renderer/components/docs-viewer/agent/agent-chat.js +75 -11
  22. package/renderer/components/docs-viewer/agent/messages/assistant-message.js +67 -3
  23. package/renderer/components/docs-viewer/agent/messages/tool-call-display.js +49 -4
  24. package/renderer/components/docs-viewer/content/content-router.js +1 -1
  25. package/renderer/components/docs-viewer/content/doc-page.js +36 -28
  26. package/renderer/components/docs-viewer/index.js +223 -58
  27. package/renderer/components/docs-viewer/playground/graphql-playground.js +131 -33
  28. package/renderer/components/docs-viewer/shared/method-badge.js +11 -2
  29. package/renderer/components/docs-viewer/sidebar/collection-tree.js +44 -6
  30. package/renderer/components/docs-viewer/sidebar/index.js +2 -1
  31. package/renderer/components/docs-viewer/sidebar/right-sidebar.js +3 -1
  32. package/renderer/components/docs-viewer/sidebar/sidebar-item.js +5 -7
  33. package/renderer/hooks/use-route-state.js +44 -56
  34. package/renderer/lib/api-docs/agent/indexer.js +73 -12
  35. package/renderer/lib/api-docs/agent/use-suggestions.js +26 -16
  36. package/renderer/lib/api-docs/code-editor/mode-context.js +16 -18
  37. package/renderer/lib/api-docs/parsers/openapi/transformer.js +8 -1
  38. package/renderer/lib/cache/purge.js +98 -0
  39. package/renderer/lib/docs/config/domain-schema.js +23 -1
  40. package/renderer/lib/docs/config/index.js +1 -1
  41. package/renderer/lib/docs-link-utils.js +146 -0
  42. package/renderer/lib/docs-navigation-context.js +3 -2
  43. package/renderer/lib/docs-navigation.js +50 -41
  44. package/renderer/lib/rate-limit.js +203 -0
  45. package/renderer/lib/storage/blob.js +4 -2
  46. 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, getDnsInstructions } from '@/lib/docs/config';
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
- * cname: { name: "docs", value: "cname.devdoc-dns.com" },
23
- * txt: { name: "_devdoc-verify.docs.example.com", value: "devdoc-verify=xxx" }
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
- // Add the custom domain
76
- const result = await addCustomDomain(projectSlug, customDomain);
77
- if (!result.success) {
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
- error: result.error
80
- }, {
81
- status: 400
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 the domain
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
- * Checks CNAME and TXT records, then triggers SSL provisioning.
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: "dns_verified",
25
- * checks: {
26
- * cname: { found: true, value: "cname.devdoc-dns.com" },
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
- // Check DNS records
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.devdoc-dns.com'
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('devdoc');
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, update status
125
- newStatus = 'dns_verified';
126
- message = 'DNS verified! SSL certificate will be provisioned automatically (1-24 hours).';
127
- await updateCustomDomainStatus(customDomain, 'dns_verified');
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
- // Try blob storage first (for deployed projects)
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
- if (projectContent?.files) {
51
- const file = projectContent.files.find((f)=>f.path === path || f.path === `/${path}`);
52
- if (file?.content) {
53
- return new NextResponse(file.content, {
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
- content = endpoint ? `endpoint:${endpoint.id}:${endpoint.name}:${endpoint.method}:${endpoint.path}` : `endpoint:${currentEndpointId}`;
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)=>`${e.method}:${e.path}`).join('|');
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
- const methodCounts = endpoints.reduce((acc, e)=>{
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
- return `You are helping users explore an API documentation. Generate 4 helpful starter question suggestions.
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 endpoints: ${endpoints.length}
67
- - Methods: ${Object.entries(methodCounts).map(([m, c])=>`${m}: ${c}`).join(', ')}
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 endpoints:
71
- ${endpoints.slice(0, 8).map((e)=>`- ${e.method} ${e.name}: ${e.description || 'No description'}`).join('\n')}
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 endpoint for their use case
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
@@ -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
  }