@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.
- package/dist/cli/commands/deploy.js +72 -4
- package/dist/cli/commands/domain.js +51 -58
- package/package.json +1 -1
- package/renderer/app/api/collections/route.js +16 -7
- package/renderer/app/api/deploy/route.js +3 -3
- package/renderer/app/api/domains/add/route.js +73 -31
- package/renderer/app/api/domains/remove/route.js +25 -4
- package/renderer/app/api/domains/status/route.js +17 -19
- package/renderer/app/api/domains/verify/route.js +72 -83
- package/renderer/app/api/schema/route.js +12 -11
- package/renderer/components/docs-header.js +11 -11
- package/renderer/lib/docs/config/domain-schema.js +0 -16
- package/renderer/lib/docs/config/index.js +1 -1
- package/renderer/lib/storage/blob.js +4 -2
- package/renderer/lib/vercel/domains.js +275 -0
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server';
|
|
2
2
|
import { validateApiKey, getProjectCustomDomain, getCustomDomainEntry } from '@/lib/storage/blob';
|
|
3
|
-
import { normalizeDomain
|
|
3
|
+
import { normalizeDomain } from '@/lib/docs/config';
|
|
4
4
|
import { getProjectUrl } from '@/lib/multi-tenant/context';
|
|
5
|
+
import { getDomainConfig, isVercelIntegrationEnabled, formatVerificationInstructions } from '@/lib/vercel/domains';
|
|
5
6
|
/**
|
|
6
7
|
* GET /api/domains/status
|
|
7
8
|
*
|
|
8
|
-
* Get the status of a custom domain.
|
|
9
|
+
* Get the status of a custom domain via Vercel API.
|
|
10
|
+
*
|
|
11
|
+
* Requires environment variables:
|
|
12
|
+
* - VERCEL_API_TOKEN (or VERCEL_TOKEN)
|
|
13
|
+
* - VERCEL_PROJECT_ID
|
|
14
|
+
* - VERCEL_TEAM_ID (optional)
|
|
9
15
|
*
|
|
10
16
|
* Headers:
|
|
11
17
|
* Authorization: Bearer <api_key>
|
|
@@ -94,26 +100,18 @@ import { getProjectUrl } from '@/lib/multi-tenant/context';
|
|
|
94
100
|
switch(domainEntry.status){
|
|
95
101
|
case 'pending':
|
|
96
102
|
{
|
|
97
|
-
const dnsInstructions = getDnsInstructions(domainEntry.customDomain);
|
|
98
|
-
dnsInstructions.txt.value = domainEntry.verificationToken || '';
|
|
99
103
|
response.message = 'Waiting for DNS configuration';
|
|
100
|
-
response.
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
104
|
+
response.nextStep = 'Add the DNS records below, then run "devdoc domain verify"';
|
|
105
|
+
// Get verification instructions from Vercel if available
|
|
106
|
+
if (isVercelIntegrationEnabled()) {
|
|
107
|
+
const configResult = await getDomainConfig(domainEntry.customDomain);
|
|
108
|
+
if (configResult.success && configResult.domain?.verification) {
|
|
109
|
+
const { records } = formatVerificationInstructions(configResult.domain.verification);
|
|
110
|
+
response.verification = records;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
105
113
|
break;
|
|
106
114
|
}
|
|
107
|
-
case 'dns_verified':
|
|
108
|
-
response.message = 'DNS verified, SSL certificate provisioning in progress';
|
|
109
|
-
response.verifiedAt = domainEntry.verifiedAt;
|
|
110
|
-
response.nextStep = 'SSL certificate should be ready within 1-24 hours';
|
|
111
|
-
break;
|
|
112
|
-
case 'ssl_provisioning':
|
|
113
|
-
response.message = 'SSL certificate being provisioned';
|
|
114
|
-
response.verifiedAt = domainEntry.verifiedAt;
|
|
115
|
-
response.nextStep = 'Almost ready! Check back in a few minutes';
|
|
116
|
-
break;
|
|
117
115
|
case 'active':
|
|
118
116
|
response.message = 'Domain is active and working';
|
|
119
117
|
response.customUrl = `https://${domainEntry.customDomain}`;
|
|
@@ -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
|
|
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
|
-
*
|
|
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: "
|
|
25
|
-
*
|
|
26
|
-
*
|
|
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
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
//
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
116
|
-
|
|
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
|
-
//
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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)) {
|
|
@@ -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
|
|
11
|
-
const
|
|
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
|
|
23
|
+
url: docsLogo?.url,
|
|
24
24
|
light: docsLogo?.light,
|
|
25
25
|
dark: docsLogo?.dark,
|
|
26
|
-
alt: docsLogo?.alt ||
|
|
27
|
-
width: docsLogo?.width ||
|
|
28
|
-
height: docsLogo?.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
|
|
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
|
|
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
|