@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.
Files changed (268) hide show
  1. package/LICENSE +33 -0
  2. package/README.md +415 -0
  3. package/bin/devdoc.js +13 -0
  4. package/dist/cli/commands/build.d.ts +5 -0
  5. package/dist/cli/commands/build.js +87 -0
  6. package/dist/cli/commands/check.d.ts +1 -0
  7. package/dist/cli/commands/check.js +143 -0
  8. package/dist/cli/commands/create.d.ts +24 -0
  9. package/dist/cli/commands/create.js +387 -0
  10. package/dist/cli/commands/deploy.d.ts +9 -0
  11. package/dist/cli/commands/deploy.js +433 -0
  12. package/dist/cli/commands/dev.d.ts +6 -0
  13. package/dist/cli/commands/dev.js +139 -0
  14. package/dist/cli/commands/init.d.ts +11 -0
  15. package/dist/cli/commands/init.js +238 -0
  16. package/dist/cli/commands/keys.d.ts +12 -0
  17. package/dist/cli/commands/keys.js +165 -0
  18. package/dist/cli/commands/start.d.ts +5 -0
  19. package/dist/cli/commands/start.js +56 -0
  20. package/dist/cli/commands/upload.d.ts +13 -0
  21. package/dist/cli/commands/upload.js +238 -0
  22. package/dist/cli/commands/whoami.d.ts +8 -0
  23. package/dist/cli/commands/whoami.js +91 -0
  24. package/dist/cli/index.d.ts +1 -0
  25. package/dist/cli/index.js +106 -0
  26. package/dist/config/index.d.ts +80 -0
  27. package/dist/config/index.js +133 -0
  28. package/dist/constants.d.ts +9 -0
  29. package/dist/constants.js +13 -0
  30. package/dist/index.d.ts +7 -0
  31. package/dist/index.js +12 -0
  32. package/dist/utils/logger.d.ts +16 -0
  33. package/dist/utils/logger.js +61 -0
  34. package/dist/utils/paths.d.ts +16 -0
  35. package/dist/utils/paths.js +50 -0
  36. package/package.json +51 -0
  37. package/renderer/app/api/assets/[...path]/route.ts +123 -0
  38. package/renderer/app/api/assets/route.ts +124 -0
  39. package/renderer/app/api/assets/upload/route.ts +177 -0
  40. package/renderer/app/api/auth-schemes/route.ts +77 -0
  41. package/renderer/app/api/chat/route.ts +858 -0
  42. package/renderer/app/api/codegen/route.ts +72 -0
  43. package/renderer/app/api/collections/route.ts +1016 -0
  44. package/renderer/app/api/debug/route.ts +53 -0
  45. package/renderer/app/api/deploy/route.ts +234 -0
  46. package/renderer/app/api/device/route.ts +42 -0
  47. package/renderer/app/api/docs/route.ts +187 -0
  48. package/renderer/app/api/keys/regenerate/route.ts +80 -0
  49. package/renderer/app/api/openapi-spec/route.ts +151 -0
  50. package/renderer/app/api/projects/[slug]/route.ts +153 -0
  51. package/renderer/app/api/projects/[slug]/stats/route.ts +96 -0
  52. package/renderer/app/api/projects/register/route.ts +152 -0
  53. package/renderer/app/api/proxy/route.ts +149 -0
  54. package/renderer/app/api/proxy-stream/route.ts +168 -0
  55. package/renderer/app/api/redirects/route.ts +47 -0
  56. package/renderer/app/api/schema/route.ts +65 -0
  57. package/renderer/app/api/subdomains/check/route.ts +172 -0
  58. package/renderer/app/api/suggestions/route.ts +144 -0
  59. package/renderer/app/favicon.ico +0 -0
  60. package/renderer/app/globals.css +1103 -0
  61. package/renderer/app/layout.tsx +47 -0
  62. package/renderer/app/llms-full.txt/route.ts +346 -0
  63. package/renderer/app/llms.txt/route.ts +279 -0
  64. package/renderer/app/page.tsx +14 -0
  65. package/renderer/app/robots.txt/route.ts +84 -0
  66. package/renderer/app/sitemap.xml/route.ts +199 -0
  67. package/renderer/components/docs/index.ts +12 -0
  68. package/renderer/components/docs/mdx/accordion.tsx +169 -0
  69. package/renderer/components/docs/mdx/badge.tsx +132 -0
  70. package/renderer/components/docs/mdx/callouts.tsx +154 -0
  71. package/renderer/components/docs/mdx/cards.tsx +213 -0
  72. package/renderer/components/docs/mdx/changelog.tsx +120 -0
  73. package/renderer/components/docs/mdx/code-block.tsx +186 -0
  74. package/renderer/components/docs/mdx/code-group.tsx +421 -0
  75. package/renderer/components/docs/mdx/file-embeds.tsx +105 -0
  76. package/renderer/components/docs/mdx/frame.tsx +112 -0
  77. package/renderer/components/docs/mdx/highlight.tsx +151 -0
  78. package/renderer/components/docs/mdx/iframe.tsx +134 -0
  79. package/renderer/components/docs/mdx/image.tsx +235 -0
  80. package/renderer/components/docs/mdx/index.ts +204 -0
  81. package/renderer/components/docs/mdx/mermaid.tsx +240 -0
  82. package/renderer/components/docs/mdx/param-field.tsx +200 -0
  83. package/renderer/components/docs/mdx/steps.tsx +113 -0
  84. package/renderer/components/docs/mdx/tabs.tsx +86 -0
  85. package/renderer/components/docs/mdx-renderer.tsx +100 -0
  86. package/renderer/components/docs/navigation/breadcrumbs.tsx +76 -0
  87. package/renderer/components/docs/navigation/index.ts +8 -0
  88. package/renderer/components/docs/navigation/page-nav.tsx +64 -0
  89. package/renderer/components/docs/navigation/sidebar.tsx +515 -0
  90. package/renderer/components/docs/navigation/toc.tsx +113 -0
  91. package/renderer/components/docs/notice.tsx +105 -0
  92. package/renderer/components/docs-header.tsx +274 -0
  93. package/renderer/components/docs-viewer/agent/agent-chat.tsx +2076 -0
  94. package/renderer/components/docs-viewer/agent/cards/debug-context-card.tsx +90 -0
  95. package/renderer/components/docs-viewer/agent/cards/endpoint-context-card.tsx +49 -0
  96. package/renderer/components/docs-viewer/agent/cards/index.tsx +50 -0
  97. package/renderer/components/docs-viewer/agent/cards/response-options-card.tsx +212 -0
  98. package/renderer/components/docs-viewer/agent/cards/types.ts +84 -0
  99. package/renderer/components/docs-viewer/agent/chat-message.tsx +17 -0
  100. package/renderer/components/docs-viewer/agent/index.tsx +6 -0
  101. package/renderer/components/docs-viewer/agent/messages/assistant-message.tsx +119 -0
  102. package/renderer/components/docs-viewer/agent/messages/chat-message.tsx +46 -0
  103. package/renderer/components/docs-viewer/agent/messages/index.ts +17 -0
  104. package/renderer/components/docs-viewer/agent/messages/tool-call-display.tsx +721 -0
  105. package/renderer/components/docs-viewer/agent/messages/types.ts +61 -0
  106. package/renderer/components/docs-viewer/agent/messages/typing-indicator.tsx +24 -0
  107. package/renderer/components/docs-viewer/agent/messages/user-message.tsx +51 -0
  108. package/renderer/components/docs-viewer/code-editor/index.tsx +2 -0
  109. package/renderer/components/docs-viewer/code-editor/notes-mode.tsx +1283 -0
  110. package/renderer/components/docs-viewer/content/changelog-page.tsx +331 -0
  111. package/renderer/components/docs-viewer/content/doc-page.tsx +285 -0
  112. package/renderer/components/docs-viewer/content/documentation-viewer.tsx +17 -0
  113. package/renderer/components/docs-viewer/content/index.tsx +29 -0
  114. package/renderer/components/docs-viewer/content/introduction.tsx +21 -0
  115. package/renderer/components/docs-viewer/content/request-details.tsx +330 -0
  116. package/renderer/components/docs-viewer/content/sections/auth.tsx +69 -0
  117. package/renderer/components/docs-viewer/content/sections/body.tsx +66 -0
  118. package/renderer/components/docs-viewer/content/sections/headers.tsx +43 -0
  119. package/renderer/components/docs-viewer/content/sections/overview.tsx +40 -0
  120. package/renderer/components/docs-viewer/content/sections/parameters.tsx +43 -0
  121. package/renderer/components/docs-viewer/content/sections/responses.tsx +87 -0
  122. package/renderer/components/docs-viewer/global-auth-modal.tsx +352 -0
  123. package/renderer/components/docs-viewer/index.tsx +1466 -0
  124. package/renderer/components/docs-viewer/playground/auth-editor.tsx +280 -0
  125. package/renderer/components/docs-viewer/playground/body-editor.tsx +221 -0
  126. package/renderer/components/docs-viewer/playground/code-editor.tsx +224 -0
  127. package/renderer/components/docs-viewer/playground/code-snippet.tsx +387 -0
  128. package/renderer/components/docs-viewer/playground/graphql-playground.tsx +745 -0
  129. package/renderer/components/docs-viewer/playground/index.tsx +671 -0
  130. package/renderer/components/docs-viewer/playground/key-value-editor.tsx +261 -0
  131. package/renderer/components/docs-viewer/playground/method-selector.tsx +60 -0
  132. package/renderer/components/docs-viewer/playground/request-builder.tsx +179 -0
  133. package/renderer/components/docs-viewer/playground/request-tabs.tsx +237 -0
  134. package/renderer/components/docs-viewer/playground/response-cards/idle-card.tsx +21 -0
  135. package/renderer/components/docs-viewer/playground/response-cards/index.tsx +93 -0
  136. package/renderer/components/docs-viewer/playground/response-cards/loading-card.tsx +16 -0
  137. package/renderer/components/docs-viewer/playground/response-cards/network-error-card.tsx +23 -0
  138. package/renderer/components/docs-viewer/playground/response-cards/response-body-card.tsx +268 -0
  139. package/renderer/components/docs-viewer/playground/response-cards/types.ts +82 -0
  140. package/renderer/components/docs-viewer/playground/response-viewer.tsx +43 -0
  141. package/renderer/components/docs-viewer/search/index.ts +2 -0
  142. package/renderer/components/docs-viewer/search/search-dialog.tsx +331 -0
  143. package/renderer/components/docs-viewer/search/use-search.ts +117 -0
  144. package/renderer/components/docs-viewer/shared/markdown-renderer.tsx +431 -0
  145. package/renderer/components/docs-viewer/shared/method-badge.tsx +41 -0
  146. package/renderer/components/docs-viewer/shared/schema-viewer.tsx +349 -0
  147. package/renderer/components/docs-viewer/sidebar/collection-tree.tsx +239 -0
  148. package/renderer/components/docs-viewer/sidebar/endpoint-options.tsx +316 -0
  149. package/renderer/components/docs-viewer/sidebar/index.tsx +343 -0
  150. package/renderer/components/docs-viewer/sidebar/right-sidebar.tsx +202 -0
  151. package/renderer/components/docs-viewer/sidebar/sidebar-group.tsx +118 -0
  152. package/renderer/components/docs-viewer/sidebar/sidebar-item.tsx +226 -0
  153. package/renderer/components/docs-viewer/sidebar/sidebar-section.tsx +52 -0
  154. package/renderer/components/theme-provider.tsx +11 -0
  155. package/renderer/components/theme-toggle.tsx +76 -0
  156. package/renderer/components/ui/badge.tsx +46 -0
  157. package/renderer/components/ui/button.tsx +59 -0
  158. package/renderer/components/ui/dialog.tsx +118 -0
  159. package/renderer/components/ui/dropdown-menu.tsx +257 -0
  160. package/renderer/components/ui/input.tsx +21 -0
  161. package/renderer/components/ui/label.tsx +24 -0
  162. package/renderer/components/ui/navigation-menu.tsx +168 -0
  163. package/renderer/components/ui/select.tsx +190 -0
  164. package/renderer/components/ui/spinner.tsx +114 -0
  165. package/renderer/components/ui/tabs.tsx +66 -0
  166. package/renderer/components/ui/tooltip.tsx +61 -0
  167. package/renderer/hooks/use-code-copy.ts +88 -0
  168. package/renderer/hooks/use-openapi-title.ts +44 -0
  169. package/renderer/lib/api-docs/agent/index.ts +6 -0
  170. package/renderer/lib/api-docs/agent/indexer.ts +323 -0
  171. package/renderer/lib/api-docs/agent/spec-summary.ts +335 -0
  172. package/renderer/lib/api-docs/agent/types.ts +116 -0
  173. package/renderer/lib/api-docs/auth/auth-context.tsx +225 -0
  174. package/renderer/lib/api-docs/auth/auth-storage.ts +87 -0
  175. package/renderer/lib/api-docs/auth/crypto.ts +89 -0
  176. package/renderer/lib/api-docs/auth/index.ts +4 -0
  177. package/renderer/lib/api-docs/code-editor/db.ts +164 -0
  178. package/renderer/lib/api-docs/code-editor/hooks.ts +266 -0
  179. package/renderer/lib/api-docs/code-editor/index.ts +6 -0
  180. package/renderer/lib/api-docs/code-editor/mode-context.tsx +207 -0
  181. package/renderer/lib/api-docs/code-editor/types.ts +105 -0
  182. package/renderer/lib/api-docs/codegen/definitions.ts +297 -0
  183. package/renderer/lib/api-docs/codegen/har.ts +251 -0
  184. package/renderer/lib/api-docs/codegen/index.ts +159 -0
  185. package/renderer/lib/api-docs/factories.ts +151 -0
  186. package/renderer/lib/api-docs/index.ts +17 -0
  187. package/renderer/lib/api-docs/mobile-context.tsx +112 -0
  188. package/renderer/lib/api-docs/navigation-context.tsx +88 -0
  189. package/renderer/lib/api-docs/parsers/graphql/README.md +129 -0
  190. package/renderer/lib/api-docs/parsers/graphql/index.ts +91 -0
  191. package/renderer/lib/api-docs/parsers/graphql/parser.ts +491 -0
  192. package/renderer/lib/api-docs/parsers/graphql/transformer.ts +246 -0
  193. package/renderer/lib/api-docs/parsers/graphql/types.ts +283 -0
  194. package/renderer/lib/api-docs/parsers/openapi/README.md +32 -0
  195. package/renderer/lib/api-docs/parsers/openapi/dereferencer.ts +60 -0
  196. package/renderer/lib/api-docs/parsers/openapi/extractors/auth.ts +574 -0
  197. package/renderer/lib/api-docs/parsers/openapi/extractors/body.ts +403 -0
  198. package/renderer/lib/api-docs/parsers/openapi/extractors/index.ts +232 -0
  199. package/renderer/lib/api-docs/parsers/openapi/index.ts +171 -0
  200. package/renderer/lib/api-docs/parsers/openapi/transformer.ts +277 -0
  201. package/renderer/lib/api-docs/parsers/openapi/validator.ts +31 -0
  202. package/renderer/lib/api-docs/playground/context.tsx +107 -0
  203. package/renderer/lib/api-docs/playground/navigation-context.tsx +124 -0
  204. package/renderer/lib/api-docs/playground/request-builder.ts +223 -0
  205. package/renderer/lib/api-docs/playground/request-runner.ts +282 -0
  206. package/renderer/lib/api-docs/playground/types.ts +35 -0
  207. package/renderer/lib/api-docs/types.ts +269 -0
  208. package/renderer/lib/api-docs/utils.ts +311 -0
  209. package/renderer/lib/cache.ts +193 -0
  210. package/renderer/lib/docs/config/index.ts +29 -0
  211. package/renderer/lib/docs/config/loader.ts +142 -0
  212. package/renderer/lib/docs/config/schema.ts +298 -0
  213. package/renderer/lib/docs/index.ts +12 -0
  214. package/renderer/lib/docs/mdx/compiler.ts +176 -0
  215. package/renderer/lib/docs/mdx/frontmatter.ts +80 -0
  216. package/renderer/lib/docs/mdx/index.ts +26 -0
  217. package/renderer/lib/docs/navigation/generator.ts +348 -0
  218. package/renderer/lib/docs/navigation/index.ts +12 -0
  219. package/renderer/lib/docs/navigation/types.ts +123 -0
  220. package/renderer/lib/docs-navigation-context.tsx +80 -0
  221. package/renderer/lib/multi-tenant/context.ts +105 -0
  222. package/renderer/lib/storage/blob.ts +845 -0
  223. package/renderer/lib/utils.ts +6 -0
  224. package/renderer/next.config.ts +76 -0
  225. package/renderer/package.json +66 -0
  226. package/renderer/postcss.config.mjs +5 -0
  227. package/renderer/public/assets/images/screenshot.png +0 -0
  228. package/renderer/public/assets/logo/dark.svg +9 -0
  229. package/renderer/public/assets/logo/light.svg +9 -0
  230. package/renderer/public/assets/logo.svg +9 -0
  231. package/renderer/public/file.svg +1 -0
  232. package/renderer/public/globe.svg +1 -0
  233. package/renderer/public/icon.png +0 -0
  234. package/renderer/public/logo.svg +9 -0
  235. package/renderer/public/window.svg +1 -0
  236. package/renderer/tsconfig.json +28 -0
  237. package/templates/basic/README.md +139 -0
  238. package/templates/basic/assets/favicon.svg +4 -0
  239. package/templates/basic/assets/logo.svg +9 -0
  240. package/templates/basic/docs.json +47 -0
  241. package/templates/basic/guides/configuration.mdx +149 -0
  242. package/templates/basic/guides/overview.mdx +96 -0
  243. package/templates/basic/index.mdx +39 -0
  244. package/templates/basic/package.json +14 -0
  245. package/templates/basic/quickstart.mdx +92 -0
  246. package/templates/basic/vercel.json +6 -0
  247. package/templates/graphql/README.md +139 -0
  248. package/templates/graphql/api-reference/schema.graphql +305 -0
  249. package/templates/graphql/assets/favicon.svg +4 -0
  250. package/templates/graphql/assets/logo.svg +9 -0
  251. package/templates/graphql/docs.json +54 -0
  252. package/templates/graphql/guides/configuration.mdx +149 -0
  253. package/templates/graphql/guides/overview.mdx +96 -0
  254. package/templates/graphql/index.mdx +39 -0
  255. package/templates/graphql/package.json +14 -0
  256. package/templates/graphql/quickstart.mdx +92 -0
  257. package/templates/graphql/vercel.json +6 -0
  258. package/templates/openapi/README.md +139 -0
  259. package/templates/openapi/api-reference/openapi.json +419 -0
  260. package/templates/openapi/assets/favicon.svg +4 -0
  261. package/templates/openapi/assets/logo.svg +9 -0
  262. package/templates/openapi/docs.json +61 -0
  263. package/templates/openapi/guides/configuration.mdx +149 -0
  264. package/templates/openapi/guides/overview.mdx +96 -0
  265. package/templates/openapi/index.mdx +39 -0
  266. package/templates/openapi/package.json +14 -0
  267. package/templates/openapi/quickstart.mdx +92 -0
  268. 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
+ }