@brainfish-ai/devdoc 0.1.49 → 0.1.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,11 +1,17 @@
1
1
  import { NextResponse } from 'next/server';
2
2
  import { validateApiKey, getProjectCustomDomain, getCustomDomainEntry } from '@/lib/storage/blob';
3
- import { normalizeDomain, getDnsInstructions } from '@/lib/docs/config';
3
+ import { normalizeDomain } from '@/lib/docs/config';
4
4
  import { getProjectUrl } from '@/lib/multi-tenant/context';
5
+ import { getDomainConfig, isVercelIntegrationEnabled, formatVerificationInstructions } from '@/lib/vercel/domains';
5
6
  /**
6
7
  * GET /api/domains/status
7
8
  *
8
- * Get the status of a custom domain.
9
+ * Get the status of a custom domain via Vercel API.
10
+ *
11
+ * Requires environment variables:
12
+ * - VERCEL_API_TOKEN (or VERCEL_TOKEN)
13
+ * - VERCEL_PROJECT_ID
14
+ * - VERCEL_TEAM_ID (optional)
9
15
  *
10
16
  * Headers:
11
17
  * Authorization: Bearer <api_key>
@@ -94,26 +100,18 @@ import { getProjectUrl } from '@/lib/multi-tenant/context';
94
100
  switch(domainEntry.status){
95
101
  case 'pending':
96
102
  {
97
- const dnsInstructions = getDnsInstructions(domainEntry.customDomain);
98
- dnsInstructions.txt.value = domainEntry.verificationToken || '';
99
103
  response.message = 'Waiting for DNS configuration';
100
- response.dnsRecords = {
101
- cname: dnsInstructions.cname,
102
- txt: dnsInstructions.txt
103
- };
104
- response.nextStep = 'Add the DNS records above, then run "devdoc domain verify"';
104
+ response.nextStep = 'Add the DNS records below, then run "devdoc domain verify"';
105
+ // Get verification instructions from Vercel if available
106
+ if (isVercelIntegrationEnabled()) {
107
+ const configResult = await getDomainConfig(domainEntry.customDomain);
108
+ if (configResult.success && configResult.domain?.verification) {
109
+ const { records } = formatVerificationInstructions(configResult.domain.verification);
110
+ response.verification = records;
111
+ }
112
+ }
105
113
  break;
106
114
  }
107
- case 'dns_verified':
108
- response.message = 'DNS verified, SSL certificate provisioning in progress';
109
- response.verifiedAt = domainEntry.verifiedAt;
110
- response.nextStep = 'SSL certificate should be ready within 1-24 hours';
111
- break;
112
- case 'ssl_provisioning':
113
- response.message = 'SSL certificate being provisioned';
114
- response.verifiedAt = domainEntry.verifiedAt;
115
- response.nextStep = 'Almost ready! Check back in a few minutes';
116
- break;
117
115
  case 'active':
118
116
  response.message = 'Domain is active and working';
119
117
  response.customUrl = `https://${domainEntry.customDomain}`;
@@ -1,15 +1,17 @@
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 dns from 'dns';
5
- import { promisify } from 'util';
6
- const resolveCname = promisify(dns.resolveCname);
7
- const resolveTxt = promisify(dns.resolveTxt);
4
+ import { verifyDomain as vercelVerifyDomain, getDomainConfig, isVercelIntegrationEnabled, formatVerificationInstructions } from '@/lib/vercel/domains';
8
5
  /**
9
6
  * POST /api/domains/verify
10
7
  *
11
- * Verify DNS configuration for a custom domain.
12
- * Checks CNAME and TXT records, then triggers SSL provisioning.
8
+ * Verify DNS configuration for a custom domain via Vercel API.
9
+ * Vercel handles DNS verification and automatic SSL provisioning.
10
+ *
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>
@@ -21,11 +23,9 @@ const resolveTxt = promisify(dns.resolveTxt);
21
23
  * {
22
24
  * success: true,
23
25
  * 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
- * }
26
+ * status: "active",
27
+ * verified: true,
28
+ * message: "Domain verified! SSL will be provisioned automatically."
29
29
  * }
30
30
  */ export async function POST(request) {
31
31
  try {
@@ -62,7 +62,7 @@ const resolveTxt = promisify(dns.resolveTxt);
62
62
  }
63
63
  customDomain = projectDomain.customDomain;
64
64
  }
65
- // Get domain entry
65
+ // Get domain entry from our registry
66
66
  const domainEntry = await getCustomDomainEntry(customDomain);
67
67
  if (!domainEntry) {
68
68
  return NextResponse.json({
@@ -79,82 +79,71 @@ const resolveTxt = promisify(dns.resolveTxt);
79
79
  status: 403
80
80
  });
81
81
  }
82
- // Check DNS records
83
- const checks = {
84
- cname: {
85
- found: false,
86
- value: null,
87
- expected: 'cname.devdoc-dns.com'
88
- },
89
- txt: {
90
- found: false,
91
- verified: false,
92
- expected: domainEntry.verificationToken
93
- }
94
- };
95
- // Check CNAME record
96
- try {
97
- const cnameRecords = await resolveCname(customDomain);
98
- if (cnameRecords && cnameRecords.length > 0) {
99
- checks.cname.found = true;
100
- checks.cname.value = cnameRecords[0];
101
- }
102
- } catch {
103
- // 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
+ });
104
89
  }
105
- // Check TXT record for verification
106
- const txtRecordName = `_devdoc-verify.${customDomain}`;
107
- try {
108
- const txtRecords = await resolveTxt(txtRecordName);
109
- if (txtRecords && txtRecords.length > 0) {
110
- checks.txt.found = true;
111
- // TXT records can be arrays, flatten and check
112
- const allTxtValues = txtRecords.flat();
113
- checks.txt.verified = allTxtValues.some((val)=>val === domainEntry.verificationToken);
90
+ // Call Vercel's verify endpoint
91
+ const verifyResult = await vercelVerifyDomain(customDomain);
92
+ if (!verifyResult.success) {
93
+ // Get current domain config to show what's needed
94
+ const configResult = await getDomainConfig(customDomain);
95
+ const verification = configResult.domain?.verification || [];
96
+ let instructions = [];
97
+ if (verification.length > 0) {
98
+ const formatted = formatVerificationInstructions(verification);
99
+ instructions = formatted.instructions;
114
100
  }
115
- } catch {
116
- // TXT not found or DNS error
101
+ return NextResponse.json({
102
+ success: false,
103
+ domain: customDomain,
104
+ projectSlug,
105
+ status: domainEntry.status,
106
+ verified: false,
107
+ message: verifyResult.error || 'DNS verification failed. Please check your DNS records.',
108
+ verification: verification.map((v)=>({
109
+ type: v.type,
110
+ name: v.domain,
111
+ value: v.value
112
+ })),
113
+ instructions
114
+ });
117
115
  }
118
- // Determine overall status
119
- const cnameValid = checks.cname.found && checks.cname.value?.toLowerCase().includes('devdoc');
120
- const txtValid = checks.txt.found && checks.txt.verified;
121
- let newStatus = domainEntry.status;
122
- let message = '';
123
- 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
131
- } else if (cnameValid && !txtValid) {
132
- message = 'CNAME record found, but TXT verification record is missing or incorrect.';
133
- } else if (!cnameValid && txtValid) {
134
- message = 'TXT verification found, but CNAME record is missing or incorrect.';
116
+ // Domain verified successfully
117
+ if (verifyResult.verified) {
118
+ // Update our registry to mark as active
119
+ await updateCustomDomainStatus(customDomain, 'active');
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
+ });
135
128
  } else {
136
- message = 'DNS records not found. Please add the required CNAME and TXT records.';
129
+ // Vercel didn't verify yet, return what's needed
130
+ const verification = verifyResult.domain?.verification || [];
131
+ const { instructions } = formatVerificationInstructions(verification);
132
+ return NextResponse.json({
133
+ success: false,
134
+ domain: customDomain,
135
+ projectSlug,
136
+ status: 'pending',
137
+ verified: false,
138
+ message: 'DNS records not yet propagated. This can take up to 48 hours.',
139
+ verification: verification.map((v)=>({
140
+ type: v.type,
141
+ name: v.domain,
142
+ value: v.value
143
+ })),
144
+ instructions
145
+ });
137
146
  }
138
- return NextResponse.json({
139
- success: cnameValid && txtValid,
140
- domain: customDomain,
141
- projectSlug,
142
- status: newStatus,
143
- message,
144
- checks: {
145
- cname: {
146
- found: checks.cname.found,
147
- value: checks.cname.value,
148
- valid: cnameValid,
149
- expected: checks.cname.expected
150
- },
151
- txt: {
152
- found: checks.txt.found,
153
- verified: checks.txt.verified,
154
- expected: checks.txt.expected
155
- }
156
- }
157
- });
158
147
  } catch (error) {
159
148
  console.error('[Domains API] Error verifying domain:', error);
160
149
  const message = error instanceof Error ? error.message : String(error);
@@ -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)) {
@@ -7,9 +7,8 @@ import { ThemeToggle } from "@/components/theme-toggle";
7
7
  import { useMobile } from "@/lib/api-docs/mobile-context";
8
8
  import { Button } from "@/components/ui/button";
9
9
  import { cn } from "@/lib/utils";
10
- // Default logo fallback
11
- const DEFAULT_LOGO = {
12
- url: 'https://cdn.prod.website-files.com/639181964f4fe88697a20a0a/67af64a5ba42bb659b8560c9_Logo%20(3).svg',
10
+ // Default logo dimensions (no default URL - show name only if no logo configured)
11
+ const DEFAULT_LOGO_DIMENSIONS = {
13
12
  alt: 'Logo',
14
13
  width: 80,
15
14
  height: 24
@@ -19,13 +18,14 @@ export function DocsHeader({ navigationTabs = [], activeTab, onTabChange, docsNa
19
18
  // Use config values with defaults
20
19
  // Support both single URL and separate light/dark logos
21
20
  const hasLightDarkLogos = docsLogo?.light && docsLogo?.dark;
21
+ const hasAnyLogo = docsLogo?.url || hasLightDarkLogos;
22
22
  const logo = {
23
- url: docsLogo?.url || DEFAULT_LOGO.url,
23
+ url: docsLogo?.url,
24
24
  light: docsLogo?.light,
25
25
  dark: docsLogo?.dark,
26
- alt: docsLogo?.alt || DEFAULT_LOGO.alt,
27
- width: docsLogo?.width || DEFAULT_LOGO.width,
28
- height: docsLogo?.height || DEFAULT_LOGO.height
26
+ alt: docsLogo?.alt || DEFAULT_LOGO_DIMENSIONS.alt,
27
+ width: docsLogo?.width || DEFAULT_LOGO_DIMENSIONS.width,
28
+ height: docsLogo?.height || DEFAULT_LOGO_DIMENSIONS.height
29
29
  };
30
30
  const showSearch = docsHeader?.showSearch !== false // Default true
31
31
  ;
@@ -87,7 +87,7 @@ export function DocsHeader({ navigationTabs = [], activeTab, onTabChange, docsNa
87
87
  priority: true
88
88
  })
89
89
  ]
90
- }) : /*#__PURE__*/ _jsx(Image, {
90
+ }) : logo.url ? /*#__PURE__*/ _jsx(Image, {
91
91
  src: logo.url,
92
92
  alt: logo.alt,
93
93
  width: logo.width,
@@ -97,14 +97,14 @@ export function DocsHeader({ navigationTabs = [], activeTab, onTabChange, docsNa
97
97
  height: logo.height
98
98
  },
99
99
  priority: true
100
- }),
100
+ }) : null,
101
101
  docsName && /*#__PURE__*/ _jsxs(_Fragment, {
102
102
  children: [
103
- /*#__PURE__*/ _jsx("div", {
103
+ hasAnyLogo && /*#__PURE__*/ _jsx("div", {
104
104
  className: "hidden sm:block h-5 w-px bg-border"
105
105
  }),
106
106
  /*#__PURE__*/ _jsx("span", {
107
- className: "docs-header-title hidden sm:inline text-sm font-medium text-muted-foreground pointer-events-none",
107
+ className: cn("docs-header-title text-sm font-medium pointer-events-none", hasAnyLogo ? "hidden sm:inline text-muted-foreground" : "text-foreground font-semibold"),
108
108
  children: docsName
109
109
  })
110
110
  ]
@@ -143,19 +143,3 @@ export const domainConfigSchema = z.object({
143
143
  normalized = normalized.split(':')[0];
144
144
  return normalized;
145
145
  }
146
- /**
147
- * Get DNS instructions for a custom domain
148
- */ export function getDnsInstructions(customDomain) {
149
- const parts = customDomain.split('.');
150
- const subdomain = parts.length > 2 ? parts[0] : '@';
151
- return {
152
- cname: {
153
- name: subdomain === '@' ? customDomain : subdomain,
154
- value: 'cname.devdoc-dns.com'
155
- },
156
- txt: {
157
- name: `_devdoc-verify.${customDomain}`,
158
- value: ''
159
- }
160
- };
161
- }
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Configuration Module Exports
3
3
  */ export { docsConfigSchema, parseDocsConfig, safeParseDocsConfig, getDefaultDocsConfig } from './schema';
4
- export { domainConfigSchema, parseDomainConfig, safeParseDomainConfig, isValidDomain, normalizeDomain, getDnsInstructions } from './domain-schema';
4
+ export { domainConfigSchema, parseDomainConfig, safeParseDomainConfig, isValidDomain, normalizeDomain } from './domain-schema';
5
5
  export { loadDocsConfig, safeLoadDocsConfig, clearConfigCache, hasDocsConfig, getContentDir, resolvePagePath, loadPageContent, listMdxFiles } from './loader';
6
6
  export { isDevMode, isProductionMode, shouldShowItem } from './environment';
@@ -30,7 +30,7 @@ function _getFileBlobPath(slug, filePath) {
30
30
  }
31
31
  /**
32
32
  * Store project content in Vercel Blob (or local filesystem in dev)
33
- */ export async function storeProjectContent(slug, name, docsJson, files, themeJson, openApiSpecs) {
33
+ */ export async function storeProjectContent(slug, name, docsJson, files, themeJson, openApiSpecs, graphqlSchemas) {
34
34
  const now = new Date().toISOString();
35
35
  const content = {
36
36
  slug,
@@ -41,6 +41,7 @@ function _getFileBlobPath(slug, filePath) {
41
41
  k,
42
42
  JSON.stringify(v)
43
43
  ])) : undefined,
44
+ graphqlSchemas,
44
45
  files,
45
46
  createdAt: now,
46
47
  updatedAt: now
@@ -92,7 +93,7 @@ function _getFileBlobPath(slug, filePath) {
92
93
  }
93
94
  /**
94
95
  * Update existing project content
95
- */ export async function updateProjectContent(slug, docsJson, files, themeJson, openApiSpecs) {
96
+ */ export async function updateProjectContent(slug, docsJson, files, themeJson, openApiSpecs, graphqlSchemas) {
96
97
  // Get existing content to preserve createdAt
97
98
  const existing = await getProjectContent(slug);
98
99
  const now = new Date().toISOString();
@@ -105,6 +106,7 @@ function _getFileBlobPath(slug, filePath) {
105
106
  k,
106
107
  JSON.stringify(v)
107
108
  ])) : undefined,
109
+ graphqlSchemas,
108
110
  files,
109
111
  createdAt: existing?.createdAt || now,
110
112
  updatedAt: now