@brainfish-ai/devdoc 0.1.21
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/LICENSE +33 -0
- package/README.md +415 -0
- package/bin/devdoc.js +13 -0
- package/dist/cli/commands/build.d.ts +5 -0
- package/dist/cli/commands/build.js +87 -0
- package/dist/cli/commands/check.d.ts +1 -0
- package/dist/cli/commands/check.js +143 -0
- package/dist/cli/commands/create.d.ts +24 -0
- package/dist/cli/commands/create.js +387 -0
- package/dist/cli/commands/deploy.d.ts +9 -0
- package/dist/cli/commands/deploy.js +433 -0
- package/dist/cli/commands/dev.d.ts +6 -0
- package/dist/cli/commands/dev.js +139 -0
- package/dist/cli/commands/init.d.ts +11 -0
- package/dist/cli/commands/init.js +238 -0
- package/dist/cli/commands/keys.d.ts +12 -0
- package/dist/cli/commands/keys.js +165 -0
- package/dist/cli/commands/start.d.ts +5 -0
- package/dist/cli/commands/start.js +56 -0
- package/dist/cli/commands/upload.d.ts +13 -0
- package/dist/cli/commands/upload.js +238 -0
- package/dist/cli/commands/whoami.d.ts +8 -0
- package/dist/cli/commands/whoami.js +91 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +106 -0
- package/dist/config/index.d.ts +80 -0
- package/dist/config/index.js +133 -0
- package/dist/constants.d.ts +9 -0
- package/dist/constants.js +13 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +12 -0
- package/dist/utils/logger.d.ts +16 -0
- package/dist/utils/logger.js +61 -0
- package/dist/utils/paths.d.ts +16 -0
- package/dist/utils/paths.js +50 -0
- package/package.json +51 -0
- package/renderer/app/api/assets/[...path]/route.ts +123 -0
- package/renderer/app/api/assets/route.ts +124 -0
- package/renderer/app/api/assets/upload/route.ts +177 -0
- package/renderer/app/api/auth-schemes/route.ts +77 -0
- package/renderer/app/api/chat/route.ts +858 -0
- package/renderer/app/api/codegen/route.ts +72 -0
- package/renderer/app/api/collections/route.ts +1016 -0
- package/renderer/app/api/debug/route.ts +53 -0
- package/renderer/app/api/deploy/route.ts +234 -0
- package/renderer/app/api/device/route.ts +42 -0
- package/renderer/app/api/docs/route.ts +187 -0
- package/renderer/app/api/keys/regenerate/route.ts +80 -0
- package/renderer/app/api/openapi-spec/route.ts +151 -0
- package/renderer/app/api/projects/[slug]/route.ts +153 -0
- package/renderer/app/api/projects/[slug]/stats/route.ts +96 -0
- package/renderer/app/api/projects/register/route.ts +152 -0
- package/renderer/app/api/proxy/route.ts +149 -0
- package/renderer/app/api/proxy-stream/route.ts +168 -0
- package/renderer/app/api/redirects/route.ts +47 -0
- package/renderer/app/api/schema/route.ts +65 -0
- package/renderer/app/api/subdomains/check/route.ts +172 -0
- package/renderer/app/api/suggestions/route.ts +144 -0
- package/renderer/app/favicon.ico +0 -0
- package/renderer/app/globals.css +1103 -0
- package/renderer/app/layout.tsx +47 -0
- package/renderer/app/llms-full.txt/route.ts +346 -0
- package/renderer/app/llms.txt/route.ts +279 -0
- package/renderer/app/page.tsx +14 -0
- package/renderer/app/robots.txt/route.ts +84 -0
- package/renderer/app/sitemap.xml/route.ts +199 -0
- package/renderer/components/docs/index.ts +12 -0
- package/renderer/components/docs/mdx/accordion.tsx +169 -0
- package/renderer/components/docs/mdx/badge.tsx +132 -0
- package/renderer/components/docs/mdx/callouts.tsx +154 -0
- package/renderer/components/docs/mdx/cards.tsx +213 -0
- package/renderer/components/docs/mdx/changelog.tsx +120 -0
- package/renderer/components/docs/mdx/code-block.tsx +186 -0
- package/renderer/components/docs/mdx/code-group.tsx +421 -0
- package/renderer/components/docs/mdx/file-embeds.tsx +105 -0
- package/renderer/components/docs/mdx/frame.tsx +112 -0
- package/renderer/components/docs/mdx/highlight.tsx +151 -0
- package/renderer/components/docs/mdx/iframe.tsx +134 -0
- package/renderer/components/docs/mdx/image.tsx +235 -0
- package/renderer/components/docs/mdx/index.ts +204 -0
- package/renderer/components/docs/mdx/mermaid.tsx +240 -0
- package/renderer/components/docs/mdx/param-field.tsx +200 -0
- package/renderer/components/docs/mdx/steps.tsx +113 -0
- package/renderer/components/docs/mdx/tabs.tsx +86 -0
- package/renderer/components/docs/mdx-renderer.tsx +100 -0
- package/renderer/components/docs/navigation/breadcrumbs.tsx +76 -0
- package/renderer/components/docs/navigation/index.ts +8 -0
- package/renderer/components/docs/navigation/page-nav.tsx +64 -0
- package/renderer/components/docs/navigation/sidebar.tsx +515 -0
- package/renderer/components/docs/navigation/toc.tsx +113 -0
- package/renderer/components/docs/notice.tsx +105 -0
- package/renderer/components/docs-header.tsx +274 -0
- package/renderer/components/docs-viewer/agent/agent-chat.tsx +2076 -0
- package/renderer/components/docs-viewer/agent/cards/debug-context-card.tsx +90 -0
- package/renderer/components/docs-viewer/agent/cards/endpoint-context-card.tsx +49 -0
- package/renderer/components/docs-viewer/agent/cards/index.tsx +50 -0
- package/renderer/components/docs-viewer/agent/cards/response-options-card.tsx +212 -0
- package/renderer/components/docs-viewer/agent/cards/types.ts +84 -0
- package/renderer/components/docs-viewer/agent/chat-message.tsx +17 -0
- package/renderer/components/docs-viewer/agent/index.tsx +6 -0
- package/renderer/components/docs-viewer/agent/messages/assistant-message.tsx +119 -0
- package/renderer/components/docs-viewer/agent/messages/chat-message.tsx +46 -0
- package/renderer/components/docs-viewer/agent/messages/index.ts +17 -0
- package/renderer/components/docs-viewer/agent/messages/tool-call-display.tsx +721 -0
- package/renderer/components/docs-viewer/agent/messages/types.ts +61 -0
- package/renderer/components/docs-viewer/agent/messages/typing-indicator.tsx +24 -0
- package/renderer/components/docs-viewer/agent/messages/user-message.tsx +51 -0
- package/renderer/components/docs-viewer/code-editor/index.tsx +2 -0
- package/renderer/components/docs-viewer/code-editor/notes-mode.tsx +1283 -0
- package/renderer/components/docs-viewer/content/changelog-page.tsx +331 -0
- package/renderer/components/docs-viewer/content/doc-page.tsx +285 -0
- package/renderer/components/docs-viewer/content/documentation-viewer.tsx +17 -0
- package/renderer/components/docs-viewer/content/index.tsx +29 -0
- package/renderer/components/docs-viewer/content/introduction.tsx +21 -0
- package/renderer/components/docs-viewer/content/request-details.tsx +330 -0
- package/renderer/components/docs-viewer/content/sections/auth.tsx +69 -0
- package/renderer/components/docs-viewer/content/sections/body.tsx +66 -0
- package/renderer/components/docs-viewer/content/sections/headers.tsx +43 -0
- package/renderer/components/docs-viewer/content/sections/overview.tsx +40 -0
- package/renderer/components/docs-viewer/content/sections/parameters.tsx +43 -0
- package/renderer/components/docs-viewer/content/sections/responses.tsx +87 -0
- package/renderer/components/docs-viewer/global-auth-modal.tsx +352 -0
- package/renderer/components/docs-viewer/index.tsx +1466 -0
- package/renderer/components/docs-viewer/playground/auth-editor.tsx +280 -0
- package/renderer/components/docs-viewer/playground/body-editor.tsx +221 -0
- package/renderer/components/docs-viewer/playground/code-editor.tsx +224 -0
- package/renderer/components/docs-viewer/playground/code-snippet.tsx +387 -0
- package/renderer/components/docs-viewer/playground/graphql-playground.tsx +745 -0
- package/renderer/components/docs-viewer/playground/index.tsx +671 -0
- package/renderer/components/docs-viewer/playground/key-value-editor.tsx +261 -0
- package/renderer/components/docs-viewer/playground/method-selector.tsx +60 -0
- package/renderer/components/docs-viewer/playground/request-builder.tsx +179 -0
- package/renderer/components/docs-viewer/playground/request-tabs.tsx +237 -0
- package/renderer/components/docs-viewer/playground/response-cards/idle-card.tsx +21 -0
- package/renderer/components/docs-viewer/playground/response-cards/index.tsx +93 -0
- package/renderer/components/docs-viewer/playground/response-cards/loading-card.tsx +16 -0
- package/renderer/components/docs-viewer/playground/response-cards/network-error-card.tsx +23 -0
- package/renderer/components/docs-viewer/playground/response-cards/response-body-card.tsx +268 -0
- package/renderer/components/docs-viewer/playground/response-cards/types.ts +82 -0
- package/renderer/components/docs-viewer/playground/response-viewer.tsx +43 -0
- package/renderer/components/docs-viewer/search/index.ts +2 -0
- package/renderer/components/docs-viewer/search/search-dialog.tsx +331 -0
- package/renderer/components/docs-viewer/search/use-search.ts +117 -0
- package/renderer/components/docs-viewer/shared/markdown-renderer.tsx +431 -0
- package/renderer/components/docs-viewer/shared/method-badge.tsx +41 -0
- package/renderer/components/docs-viewer/shared/schema-viewer.tsx +349 -0
- package/renderer/components/docs-viewer/sidebar/collection-tree.tsx +239 -0
- package/renderer/components/docs-viewer/sidebar/endpoint-options.tsx +316 -0
- package/renderer/components/docs-viewer/sidebar/index.tsx +343 -0
- package/renderer/components/docs-viewer/sidebar/right-sidebar.tsx +202 -0
- package/renderer/components/docs-viewer/sidebar/sidebar-group.tsx +118 -0
- package/renderer/components/docs-viewer/sidebar/sidebar-item.tsx +226 -0
- package/renderer/components/docs-viewer/sidebar/sidebar-section.tsx +52 -0
- package/renderer/components/theme-provider.tsx +11 -0
- package/renderer/components/theme-toggle.tsx +76 -0
- package/renderer/components/ui/badge.tsx +46 -0
- package/renderer/components/ui/button.tsx +59 -0
- package/renderer/components/ui/dialog.tsx +118 -0
- package/renderer/components/ui/dropdown-menu.tsx +257 -0
- package/renderer/components/ui/input.tsx +21 -0
- package/renderer/components/ui/label.tsx +24 -0
- package/renderer/components/ui/navigation-menu.tsx +168 -0
- package/renderer/components/ui/select.tsx +190 -0
- package/renderer/components/ui/spinner.tsx +114 -0
- package/renderer/components/ui/tabs.tsx +66 -0
- package/renderer/components/ui/tooltip.tsx +61 -0
- package/renderer/hooks/use-code-copy.ts +88 -0
- package/renderer/hooks/use-openapi-title.ts +44 -0
- package/renderer/lib/api-docs/agent/index.ts +6 -0
- package/renderer/lib/api-docs/agent/indexer.ts +323 -0
- package/renderer/lib/api-docs/agent/spec-summary.ts +335 -0
- package/renderer/lib/api-docs/agent/types.ts +116 -0
- package/renderer/lib/api-docs/auth/auth-context.tsx +225 -0
- package/renderer/lib/api-docs/auth/auth-storage.ts +87 -0
- package/renderer/lib/api-docs/auth/crypto.ts +89 -0
- package/renderer/lib/api-docs/auth/index.ts +4 -0
- package/renderer/lib/api-docs/code-editor/db.ts +164 -0
- package/renderer/lib/api-docs/code-editor/hooks.ts +266 -0
- package/renderer/lib/api-docs/code-editor/index.ts +6 -0
- package/renderer/lib/api-docs/code-editor/mode-context.tsx +207 -0
- package/renderer/lib/api-docs/code-editor/types.ts +105 -0
- package/renderer/lib/api-docs/codegen/definitions.ts +297 -0
- package/renderer/lib/api-docs/codegen/har.ts +251 -0
- package/renderer/lib/api-docs/codegen/index.ts +159 -0
- package/renderer/lib/api-docs/factories.ts +151 -0
- package/renderer/lib/api-docs/index.ts +17 -0
- package/renderer/lib/api-docs/mobile-context.tsx +112 -0
- package/renderer/lib/api-docs/navigation-context.tsx +88 -0
- package/renderer/lib/api-docs/parsers/graphql/README.md +129 -0
- package/renderer/lib/api-docs/parsers/graphql/index.ts +91 -0
- package/renderer/lib/api-docs/parsers/graphql/parser.ts +491 -0
- package/renderer/lib/api-docs/parsers/graphql/transformer.ts +246 -0
- package/renderer/lib/api-docs/parsers/graphql/types.ts +283 -0
- package/renderer/lib/api-docs/parsers/openapi/README.md +32 -0
- package/renderer/lib/api-docs/parsers/openapi/dereferencer.ts +60 -0
- package/renderer/lib/api-docs/parsers/openapi/extractors/auth.ts +574 -0
- package/renderer/lib/api-docs/parsers/openapi/extractors/body.ts +403 -0
- package/renderer/lib/api-docs/parsers/openapi/extractors/index.ts +232 -0
- package/renderer/lib/api-docs/parsers/openapi/index.ts +171 -0
- package/renderer/lib/api-docs/parsers/openapi/transformer.ts +277 -0
- package/renderer/lib/api-docs/parsers/openapi/validator.ts +31 -0
- package/renderer/lib/api-docs/playground/context.tsx +107 -0
- package/renderer/lib/api-docs/playground/navigation-context.tsx +124 -0
- package/renderer/lib/api-docs/playground/request-builder.ts +223 -0
- package/renderer/lib/api-docs/playground/request-runner.ts +282 -0
- package/renderer/lib/api-docs/playground/types.ts +35 -0
- package/renderer/lib/api-docs/types.ts +269 -0
- package/renderer/lib/api-docs/utils.ts +311 -0
- package/renderer/lib/cache.ts +193 -0
- package/renderer/lib/docs/config/index.ts +29 -0
- package/renderer/lib/docs/config/loader.ts +142 -0
- package/renderer/lib/docs/config/schema.ts +298 -0
- package/renderer/lib/docs/index.ts +12 -0
- package/renderer/lib/docs/mdx/compiler.ts +176 -0
- package/renderer/lib/docs/mdx/frontmatter.ts +80 -0
- package/renderer/lib/docs/mdx/index.ts +26 -0
- package/renderer/lib/docs/navigation/generator.ts +348 -0
- package/renderer/lib/docs/navigation/index.ts +12 -0
- package/renderer/lib/docs/navigation/types.ts +123 -0
- package/renderer/lib/docs-navigation-context.tsx +80 -0
- package/renderer/lib/multi-tenant/context.ts +105 -0
- package/renderer/lib/storage/blob.ts +845 -0
- package/renderer/lib/utils.ts +6 -0
- package/renderer/next.config.ts +76 -0
- package/renderer/package.json +66 -0
- package/renderer/postcss.config.mjs +5 -0
- package/renderer/public/assets/images/screenshot.png +0 -0
- package/renderer/public/assets/logo/dark.svg +9 -0
- package/renderer/public/assets/logo/light.svg +9 -0
- package/renderer/public/assets/logo.svg +9 -0
- package/renderer/public/file.svg +1 -0
- package/renderer/public/globe.svg +1 -0
- package/renderer/public/icon.png +0 -0
- package/renderer/public/logo.svg +9 -0
- package/renderer/public/window.svg +1 -0
- package/renderer/tsconfig.json +28 -0
- package/templates/basic/README.md +139 -0
- package/templates/basic/assets/favicon.svg +4 -0
- package/templates/basic/assets/logo.svg +9 -0
- package/templates/basic/docs.json +47 -0
- package/templates/basic/guides/configuration.mdx +149 -0
- package/templates/basic/guides/overview.mdx +96 -0
- package/templates/basic/index.mdx +39 -0
- package/templates/basic/package.json +14 -0
- package/templates/basic/quickstart.mdx +92 -0
- package/templates/basic/vercel.json +6 -0
- package/templates/graphql/README.md +139 -0
- package/templates/graphql/api-reference/schema.graphql +305 -0
- package/templates/graphql/assets/favicon.svg +4 -0
- package/templates/graphql/assets/logo.svg +9 -0
- package/templates/graphql/docs.json +54 -0
- package/templates/graphql/guides/configuration.mdx +149 -0
- package/templates/graphql/guides/overview.mdx +96 -0
- package/templates/graphql/index.mdx +39 -0
- package/templates/graphql/package.json +14 -0
- package/templates/graphql/quickstart.mdx +92 -0
- package/templates/graphql/vercel.json +6 -0
- package/templates/openapi/README.md +139 -0
- package/templates/openapi/api-reference/openapi.json +419 -0
- package/templates/openapi/assets/favicon.svg +4 -0
- package/templates/openapi/assets/logo.svg +9 -0
- package/templates/openapi/docs.json +61 -0
- package/templates/openapi/guides/configuration.mdx +149 -0
- package/templates/openapi/guides/overview.mdx +96 -0
- package/templates/openapi/index.mdx +39 -0
- package/templates/openapi/package.json +14 -0
- package/templates/openapi/quickstart.mdx +92 -0
- package/templates/openapi/vercel.json +6 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server'
|
|
2
|
+
import { headers } from 'next/headers'
|
|
3
|
+
import { getProjectContent, projectExists } from '@/lib/storage/blob'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Debug endpoint to check multi-tenant routing
|
|
7
|
+
*
|
|
8
|
+
* GET /api/debug?slug=acme-api-docs-i2rjwv
|
|
9
|
+
*/
|
|
10
|
+
export async function GET(request: NextRequest) {
|
|
11
|
+
const headersList = await headers()
|
|
12
|
+
const { searchParams } = new URL(request.url)
|
|
13
|
+
const testSlug = searchParams.get('slug')
|
|
14
|
+
|
|
15
|
+
// Collect all headers
|
|
16
|
+
const allHeaders: Record<string, string> = {}
|
|
17
|
+
headersList.forEach((value, key) => {
|
|
18
|
+
allHeaders[key] = value
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
// Check for project header
|
|
22
|
+
const projectSlug = headersList.get('x-devdoc-project')
|
|
23
|
+
|
|
24
|
+
// Test Blob storage if slug provided
|
|
25
|
+
let blobTest = null
|
|
26
|
+
if (testSlug) {
|
|
27
|
+
try {
|
|
28
|
+
const exists = await projectExists(testSlug)
|
|
29
|
+
const content = exists ? await getProjectContent(testSlug) : null
|
|
30
|
+
blobTest = {
|
|
31
|
+
slug: testSlug,
|
|
32
|
+
exists,
|
|
33
|
+
hasContent: !!content,
|
|
34
|
+
filesCount: content?.files?.length || 0,
|
|
35
|
+
projectName: content?.name || null,
|
|
36
|
+
}
|
|
37
|
+
} catch (error) {
|
|
38
|
+
blobTest = {
|
|
39
|
+
slug: testSlug,
|
|
40
|
+
error: String(error),
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return NextResponse.json({
|
|
46
|
+
timestamp: new Date().toISOString(),
|
|
47
|
+
host: headersList.get('host'),
|
|
48
|
+
projectSlug,
|
|
49
|
+
hasBlobToken: !!process.env.BLOB_READ_WRITE_TOKEN,
|
|
50
|
+
blobTest,
|
|
51
|
+
headers: allHeaders,
|
|
52
|
+
})
|
|
53
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server'
|
|
2
|
+
import {
|
|
3
|
+
storeProjectContent,
|
|
4
|
+
updateProjectContent,
|
|
5
|
+
projectExists,
|
|
6
|
+
generateProjectSlug,
|
|
7
|
+
generateApiKey,
|
|
8
|
+
storeProjectApiKey,
|
|
9
|
+
validateApiKey,
|
|
10
|
+
getProjectApiKey,
|
|
11
|
+
type ProjectFile
|
|
12
|
+
} from '@/lib/storage/blob'
|
|
13
|
+
import { getProjectUrl, isValidSlug } from '@/lib/multi-tenant/context'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Deploy API - receives content from CLI and stores in Vercel Blob
|
|
17
|
+
*
|
|
18
|
+
* POST /api/deploy
|
|
19
|
+
* Headers:
|
|
20
|
+
* Authorization: Bearer <api_key> // Required for updates
|
|
21
|
+
* Body: {
|
|
22
|
+
* name: string, // Project name from docs.json
|
|
23
|
+
* slug?: string, // Optional: existing slug for updates
|
|
24
|
+
* docsJson: object, // The docs.json configuration
|
|
25
|
+
* files: Array<{ // MDX and other content files
|
|
26
|
+
* path: string,
|
|
27
|
+
* content: string
|
|
28
|
+
* }>
|
|
29
|
+
* }
|
|
30
|
+
*/
|
|
31
|
+
export async function POST(request: NextRequest) {
|
|
32
|
+
try {
|
|
33
|
+
const body = await request.json()
|
|
34
|
+
|
|
35
|
+
// Validate request body
|
|
36
|
+
const { name, slug: existingSlug, docsJson, files } = body
|
|
37
|
+
|
|
38
|
+
if (!name || typeof name !== 'string') {
|
|
39
|
+
return NextResponse.json(
|
|
40
|
+
{ error: 'Missing or invalid project name' },
|
|
41
|
+
{ status: 400 }
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!docsJson || typeof docsJson !== 'object') {
|
|
46
|
+
return NextResponse.json(
|
|
47
|
+
{ error: 'Missing or invalid docs.json configuration' },
|
|
48
|
+
{ status: 400 }
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!files || !Array.isArray(files)) {
|
|
53
|
+
return NextResponse.json(
|
|
54
|
+
{ error: 'Missing or invalid files array' },
|
|
55
|
+
{ status: 400 }
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Validate files structure
|
|
60
|
+
const validFiles: ProjectFile[] = []
|
|
61
|
+
for (const file of files) {
|
|
62
|
+
if (!file.path || typeof file.path !== 'string') {
|
|
63
|
+
return NextResponse.json(
|
|
64
|
+
{ error: `Invalid file entry: missing path` },
|
|
65
|
+
{ status: 400 }
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
if (typeof file.content !== 'string') {
|
|
69
|
+
return NextResponse.json(
|
|
70
|
+
{ error: `Invalid file entry for ${file.path}: content must be a string` },
|
|
71
|
+
{ status: 400 }
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
validFiles.push({
|
|
75
|
+
path: file.path,
|
|
76
|
+
content: file.content,
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let slug: string
|
|
81
|
+
let isUpdate = false
|
|
82
|
+
let apiKey: string | null = null
|
|
83
|
+
|
|
84
|
+
// Check if this is an update to an existing project
|
|
85
|
+
if (existingSlug) {
|
|
86
|
+
if (!isValidSlug(existingSlug)) {
|
|
87
|
+
return NextResponse.json(
|
|
88
|
+
{ error: 'Invalid project slug format' },
|
|
89
|
+
{ status: 400 }
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const exists = await projectExists(existingSlug)
|
|
94
|
+
if (!exists) {
|
|
95
|
+
return NextResponse.json(
|
|
96
|
+
{ error: `Project with slug "${existingSlug}" not found` },
|
|
97
|
+
{ status: 404 }
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// For updates, validate API key
|
|
102
|
+
const authHeader = request.headers.get('Authorization')
|
|
103
|
+
const providedKey = authHeader?.replace('Bearer ', '') || body.apiKey
|
|
104
|
+
|
|
105
|
+
if (!providedKey) {
|
|
106
|
+
return NextResponse.json(
|
|
107
|
+
{ error: 'API key required for project updates. Provide via Authorization header or apiKey field.' },
|
|
108
|
+
{ status: 401 }
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const validatedSlug = await validateApiKey(providedKey)
|
|
113
|
+
if (validatedSlug !== existingSlug) {
|
|
114
|
+
return NextResponse.json(
|
|
115
|
+
{ error: 'Invalid API key for this project' },
|
|
116
|
+
{ status: 403 }
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
slug = existingSlug
|
|
121
|
+
isUpdate = true
|
|
122
|
+
} else {
|
|
123
|
+
// Generate a new unique slug
|
|
124
|
+
slug = generateProjectSlug(name)
|
|
125
|
+
|
|
126
|
+
// Ensure it doesn't already exist (very unlikely but possible)
|
|
127
|
+
let attempts = 0
|
|
128
|
+
while (await projectExists(slug) && attempts < 5) {
|
|
129
|
+
slug = generateProjectSlug(name)
|
|
130
|
+
attempts++
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (attempts >= 5) {
|
|
134
|
+
return NextResponse.json(
|
|
135
|
+
{ error: 'Failed to generate unique project slug' },
|
|
136
|
+
{ status: 500 }
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Generate API key for new project
|
|
141
|
+
apiKey = generateApiKey()
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Store or update content
|
|
145
|
+
let result
|
|
146
|
+
if (isUpdate) {
|
|
147
|
+
result = await updateProjectContent(slug, docsJson, validFiles)
|
|
148
|
+
} else {
|
|
149
|
+
result = await storeProjectContent(slug, name, docsJson, validFiles)
|
|
150
|
+
// Store the API key for new projects
|
|
151
|
+
if (apiKey) {
|
|
152
|
+
await storeProjectApiKey(slug, apiKey)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Build response
|
|
157
|
+
const projectUrl = getProjectUrl(slug)
|
|
158
|
+
|
|
159
|
+
// For new projects, include the API key in response
|
|
160
|
+
const response: {
|
|
161
|
+
success: boolean
|
|
162
|
+
slug: string
|
|
163
|
+
url: string
|
|
164
|
+
blobUrl: string
|
|
165
|
+
isUpdate: boolean
|
|
166
|
+
filesCount: number
|
|
167
|
+
apiKey?: string
|
|
168
|
+
} = {
|
|
169
|
+
success: true,
|
|
170
|
+
slug,
|
|
171
|
+
url: projectUrl,
|
|
172
|
+
blobUrl: result.url,
|
|
173
|
+
isUpdate,
|
|
174
|
+
filesCount: validFiles.length,
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (apiKey) {
|
|
178
|
+
response.apiKey = apiKey
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return NextResponse.json(response)
|
|
182
|
+
|
|
183
|
+
} catch (error) {
|
|
184
|
+
console.error('[Deploy API] Error:', error)
|
|
185
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
186
|
+
|
|
187
|
+
// Provide clearer error messages for common issues
|
|
188
|
+
let userMessage = 'Deployment failed'
|
|
189
|
+
if (message.includes('blob already exists')) {
|
|
190
|
+
userMessage = 'Storage conflict - please try again'
|
|
191
|
+
} else if (message.includes('network') || message.includes('fetch')) {
|
|
192
|
+
userMessage = 'Network error connecting to storage'
|
|
193
|
+
} else if (message.includes('unauthorized') || message.includes('401')) {
|
|
194
|
+
userMessage = 'Authentication failed'
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return NextResponse.json(
|
|
198
|
+
{ error: userMessage, details: message },
|
|
199
|
+
{ status: 500 }
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* GET /api/deploy - Check deployment status or get project info
|
|
206
|
+
*/
|
|
207
|
+
export async function GET(request: NextRequest) {
|
|
208
|
+
const { searchParams } = new URL(request.url)
|
|
209
|
+
const slug = searchParams.get('slug')
|
|
210
|
+
|
|
211
|
+
if (!slug) {
|
|
212
|
+
return NextResponse.json(
|
|
213
|
+
{ error: 'Missing slug parameter' },
|
|
214
|
+
{ status: 400 }
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const exists = await projectExists(slug)
|
|
219
|
+
|
|
220
|
+
if (!exists) {
|
|
221
|
+
return NextResponse.json(
|
|
222
|
+
{ exists: false, slug },
|
|
223
|
+
{ status: 404 }
|
|
224
|
+
)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const projectUrl = getProjectUrl(slug)
|
|
228
|
+
|
|
229
|
+
return NextResponse.json({
|
|
230
|
+
exists: true,
|
|
231
|
+
slug,
|
|
232
|
+
url: projectUrl,
|
|
233
|
+
})
|
|
234
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { cookies } from 'next/headers'
|
|
3
|
+
|
|
4
|
+
const DEVICE_COOKIE_NAME = 'bf_device_id'
|
|
5
|
+
const COOKIE_MAX_AGE = 60 * 60 * 24 * 365 // 1 year
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Generate a unique device ID
|
|
9
|
+
* Uses crypto.randomUUID for secure random generation
|
|
10
|
+
*/
|
|
11
|
+
function generateDeviceId(): string {
|
|
12
|
+
return crypto.randomUUID()
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* GET /api/device
|
|
17
|
+
* Returns or creates a unique device ID for the client
|
|
18
|
+
* The device ID is stored in an httpOnly cookie for security
|
|
19
|
+
*/
|
|
20
|
+
export async function GET() {
|
|
21
|
+
const cookieStore = await cookies()
|
|
22
|
+
let deviceId = cookieStore.get(DEVICE_COOKIE_NAME)?.value
|
|
23
|
+
|
|
24
|
+
// Generate new device ID if not exists
|
|
25
|
+
if (!deviceId) {
|
|
26
|
+
deviceId = generateDeviceId()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Create response with device ID
|
|
30
|
+
const response = NextResponse.json({ deviceId })
|
|
31
|
+
|
|
32
|
+
// Set/refresh the httpOnly cookie
|
|
33
|
+
response.cookies.set(DEVICE_COOKIE_NAME, deviceId, {
|
|
34
|
+
httpOnly: true,
|
|
35
|
+
secure: process.env.NODE_ENV === 'production',
|
|
36
|
+
sameSite: 'strict',
|
|
37
|
+
maxAge: COOKIE_MAX_AGE,
|
|
38
|
+
path: '/',
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
return response
|
|
42
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server'
|
|
2
|
+
import { readFileSync, existsSync } from 'fs'
|
|
3
|
+
import { join, isAbsolute } from 'path'
|
|
4
|
+
import matter from 'gray-matter'
|
|
5
|
+
import { serialize } from 'next-mdx-remote/serialize'
|
|
6
|
+
import remarkGfm from 'remark-gfm'
|
|
7
|
+
import rehypeSlug from 'rehype-slug'
|
|
8
|
+
// Note: rehype-pretty-code is dynamically imported to handle serverless environments
|
|
9
|
+
// where shiki may not be fully available
|
|
10
|
+
import { getProjectFile } from '@/lib/storage/blob'
|
|
11
|
+
|
|
12
|
+
const STARTER_PATH = process.env.STARTER_PATH || 'devdoc-docs'
|
|
13
|
+
|
|
14
|
+
// Helper to get content root - supports both relative and absolute paths
|
|
15
|
+
function getContentRoot(): string {
|
|
16
|
+
if (isAbsolute(STARTER_PATH)) {
|
|
17
|
+
return STARTER_PATH
|
|
18
|
+
}
|
|
19
|
+
return join(process.cwd(), STARTER_PATH)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Shiki theme options for syntax highlighting
|
|
23
|
+
const prettyCodeOptions = {
|
|
24
|
+
theme: 'github-dark',
|
|
25
|
+
keepBackground: true,
|
|
26
|
+
defaultLang: 'plaintext',
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function GET(request: NextRequest) {
|
|
30
|
+
const searchParams = request.nextUrl.searchParams
|
|
31
|
+
const slug = searchParams.get('slug')
|
|
32
|
+
|
|
33
|
+
if (!slug) {
|
|
34
|
+
return NextResponse.json(
|
|
35
|
+
{ error: 'Missing slug parameter' },
|
|
36
|
+
{ status: 400 }
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Check for multi-tenant mode (project slug from middleware)
|
|
41
|
+
const projectSlug = request.headers.get('x-devdoc-project')
|
|
42
|
+
|
|
43
|
+
// If multi-tenant, fetch from Blob Storage
|
|
44
|
+
if (projectSlug && !projectSlug.startsWith('custom:')) {
|
|
45
|
+
return handleMultiTenantDocs(projectSlug, slug)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const starterDir = getContentRoot()
|
|
50
|
+
|
|
51
|
+
// Try .mdx then .md
|
|
52
|
+
let fullPath = join(starterDir, `${slug}.mdx`)
|
|
53
|
+
if (!existsSync(fullPath)) {
|
|
54
|
+
fullPath = join(starterDir, `${slug}.md`)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!existsSync(fullPath)) {
|
|
58
|
+
return NextResponse.json(
|
|
59
|
+
{ error: 'Page not found' },
|
|
60
|
+
{ status: 404 }
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const fileContent = readFileSync(fullPath, 'utf-8')
|
|
65
|
+
const { data: frontmatter, content } = matter(fileContent)
|
|
66
|
+
|
|
67
|
+
// Serialize MDX for client-side rendering
|
|
68
|
+
// Note: rehype-pretty-code is dynamically imported to handle cases where shiki
|
|
69
|
+
// may not be available in serverless environments
|
|
70
|
+
let mdxSource
|
|
71
|
+
try {
|
|
72
|
+
// Try with syntax highlighting first
|
|
73
|
+
const rehypePrettyCodeModule = await import('rehype-pretty-code')
|
|
74
|
+
const rehypePrettyCode = rehypePrettyCodeModule.default
|
|
75
|
+
|
|
76
|
+
mdxSource = await serialize(content, {
|
|
77
|
+
mdxOptions: {
|
|
78
|
+
remarkPlugins: [remarkGfm],
|
|
79
|
+
rehypePlugins: [
|
|
80
|
+
rehypeSlug,
|
|
81
|
+
[rehypePrettyCode, prettyCodeOptions],
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
parseFrontmatter: false,
|
|
85
|
+
})
|
|
86
|
+
} catch (syntaxError) {
|
|
87
|
+
// Fallback: serialize without syntax highlighting if shiki is unavailable
|
|
88
|
+
console.warn('[Docs API] Syntax highlighting unavailable, using fallback:', syntaxError)
|
|
89
|
+
mdxSource = await serialize(content, {
|
|
90
|
+
mdxOptions: {
|
|
91
|
+
remarkPlugins: [remarkGfm],
|
|
92
|
+
rehypePlugins: [rehypeSlug],
|
|
93
|
+
},
|
|
94
|
+
parseFrontmatter: false,
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return NextResponse.json({
|
|
99
|
+
slug,
|
|
100
|
+
frontmatter: {
|
|
101
|
+
title: frontmatter.title || slug,
|
|
102
|
+
description: frontmatter.description,
|
|
103
|
+
icon: frontmatter.icon,
|
|
104
|
+
},
|
|
105
|
+
mdxSource,
|
|
106
|
+
rawContent: content, // Include raw content for changelog version extraction
|
|
107
|
+
})
|
|
108
|
+
} catch (error) {
|
|
109
|
+
console.error('[Docs API] Error loading page:', error)
|
|
110
|
+
return NextResponse.json(
|
|
111
|
+
{ error: 'Failed to load page', details: error instanceof Error ? error.message : String(error) },
|
|
112
|
+
{ status: 500 }
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Handle multi-tenant docs request - fetch from Blob Storage
|
|
119
|
+
*/
|
|
120
|
+
async function handleMultiTenantDocs(projectSlug: string, slug: string): Promise<Response> {
|
|
121
|
+
try {
|
|
122
|
+
// Try .mdx then .md
|
|
123
|
+
let fileContent = await getProjectFile(projectSlug, `${slug}.mdx`)
|
|
124
|
+
if (!fileContent) {
|
|
125
|
+
fileContent = await getProjectFile(projectSlug, `${slug}.md`)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!fileContent) {
|
|
129
|
+
return NextResponse.json(
|
|
130
|
+
{ error: 'Page not found' },
|
|
131
|
+
{ status: 404 }
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const { data: frontmatter, content } = matter(fileContent)
|
|
136
|
+
|
|
137
|
+
// Serialize MDX for client-side rendering
|
|
138
|
+
// Note: rehype-pretty-code is dynamically imported to handle cases where shiki
|
|
139
|
+
// may not be available in serverless environments
|
|
140
|
+
let mdxSource
|
|
141
|
+
try {
|
|
142
|
+
// Try with syntax highlighting first
|
|
143
|
+
const rehypePrettyCodeModule = await import('rehype-pretty-code')
|
|
144
|
+
const rehypePrettyCode = rehypePrettyCodeModule.default
|
|
145
|
+
|
|
146
|
+
mdxSource = await serialize(content, {
|
|
147
|
+
mdxOptions: {
|
|
148
|
+
remarkPlugins: [remarkGfm],
|
|
149
|
+
rehypePlugins: [
|
|
150
|
+
rehypeSlug,
|
|
151
|
+
[rehypePrettyCode, prettyCodeOptions],
|
|
152
|
+
],
|
|
153
|
+
},
|
|
154
|
+
parseFrontmatter: false,
|
|
155
|
+
})
|
|
156
|
+
} catch (syntaxError) {
|
|
157
|
+
// Fallback: serialize without syntax highlighting if shiki is unavailable
|
|
158
|
+
console.warn('[Docs API] Multi-tenant syntax highlighting unavailable:', syntaxError)
|
|
159
|
+
mdxSource = await serialize(content, {
|
|
160
|
+
mdxOptions: {
|
|
161
|
+
remarkPlugins: [remarkGfm],
|
|
162
|
+
rehypePlugins: [rehypeSlug],
|
|
163
|
+
},
|
|
164
|
+
parseFrontmatter: false,
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return NextResponse.json({
|
|
169
|
+
slug,
|
|
170
|
+
frontmatter: {
|
|
171
|
+
title: frontmatter.title || slug,
|
|
172
|
+
description: frontmatter.description,
|
|
173
|
+
icon: frontmatter.icon,
|
|
174
|
+
},
|
|
175
|
+
mdxSource,
|
|
176
|
+
rawContent: content,
|
|
177
|
+
isMultiTenant: true,
|
|
178
|
+
projectSlug,
|
|
179
|
+
})
|
|
180
|
+
} catch (error) {
|
|
181
|
+
console.error('[Docs API] Multi-tenant error:', error)
|
|
182
|
+
return NextResponse.json(
|
|
183
|
+
{ error: 'Failed to load page', details: error instanceof Error ? error.message : String(error) },
|
|
184
|
+
{ status: 500 }
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server'
|
|
2
|
+
import {
|
|
3
|
+
validateApiKey,
|
|
4
|
+
regenerateApiKey,
|
|
5
|
+
projectExists
|
|
6
|
+
} from '@/lib/storage/blob'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* POST /api/keys/regenerate
|
|
10
|
+
* Regenerate API key for a project
|
|
11
|
+
*
|
|
12
|
+
* Headers:
|
|
13
|
+
* Authorization: Bearer <current_api_key>
|
|
14
|
+
* Body:
|
|
15
|
+
* { slug: string }
|
|
16
|
+
*/
|
|
17
|
+
export async function POST(request: NextRequest) {
|
|
18
|
+
try {
|
|
19
|
+
const body = await request.json()
|
|
20
|
+
const { slug } = body
|
|
21
|
+
|
|
22
|
+
if (!slug || typeof slug !== 'string') {
|
|
23
|
+
return NextResponse.json(
|
|
24
|
+
{ error: 'Missing or invalid project slug' },
|
|
25
|
+
{ status: 400 }
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Validate current API key
|
|
30
|
+
const authHeader = request.headers.get('Authorization')
|
|
31
|
+
const currentKey = authHeader?.replace('Bearer ', '')
|
|
32
|
+
|
|
33
|
+
if (!currentKey) {
|
|
34
|
+
return NextResponse.json(
|
|
35
|
+
{ error: 'API key required' },
|
|
36
|
+
{ status: 401 }
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Check project exists
|
|
41
|
+
const exists = await projectExists(slug)
|
|
42
|
+
if (!exists) {
|
|
43
|
+
return NextResponse.json(
|
|
44
|
+
{ error: 'Project not found' },
|
|
45
|
+
{ status: 404 }
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Validate the provided key belongs to this project
|
|
50
|
+
const validatedSlug = await validateApiKey(currentKey)
|
|
51
|
+
if (validatedSlug !== slug) {
|
|
52
|
+
return NextResponse.json(
|
|
53
|
+
{ error: 'Invalid API key for this project' },
|
|
54
|
+
{ status: 403 }
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Regenerate the key
|
|
59
|
+
const newKey = await regenerateApiKey(slug, currentKey)
|
|
60
|
+
|
|
61
|
+
if (!newKey) {
|
|
62
|
+
return NextResponse.json(
|
|
63
|
+
{ error: 'Failed to regenerate API key' },
|
|
64
|
+
{ status: 500 }
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return NextResponse.json({
|
|
69
|
+
success: true,
|
|
70
|
+
apiKey: newKey,
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.error('[Keys API] Error:', error)
|
|
75
|
+
return NextResponse.json(
|
|
76
|
+
{ error: 'Internal server error' },
|
|
77
|
+
{ status: 500 }
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
}
|