@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,845 @@
|
|
|
1
|
+
import { put, list, del, head } from '@vercel/blob'
|
|
2
|
+
import fs from 'fs'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
|
|
5
|
+
// Check if we're in local development mode (no blob token)
|
|
6
|
+
const IS_LOCAL_DEV = !process.env.BLOB_READ_WRITE_TOKEN
|
|
7
|
+
|
|
8
|
+
// Local storage directory for development
|
|
9
|
+
const LOCAL_STORAGE_DIR = path.join(process.cwd(), '.devdoc-storage')
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Project content structure stored in Vercel Blob
|
|
13
|
+
*/
|
|
14
|
+
export interface ProjectContent {
|
|
15
|
+
slug: string
|
|
16
|
+
name: string
|
|
17
|
+
docsJson: string // Stringified docs.json
|
|
18
|
+
files: ProjectFile[]
|
|
19
|
+
createdAt: string
|
|
20
|
+
updatedAt: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ProjectFile {
|
|
24
|
+
path: string // e.g., "index.mdx", "guides/overview.mdx"
|
|
25
|
+
content: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ProjectMetadata {
|
|
29
|
+
slug: string
|
|
30
|
+
name: string
|
|
31
|
+
createdAt: string
|
|
32
|
+
updatedAt: string
|
|
33
|
+
blobUrl: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Generate a unique project slug
|
|
38
|
+
*/
|
|
39
|
+
export function generateProjectSlug(name: string): string {
|
|
40
|
+
const base = name
|
|
41
|
+
.toLowerCase()
|
|
42
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
43
|
+
.replace(/^-|-$/g, '')
|
|
44
|
+
.substring(0, 30)
|
|
45
|
+
|
|
46
|
+
const suffix = Math.random().toString(36).substring(2, 8)
|
|
47
|
+
return `${base}-${suffix}`
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Get the blob path for a project's content
|
|
52
|
+
*/
|
|
53
|
+
function getProjectBlobPath(slug: string): string {
|
|
54
|
+
return `projects/${slug}/content.json`
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get the blob path for a project's metadata
|
|
59
|
+
*/
|
|
60
|
+
function getProjectMetadataPath(slug: string): string {
|
|
61
|
+
return `projects/${slug}/metadata.json`
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Get the blob path for individual files (reserved for future use)
|
|
66
|
+
*/
|
|
67
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
68
|
+
function _getFileBlobPath(slug: string, filePath: string): string {
|
|
69
|
+
return `projects/${slug}/files/${filePath}`
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Store project content in Vercel Blob (or local filesystem in dev)
|
|
74
|
+
*/
|
|
75
|
+
export async function storeProjectContent(
|
|
76
|
+
slug: string,
|
|
77
|
+
name: string,
|
|
78
|
+
docsJson: object,
|
|
79
|
+
files: ProjectFile[]
|
|
80
|
+
): Promise<{ url: string; slug: string }> {
|
|
81
|
+
const now = new Date().toISOString()
|
|
82
|
+
|
|
83
|
+
const content: ProjectContent = {
|
|
84
|
+
slug,
|
|
85
|
+
name,
|
|
86
|
+
docsJson: JSON.stringify(docsJson),
|
|
87
|
+
files,
|
|
88
|
+
createdAt: now,
|
|
89
|
+
updatedAt: now,
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Use local filesystem in development
|
|
93
|
+
if (IS_LOCAL_DEV) {
|
|
94
|
+
const projectDir = path.join(LOCAL_STORAGE_DIR, 'projects', slug)
|
|
95
|
+
fs.mkdirSync(projectDir, { recursive: true })
|
|
96
|
+
|
|
97
|
+
const contentPath = path.join(projectDir, 'content.json')
|
|
98
|
+
fs.writeFileSync(contentPath, JSON.stringify(content, null, 2))
|
|
99
|
+
|
|
100
|
+
const metadata: ProjectMetadata = {
|
|
101
|
+
slug,
|
|
102
|
+
name,
|
|
103
|
+
createdAt: now,
|
|
104
|
+
updatedAt: now,
|
|
105
|
+
blobUrl: `file://${contentPath}`,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
fs.writeFileSync(
|
|
109
|
+
path.join(projectDir, 'metadata.json'),
|
|
110
|
+
JSON.stringify(metadata, null, 2)
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
return { url: `file://${contentPath}`, slug }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Store main content bundle in Vercel Blob
|
|
117
|
+
const contentBlob = await put(
|
|
118
|
+
getProjectBlobPath(slug),
|
|
119
|
+
JSON.stringify(content),
|
|
120
|
+
{
|
|
121
|
+
access: 'public',
|
|
122
|
+
contentType: 'application/json',
|
|
123
|
+
allowOverwrite: true,
|
|
124
|
+
}
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
// Store metadata separately for quick lookups
|
|
128
|
+
const metadata: ProjectMetadata = {
|
|
129
|
+
slug,
|
|
130
|
+
name,
|
|
131
|
+
createdAt: now,
|
|
132
|
+
updatedAt: now,
|
|
133
|
+
blobUrl: contentBlob.url,
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
await put(
|
|
137
|
+
getProjectMetadataPath(slug),
|
|
138
|
+
JSON.stringify(metadata),
|
|
139
|
+
{
|
|
140
|
+
access: 'public',
|
|
141
|
+
contentType: 'application/json',
|
|
142
|
+
allowOverwrite: true,
|
|
143
|
+
}
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
return { url: contentBlob.url, slug }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Update existing project content
|
|
151
|
+
*/
|
|
152
|
+
export async function updateProjectContent(
|
|
153
|
+
slug: string,
|
|
154
|
+
docsJson: object,
|
|
155
|
+
files: ProjectFile[]
|
|
156
|
+
): Promise<{ url: string }> {
|
|
157
|
+
// Get existing content to preserve createdAt
|
|
158
|
+
const existing = await getProjectContent(slug)
|
|
159
|
+
const now = new Date().toISOString()
|
|
160
|
+
|
|
161
|
+
const content: ProjectContent = {
|
|
162
|
+
slug,
|
|
163
|
+
name: existing?.name || slug,
|
|
164
|
+
docsJson: JSON.stringify(docsJson),
|
|
165
|
+
files,
|
|
166
|
+
createdAt: existing?.createdAt || now,
|
|
167
|
+
updatedAt: now,
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Use local filesystem in development
|
|
171
|
+
if (IS_LOCAL_DEV) {
|
|
172
|
+
const projectDir = path.join(LOCAL_STORAGE_DIR, 'projects', slug)
|
|
173
|
+
fs.mkdirSync(projectDir, { recursive: true })
|
|
174
|
+
|
|
175
|
+
const contentPath = path.join(projectDir, 'content.json')
|
|
176
|
+
fs.writeFileSync(contentPath, JSON.stringify(content, null, 2))
|
|
177
|
+
|
|
178
|
+
const metadata: ProjectMetadata = {
|
|
179
|
+
slug,
|
|
180
|
+
name: content.name,
|
|
181
|
+
createdAt: content.createdAt,
|
|
182
|
+
updatedAt: now,
|
|
183
|
+
blobUrl: `file://${contentPath}`,
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
fs.writeFileSync(
|
|
187
|
+
path.join(projectDir, 'metadata.json'),
|
|
188
|
+
JSON.stringify(metadata, null, 2)
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
return { url: `file://${contentPath}` }
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Overwrite content in Vercel Blob
|
|
195
|
+
const contentBlob = await put(
|
|
196
|
+
getProjectBlobPath(slug),
|
|
197
|
+
JSON.stringify(content),
|
|
198
|
+
{
|
|
199
|
+
access: 'public',
|
|
200
|
+
contentType: 'application/json',
|
|
201
|
+
allowOverwrite: true, // Updates overwrite existing content
|
|
202
|
+
}
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
// Update metadata
|
|
206
|
+
const metadata: ProjectMetadata = {
|
|
207
|
+
slug,
|
|
208
|
+
name: content.name,
|
|
209
|
+
createdAt: content.createdAt,
|
|
210
|
+
updatedAt: now,
|
|
211
|
+
blobUrl: contentBlob.url,
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
await put(
|
|
215
|
+
getProjectMetadataPath(slug),
|
|
216
|
+
JSON.stringify(metadata),
|
|
217
|
+
{
|
|
218
|
+
access: 'public',
|
|
219
|
+
contentType: 'application/json',
|
|
220
|
+
allowOverwrite: true, // Updates overwrite existing metadata
|
|
221
|
+
}
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
return { url: contentBlob.url }
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Get project content from Vercel Blob (or local filesystem in dev)
|
|
229
|
+
*/
|
|
230
|
+
export async function getProjectContent(slug: string): Promise<ProjectContent | null> {
|
|
231
|
+
try {
|
|
232
|
+
// Use local filesystem in development
|
|
233
|
+
if (IS_LOCAL_DEV) {
|
|
234
|
+
const contentPath = path.join(LOCAL_STORAGE_DIR, 'projects', slug, 'content.json')
|
|
235
|
+
if (!fs.existsSync(contentPath)) {
|
|
236
|
+
return null
|
|
237
|
+
}
|
|
238
|
+
const data = fs.readFileSync(contentPath, 'utf-8')
|
|
239
|
+
return JSON.parse(data) as ProjectContent
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const blobPath = getProjectBlobPath(slug)
|
|
243
|
+
const blobInfo = await head(blobPath)
|
|
244
|
+
|
|
245
|
+
if (!blobInfo) {
|
|
246
|
+
return null
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const response = await fetch(blobInfo.url)
|
|
250
|
+
if (!response.ok) {
|
|
251
|
+
return null
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const content: ProjectContent = await response.json()
|
|
255
|
+
return content
|
|
256
|
+
} catch (error) {
|
|
257
|
+
console.error(`[Blob] Error fetching project ${slug}:`, error)
|
|
258
|
+
return null
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Get project metadata (quick lookup without full content)
|
|
264
|
+
*/
|
|
265
|
+
export async function getProjectMetadata(slug: string): Promise<ProjectMetadata | null> {
|
|
266
|
+
try {
|
|
267
|
+
const blobPath = getProjectMetadataPath(slug)
|
|
268
|
+
const blobInfo = await head(blobPath)
|
|
269
|
+
|
|
270
|
+
if (!blobInfo) {
|
|
271
|
+
return null
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const response = await fetch(blobInfo.url)
|
|
275
|
+
if (!response.ok) {
|
|
276
|
+
return null
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const metadata: ProjectMetadata = await response.json()
|
|
280
|
+
return metadata
|
|
281
|
+
} catch (error) {
|
|
282
|
+
console.error(`[Blob] Error fetching metadata for ${slug}:`, error)
|
|
283
|
+
return null
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Get a specific file from project content
|
|
289
|
+
*/
|
|
290
|
+
export async function getProjectFile(
|
|
291
|
+
slug: string,
|
|
292
|
+
filePath: string
|
|
293
|
+
): Promise<string | null> {
|
|
294
|
+
const content = await getProjectContent(slug)
|
|
295
|
+
if (!content) {
|
|
296
|
+
return null
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const file = content.files.find(f => f.path === filePath)
|
|
300
|
+
return file?.content || null
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Get docs.json for a project
|
|
305
|
+
*/
|
|
306
|
+
export async function getProjectDocsJson(slug: string): Promise<object | null> {
|
|
307
|
+
const content = await getProjectContent(slug)
|
|
308
|
+
if (!content) {
|
|
309
|
+
return null
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
try {
|
|
313
|
+
return JSON.parse(content.docsJson)
|
|
314
|
+
} catch {
|
|
315
|
+
return null
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Check if a project exists
|
|
321
|
+
*/
|
|
322
|
+
export async function projectExists(slug: string): Promise<boolean> {
|
|
323
|
+
try {
|
|
324
|
+
// Use local filesystem in development
|
|
325
|
+
if (IS_LOCAL_DEV) {
|
|
326
|
+
const projectDir = path.join(LOCAL_STORAGE_DIR, 'projects', slug)
|
|
327
|
+
// Check for either metadata.json (full project) or apikey.json (registered project)
|
|
328
|
+
const metadataPath = path.join(projectDir, 'metadata.json')
|
|
329
|
+
const apiKeyPath = path.join(projectDir, 'apikey.json')
|
|
330
|
+
return fs.existsSync(metadataPath) || fs.existsSync(apiKeyPath)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// In production, check both metadata and apikey paths
|
|
334
|
+
const metadataPath = getProjectMetadataPath(slug)
|
|
335
|
+
const apiKeyPath = getApiKeyBlobPath(slug)
|
|
336
|
+
|
|
337
|
+
const [metadataInfo, apiKeyInfo] = await Promise.all([
|
|
338
|
+
head(metadataPath).catch(() => null),
|
|
339
|
+
head(apiKeyPath).catch(() => null),
|
|
340
|
+
])
|
|
341
|
+
|
|
342
|
+
return metadataInfo !== null || apiKeyInfo !== null
|
|
343
|
+
} catch {
|
|
344
|
+
return false
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Delete a project and all its content
|
|
350
|
+
*/
|
|
351
|
+
export async function deleteProject(slug: string): Promise<void> {
|
|
352
|
+
try {
|
|
353
|
+
// List all blobs for this project
|
|
354
|
+
const { blobs } = await list({ prefix: `projects/${slug}/` })
|
|
355
|
+
|
|
356
|
+
// Delete all blobs
|
|
357
|
+
for (const blob of blobs) {
|
|
358
|
+
await del(blob.url)
|
|
359
|
+
}
|
|
360
|
+
} catch (error) {
|
|
361
|
+
console.error(`[Blob] Error deleting project ${slug}:`, error)
|
|
362
|
+
throw error
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* List all projects (for admin purposes)
|
|
368
|
+
*/
|
|
369
|
+
export async function listProjects(): Promise<ProjectMetadata[]> {
|
|
370
|
+
try {
|
|
371
|
+
const { blobs } = await list({ prefix: 'projects/' })
|
|
372
|
+
|
|
373
|
+
// Filter for metadata files only
|
|
374
|
+
const metadataBlobs = blobs.filter(b => b.pathname.endsWith('/metadata.json'))
|
|
375
|
+
|
|
376
|
+
const projects: ProjectMetadata[] = []
|
|
377
|
+
for (const blob of metadataBlobs) {
|
|
378
|
+
try {
|
|
379
|
+
const response = await fetch(blob.url)
|
|
380
|
+
if (response.ok) {
|
|
381
|
+
const metadata: ProjectMetadata = await response.json()
|
|
382
|
+
projects.push(metadata)
|
|
383
|
+
}
|
|
384
|
+
} catch {
|
|
385
|
+
// Skip invalid entries
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return projects
|
|
390
|
+
} catch (error) {
|
|
391
|
+
console.error('[Blob] Error listing projects:', error)
|
|
392
|
+
return []
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// =============================================================================
|
|
397
|
+
// API Key Management
|
|
398
|
+
// =============================================================================
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* API Key data structure
|
|
402
|
+
*/
|
|
403
|
+
export interface ProjectApiKey {
|
|
404
|
+
key: string
|
|
405
|
+
slug: string
|
|
406
|
+
createdAt: string
|
|
407
|
+
lastUsedAt?: string
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Get the blob path for a project's API key
|
|
412
|
+
*/
|
|
413
|
+
function getApiKeyBlobPath(slug: string): string {
|
|
414
|
+
return `projects/${slug}/apikey.json`
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// =============================================================================
|
|
418
|
+
// Domain Registry - O(1) lookups for subdomains and API keys
|
|
419
|
+
// =============================================================================
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Registry structure - single file for all domains/projects
|
|
423
|
+
*/
|
|
424
|
+
export interface DomainRegistry {
|
|
425
|
+
domains: Record<string, DomainEntry> // subdomain -> entry
|
|
426
|
+
apiKeys: Record<string, string> // apiKey -> subdomain (for O(1) key validation)
|
|
427
|
+
updatedAt: string
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export interface DomainEntry {
|
|
431
|
+
subdomain: string
|
|
432
|
+
projectId: string
|
|
433
|
+
name: string
|
|
434
|
+
createdAt: string
|
|
435
|
+
updatedAt?: string
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const REGISTRY_PATH = 'registry/domains.json'
|
|
439
|
+
const LOCAL_REGISTRY_PATH = path.join(LOCAL_STORAGE_DIR, 'registry', 'domains.json')
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Get the domain registry (cached in memory for performance)
|
|
443
|
+
*/
|
|
444
|
+
let registryCache: DomainRegistry | null = null
|
|
445
|
+
let registryCacheTime = 0
|
|
446
|
+
const CACHE_TTL = 5000 // 5 seconds
|
|
447
|
+
|
|
448
|
+
async function getRegistry(): Promise<DomainRegistry> {
|
|
449
|
+
const now = Date.now()
|
|
450
|
+
|
|
451
|
+
// Return cached if fresh
|
|
452
|
+
if (registryCache && (now - registryCacheTime) < CACHE_TTL) {
|
|
453
|
+
return registryCache
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
try {
|
|
457
|
+
if (IS_LOCAL_DEV) {
|
|
458
|
+
if (fs.existsSync(LOCAL_REGISTRY_PATH)) {
|
|
459
|
+
const data = fs.readFileSync(LOCAL_REGISTRY_PATH, 'utf-8')
|
|
460
|
+
registryCache = JSON.parse(data) as DomainRegistry
|
|
461
|
+
registryCacheTime = now
|
|
462
|
+
return registryCache
|
|
463
|
+
}
|
|
464
|
+
} else {
|
|
465
|
+
const blobInfo = await head(REGISTRY_PATH).catch(() => null)
|
|
466
|
+
if (blobInfo) {
|
|
467
|
+
const response = await fetch(blobInfo.url)
|
|
468
|
+
if (response.ok) {
|
|
469
|
+
registryCache = await response.json() as DomainRegistry
|
|
470
|
+
registryCacheTime = now
|
|
471
|
+
return registryCache
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
} catch (error) {
|
|
476
|
+
console.error('[Registry] Error loading registry:', error)
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Return empty registry if not found
|
|
480
|
+
return { domains: {}, apiKeys: {}, updatedAt: new Date().toISOString() }
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Save the domain registry
|
|
485
|
+
*/
|
|
486
|
+
async function saveRegistry(registry: DomainRegistry): Promise<void> {
|
|
487
|
+
registry.updatedAt = new Date().toISOString()
|
|
488
|
+
|
|
489
|
+
if (IS_LOCAL_DEV) {
|
|
490
|
+
const dir = path.dirname(LOCAL_REGISTRY_PATH)
|
|
491
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
492
|
+
fs.writeFileSync(LOCAL_REGISTRY_PATH, JSON.stringify(registry, null, 2))
|
|
493
|
+
} else {
|
|
494
|
+
await put(REGISTRY_PATH, JSON.stringify(registry), {
|
|
495
|
+
access: 'public',
|
|
496
|
+
contentType: 'application/json',
|
|
497
|
+
allowOverwrite: true, // Registry is a single file that gets updated
|
|
498
|
+
})
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Update cache
|
|
502
|
+
registryCache = registry
|
|
503
|
+
registryCacheTime = Date.now()
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Check if a subdomain is registered (O(1) lookup)
|
|
508
|
+
*/
|
|
509
|
+
export async function isSubdomainRegistered(subdomain: string): Promise<boolean> {
|
|
510
|
+
const registry = await getRegistry()
|
|
511
|
+
return subdomain in registry.domains
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Register a new subdomain in the registry
|
|
516
|
+
*/
|
|
517
|
+
export async function registerSubdomain(
|
|
518
|
+
subdomain: string,
|
|
519
|
+
projectId: string,
|
|
520
|
+
name: string,
|
|
521
|
+
apiKey: string
|
|
522
|
+
): Promise<void> {
|
|
523
|
+
const registry = await getRegistry()
|
|
524
|
+
const now = new Date().toISOString()
|
|
525
|
+
|
|
526
|
+
registry.domains[subdomain] = {
|
|
527
|
+
subdomain,
|
|
528
|
+
projectId,
|
|
529
|
+
name,
|
|
530
|
+
createdAt: now,
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
registry.apiKeys[apiKey] = subdomain
|
|
534
|
+
|
|
535
|
+
await saveRegistry(registry)
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Validate API key and get subdomain (O(1) lookup)
|
|
540
|
+
*/
|
|
541
|
+
export async function validateApiKeyFromRegistry(apiKey: string): Promise<string | null> {
|
|
542
|
+
if (!apiKey || !apiKey.startsWith('sk_live_') || apiKey.length !== 40) {
|
|
543
|
+
return null
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const registry = await getRegistry()
|
|
547
|
+
return registry.apiKeys[apiKey] || null
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Get domain entry from registry
|
|
552
|
+
*/
|
|
553
|
+
export async function getDomainEntry(subdomain: string): Promise<DomainEntry | null> {
|
|
554
|
+
const registry = await getRegistry()
|
|
555
|
+
return registry.domains[subdomain] || null
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Generate a secure API key
|
|
560
|
+
*/
|
|
561
|
+
export function generateApiKey(): string {
|
|
562
|
+
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
|
|
563
|
+
let key = 'sk_live_'
|
|
564
|
+
for (let i = 0; i < 32; i++) {
|
|
565
|
+
key += chars.charAt(Math.floor(Math.random() * chars.length))
|
|
566
|
+
}
|
|
567
|
+
return key
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Store API key for a project
|
|
572
|
+
*/
|
|
573
|
+
export async function storeProjectApiKey(
|
|
574
|
+
slug: string,
|
|
575
|
+
apiKey: string
|
|
576
|
+
): Promise<void> {
|
|
577
|
+
const keyData: ProjectApiKey = {
|
|
578
|
+
key: apiKey,
|
|
579
|
+
slug,
|
|
580
|
+
createdAt: new Date().toISOString(),
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Use local filesystem in development
|
|
584
|
+
if (IS_LOCAL_DEV) {
|
|
585
|
+
const projectDir = path.join(LOCAL_STORAGE_DIR, 'projects', slug)
|
|
586
|
+
fs.mkdirSync(projectDir, { recursive: true })
|
|
587
|
+
fs.writeFileSync(
|
|
588
|
+
path.join(projectDir, 'apikey.json'),
|
|
589
|
+
JSON.stringify(keyData, null, 2)
|
|
590
|
+
)
|
|
591
|
+
return
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
await put(
|
|
595
|
+
getApiKeyBlobPath(slug),
|
|
596
|
+
JSON.stringify(keyData),
|
|
597
|
+
{
|
|
598
|
+
access: 'public', // Note: In production, consider private access
|
|
599
|
+
contentType: 'application/json',
|
|
600
|
+
allowOverwrite: true, // Allow key regeneration
|
|
601
|
+
}
|
|
602
|
+
)
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Get API key data for a project
|
|
607
|
+
*/
|
|
608
|
+
export async function getProjectApiKey(slug: string): Promise<ProjectApiKey | null> {
|
|
609
|
+
try {
|
|
610
|
+
// Use local filesystem in development
|
|
611
|
+
if (IS_LOCAL_DEV) {
|
|
612
|
+
const keyPath = path.join(LOCAL_STORAGE_DIR, 'projects', slug, 'apikey.json')
|
|
613
|
+
if (!fs.existsSync(keyPath)) {
|
|
614
|
+
return null
|
|
615
|
+
}
|
|
616
|
+
const data = fs.readFileSync(keyPath, 'utf-8')
|
|
617
|
+
return JSON.parse(data) as ProjectApiKey
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const blobPath = getApiKeyBlobPath(slug)
|
|
621
|
+
const blobInfo = await head(blobPath)
|
|
622
|
+
|
|
623
|
+
if (!blobInfo) {
|
|
624
|
+
return null
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const response = await fetch(blobInfo.url)
|
|
628
|
+
if (!response.ok) {
|
|
629
|
+
return null
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
return await response.json() as ProjectApiKey
|
|
633
|
+
} catch (error) {
|
|
634
|
+
console.error(`[Blob] Error fetching API key for ${slug}:`, error)
|
|
635
|
+
return null
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Validate an API key and return the associated project slug
|
|
641
|
+
* O(1) lookup from registry
|
|
642
|
+
*/
|
|
643
|
+
export async function validateApiKey(apiKey: string): Promise<string | null> {
|
|
644
|
+
// API key format: sk_live_<32chars>
|
|
645
|
+
if (!apiKey || !apiKey.startsWith('sk_live_') || apiKey.length !== 40) {
|
|
646
|
+
return null
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
try {
|
|
650
|
+
// O(1) registry lookup
|
|
651
|
+
const slug = await validateApiKeyFromRegistry(apiKey)
|
|
652
|
+
if (slug) {
|
|
653
|
+
// Update last used timestamp
|
|
654
|
+
await updateApiKeyLastUsed(slug)
|
|
655
|
+
}
|
|
656
|
+
return slug
|
|
657
|
+
} catch (error) {
|
|
658
|
+
console.error('[Blob] Error validating API key:', error)
|
|
659
|
+
return null
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* Update the lastUsedAt timestamp for an API key
|
|
665
|
+
*/
|
|
666
|
+
async function updateApiKeyLastUsed(slug: string): Promise<void> {
|
|
667
|
+
try {
|
|
668
|
+
if (IS_LOCAL_DEV) {
|
|
669
|
+
const keyPath = path.join(LOCAL_STORAGE_DIR, 'projects', slug, 'apikey.json')
|
|
670
|
+
if (fs.existsSync(keyPath)) {
|
|
671
|
+
const data = JSON.parse(fs.readFileSync(keyPath, 'utf-8')) as ProjectApiKey
|
|
672
|
+
data.lastUsedAt = new Date().toISOString()
|
|
673
|
+
fs.writeFileSync(keyPath, JSON.stringify(data, null, 2))
|
|
674
|
+
}
|
|
675
|
+
} else {
|
|
676
|
+
const blobPath = getApiKeyBlobPath(slug)
|
|
677
|
+
const blobInfo = await head(blobPath).catch(() => null)
|
|
678
|
+
if (blobInfo) {
|
|
679
|
+
const response = await fetch(blobInfo.url)
|
|
680
|
+
if (response.ok) {
|
|
681
|
+
const keyData: ProjectApiKey = await response.json()
|
|
682
|
+
keyData.lastUsedAt = new Date().toISOString()
|
|
683
|
+
await put(blobPath, JSON.stringify(keyData), {
|
|
684
|
+
access: 'public',
|
|
685
|
+
contentType: 'application/json',
|
|
686
|
+
allowOverwrite: true,
|
|
687
|
+
})
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
} catch {
|
|
692
|
+
// Non-critical, ignore errors
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* Regenerate API key for a project (requires old key for auth)
|
|
698
|
+
*/
|
|
699
|
+
export async function regenerateApiKey(
|
|
700
|
+
slug: string,
|
|
701
|
+
oldApiKey: string
|
|
702
|
+
): Promise<string | null> {
|
|
703
|
+
// Validate old key first
|
|
704
|
+
const validSlug = await validateApiKey(oldApiKey)
|
|
705
|
+
if (validSlug !== slug) {
|
|
706
|
+
return null
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// Generate new key
|
|
710
|
+
const newKey = generateApiKey()
|
|
711
|
+
|
|
712
|
+
// Update registry: remove old key, add new key
|
|
713
|
+
const registry = await getRegistry()
|
|
714
|
+
delete registry.apiKeys[oldApiKey]
|
|
715
|
+
registry.apiKeys[newKey] = slug
|
|
716
|
+
await saveRegistry(registry)
|
|
717
|
+
|
|
718
|
+
// Also update project's apikey.json
|
|
719
|
+
await storeProjectApiKey(slug, newKey)
|
|
720
|
+
|
|
721
|
+
return newKey
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// =============================================================================
|
|
725
|
+
// Asset Management
|
|
726
|
+
// =============================================================================
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* Asset metadata structure
|
|
730
|
+
*/
|
|
731
|
+
export interface ProjectAsset {
|
|
732
|
+
path: string
|
|
733
|
+
url: string
|
|
734
|
+
fileName: string
|
|
735
|
+
size: number
|
|
736
|
+
contentType: string
|
|
737
|
+
uploadedAt: string
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* Get the blob path for a project's assets
|
|
742
|
+
*/
|
|
743
|
+
export function getAssetBlobPath(slug: string, fileName: string): string {
|
|
744
|
+
return `projects/${slug}/assets/${fileName}`
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* List all assets for a project
|
|
749
|
+
*/
|
|
750
|
+
export async function listProjectAssets(slug: string): Promise<ProjectAsset[]> {
|
|
751
|
+
try {
|
|
752
|
+
if (IS_LOCAL_DEV) {
|
|
753
|
+
const assetsDir = path.join(LOCAL_STORAGE_DIR, 'projects', slug, 'assets')
|
|
754
|
+
if (!fs.existsSync(assetsDir)) {
|
|
755
|
+
return []
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const files = fs.readdirSync(assetsDir)
|
|
759
|
+
return files.map(fileName => {
|
|
760
|
+
const filePath = path.join(assetsDir, fileName)
|
|
761
|
+
const stats = fs.statSync(filePath)
|
|
762
|
+
return {
|
|
763
|
+
path: `projects/${slug}/assets/${fileName}`,
|
|
764
|
+
url: `file://${filePath}`,
|
|
765
|
+
fileName,
|
|
766
|
+
size: stats.size,
|
|
767
|
+
contentType: getContentType(fileName),
|
|
768
|
+
uploadedAt: stats.mtime.toISOString(),
|
|
769
|
+
}
|
|
770
|
+
})
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
const { blobs } = await list({ prefix: `projects/${slug}/assets/` })
|
|
774
|
+
|
|
775
|
+
return blobs.map(blob => {
|
|
776
|
+
const fileName = blob.pathname.split('/').pop() || ''
|
|
777
|
+
return {
|
|
778
|
+
path: blob.pathname,
|
|
779
|
+
url: blob.url,
|
|
780
|
+
fileName,
|
|
781
|
+
size: blob.size,
|
|
782
|
+
contentType: getContentType(fileName),
|
|
783
|
+
uploadedAt: blob.uploadedAt.toISOString(),
|
|
784
|
+
}
|
|
785
|
+
})
|
|
786
|
+
} catch (error) {
|
|
787
|
+
console.error(`[Blob] Error listing assets for ${slug}:`, error)
|
|
788
|
+
return []
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
/**
|
|
793
|
+
* Delete an asset
|
|
794
|
+
*/
|
|
795
|
+
export async function deleteProjectAsset(slug: string, fileName: string): Promise<boolean> {
|
|
796
|
+
try {
|
|
797
|
+
if (IS_LOCAL_DEV) {
|
|
798
|
+
const filePath = path.join(LOCAL_STORAGE_DIR, 'projects', slug, 'assets', fileName)
|
|
799
|
+
if (fs.existsSync(filePath)) {
|
|
800
|
+
fs.unlinkSync(filePath)
|
|
801
|
+
return true
|
|
802
|
+
}
|
|
803
|
+
return false
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
const blobPath = getAssetBlobPath(slug, fileName)
|
|
807
|
+
const blobInfo = await head(blobPath).catch(() => null)
|
|
808
|
+
|
|
809
|
+
if (blobInfo) {
|
|
810
|
+
await del(blobInfo.url)
|
|
811
|
+
return true
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
return false
|
|
815
|
+
} catch (error) {
|
|
816
|
+
console.error(`[Blob] Error deleting asset ${fileName} for ${slug}:`, error)
|
|
817
|
+
return false
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/**
|
|
822
|
+
* Helper to get content type from file extension
|
|
823
|
+
*/
|
|
824
|
+
function getContentType(fileName: string): string {
|
|
825
|
+
const ext = fileName.split('.').pop()?.toLowerCase()
|
|
826
|
+
const types: Record<string, string> = {
|
|
827
|
+
'jpg': 'image/jpeg',
|
|
828
|
+
'jpeg': 'image/jpeg',
|
|
829
|
+
'png': 'image/png',
|
|
830
|
+
'gif': 'image/gif',
|
|
831
|
+
'webp': 'image/webp',
|
|
832
|
+
'svg': 'image/svg+xml',
|
|
833
|
+
'ico': 'image/x-icon',
|
|
834
|
+
'pdf': 'application/pdf',
|
|
835
|
+
'mp4': 'video/mp4',
|
|
836
|
+
'webm': 'video/webm',
|
|
837
|
+
'mp3': 'audio/mpeg',
|
|
838
|
+
'wav': 'audio/wav',
|
|
839
|
+
'woff': 'font/woff',
|
|
840
|
+
'woff2': 'font/woff2',
|
|
841
|
+
'ttf': 'font/ttf',
|
|
842
|
+
'otf': 'font/otf',
|
|
843
|
+
}
|
|
844
|
+
return types[ext || ''] || 'application/octet-stream'
|
|
845
|
+
}
|