@brainfish-ai/devdoc 0.1.41 → 0.1.43
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/ai-agents/.claude/skills/bootstrap-docs/SKILL.md +710 -79
- package/ai-agents/.claude/skills/check-docs/SKILL.md +83 -8
- package/ai-agents/.claude/skills/create-doc/SKILL.md +267 -55
- package/ai-agents/.claude/skills/update-doc/SKILL.md +162 -63
- package/ai-agents/.cursor/rules/devdoc-bootstrap.mdc +145 -15
- package/ai-agents/.cursor/rules/devdoc-create.mdc +108 -57
- package/ai-agents/.cursor/rules/devdoc-update.mdc +93 -70
- package/ai-agents/.cursor/rules/devdoc.mdc +21 -0
- package/ai-agents/schemas/docs.schema.json +332 -0
- package/ai-agents/schemas/theme.schema.json +243 -0
- package/dist/cli/commands/create.js +4 -9
- package/dist/cli/commands/deploy.js +50 -25
- package/dist/cli/commands/dev.js +19 -10
- package/package.json +3 -2
- package/renderer/app/api/assets/[...path]/route.js +108 -0
- package/renderer/app/api/assets/route.js +114 -0
- package/renderer/app/api/assets/upload/route.js +163 -0
- package/renderer/app/api/auth-schemes/route.js +58 -0
- package/renderer/app/api/chat/route.js +759 -0
- package/renderer/app/api/codegen/route.js +52 -0
- package/renderer/app/api/collections/route.js +675 -0
- package/renderer/app/api/debug/route.js +47 -0
- package/renderer/app/api/deploy/route.js +199 -0
- package/renderer/app/api/device/route.js +36 -0
- package/renderer/app/api/docs/route.js +205 -0
- package/renderer/app/api/domains/add/route.js +121 -0
- package/renderer/app/api/domains/lookup/route.js +43 -0
- package/renderer/app/api/domains/remove/route.js +89 -0
- package/renderer/app/api/domains/status/route.js +140 -0
- package/renderer/app/api/domains/verify/route.js +168 -0
- package/renderer/app/api/keys/regenerate/route.js +71 -0
- package/renderer/app/api/local-assets/[...path]/route.js +108 -0
- package/renderer/app/api/openapi-spec/route.js +73 -0
- package/renderer/app/api/projects/[slug]/route.js +129 -0
- package/renderer/app/api/projects/[slug]/stats/route.js +80 -0
- package/renderer/app/api/projects/register/route.js +176 -0
- package/renderer/app/api/proxy/route.js +139 -0
- package/renderer/app/api/proxy-stream/route.js +156 -0
- package/renderer/app/api/redirects/route.js +35 -0
- package/renderer/app/api/schema/route.js +85 -0
- package/renderer/app/api/subdomains/check/route.js +158 -0
- package/renderer/app/api/suggestions/route.js +175 -0
- package/renderer/app/layout.js +47 -0
- package/renderer/app/llms-full.txt/route.js +257 -0
- package/renderer/app/llms.txt/route.js +219 -0
- package/renderer/app/page.js +12 -0
- package/renderer/app/robots.txt/route.js +66 -0
- package/renderer/app/sitemap.xml/route.js +145 -0
- package/renderer/components/docs/index.js +8 -0
- package/renderer/components/docs/mdx/accordion.js +113 -0
- package/renderer/components/docs/mdx/badge.js +72 -0
- package/renderer/components/docs/mdx/callouts.js +137 -0
- package/renderer/components/docs/mdx/cards.js +175 -0
- package/renderer/components/docs/mdx/changelog.js +100 -0
- package/renderer/components/docs/mdx/code-block.js +147 -0
- package/renderer/components/docs/mdx/code-group.js +287 -0
- package/renderer/components/docs/mdx/file-embeds.js +82 -0
- package/renderer/components/docs/mdx/frame.js +59 -0
- package/renderer/components/docs/mdx/highlight.js +90 -0
- package/renderer/components/docs/mdx/iframe.js +69 -0
- package/renderer/components/docs/mdx/image.js +135 -0
- package/renderer/components/docs/mdx/index.js +134 -0
- package/renderer/components/docs/mdx/landing.js +315 -0
- package/renderer/components/docs/mdx/mermaid.js +212 -0
- package/renderer/components/docs/mdx/param-field.js +112 -0
- package/renderer/components/docs/mdx/steps.js +74 -0
- package/renderer/components/docs/mdx/tabs.js +50 -0
- package/renderer/components/docs/mdx-renderer.js +77 -0
- package/renderer/components/docs/navigation/breadcrumbs.js +64 -0
- package/renderer/components/docs/navigation/index.js +6 -0
- package/renderer/components/docs/navigation/page-nav.js +57 -0
- package/renderer/components/docs/navigation/sidebar.js +375 -0
- package/renderer/components/docs/navigation/toc.js +89 -0
- package/renderer/components/docs/notice.js +77 -0
- package/renderer/components/docs-header.js +202 -0
- package/renderer/components/docs-viewer/agent/agent-chat.js +1930 -0
- package/renderer/components/docs-viewer/agent/cards/debug-context-card.js +107 -0
- package/renderer/components/docs-viewer/agent/cards/endpoint-context-card.js +57 -0
- package/renderer/components/docs-viewer/agent/cards/index.js +45 -0
- package/renderer/components/docs-viewer/agent/cards/response-options-card.js +154 -0
- package/renderer/components/docs-viewer/agent/cards/types.js +22 -0
- package/renderer/components/docs-viewer/agent/chat-message.js +2 -0
- package/renderer/components/docs-viewer/agent/index.js +4 -0
- package/renderer/components/docs-viewer/agent/messages/assistant-message.js +108 -0
- package/renderer/components/docs-viewer/agent/messages/chat-message.js +34 -0
- package/renderer/components/docs-viewer/agent/messages/index.js +6 -0
- package/renderer/components/docs-viewer/agent/messages/tool-call-display.js +1065 -0
- package/renderer/components/docs-viewer/agent/messages/types.js +2 -0
- package/renderer/components/docs-viewer/agent/messages/typing-indicator.js +26 -0
- package/renderer/components/docs-viewer/agent/messages/user-message.js +37 -0
- package/renderer/components/docs-viewer/code-editor/{index.tsx → index.js} +1 -1
- package/renderer/components/docs-viewer/code-editor/notes-mode.js +1338 -0
- package/renderer/components/docs-viewer/content/changelog-page.js +297 -0
- package/renderer/components/docs-viewer/content/doc-page.js +264 -0
- package/renderer/components/docs-viewer/content/documentation-viewer.js +14 -0
- package/renderer/components/docs-viewer/content/index.js +29 -0
- package/renderer/components/docs-viewer/content/not-found-page.js +300 -0
- package/renderer/components/docs-viewer/content/request-details.js +528 -0
- package/renderer/components/docs-viewer/content/sections/auth.js +108 -0
- package/renderer/components/docs-viewer/content/sections/body.js +80 -0
- package/renderer/components/docs-viewer/content/sections/headers.js +64 -0
- package/renderer/components/docs-viewer/content/sections/overview.js +56 -0
- package/renderer/components/docs-viewer/content/sections/parameters.js +64 -0
- package/renderer/components/docs-viewer/content/sections/responses.js +91 -0
- package/renderer/components/docs-viewer/global-auth-modal.js +427 -0
- package/renderer/components/docs-viewer/index.js +1552 -0
- package/renderer/components/docs-viewer/playground/auth-editor.js +418 -0
- package/renderer/components/docs-viewer/playground/body-editor.js +240 -0
- package/renderer/components/docs-viewer/playground/code-editor.js +135 -0
- package/renderer/components/docs-viewer/playground/code-snippet.js +393 -0
- package/renderer/components/docs-viewer/playground/graphql-playground.js +734 -0
- package/renderer/components/docs-viewer/playground/index.js +682 -0
- package/renderer/components/docs-viewer/playground/key-value-editor.js +317 -0
- package/renderer/components/docs-viewer/playground/method-selector.js +65 -0
- package/renderer/components/docs-viewer/playground/request-builder.js +181 -0
- package/renderer/components/docs-viewer/playground/request-tabs.js +240 -0
- package/renderer/components/docs-viewer/playground/response-cards/idle-card.js +42 -0
- package/renderer/components/docs-viewer/playground/response-cards/index.js +72 -0
- package/renderer/components/docs-viewer/playground/response-cards/loading-card.js +24 -0
- package/renderer/components/docs-viewer/playground/response-cards/network-error-card.js +28 -0
- package/renderer/components/docs-viewer/playground/response-cards/response-body-card.js +308 -0
- package/renderer/components/docs-viewer/playground/response-cards/types.js +9 -0
- package/renderer/components/docs-viewer/playground/response-viewer.js +18 -0
- package/renderer/components/docs-viewer/search/index.js +2 -0
- package/renderer/components/docs-viewer/search/search-dialog.js +367 -0
- package/renderer/components/docs-viewer/search/use-search.js +89 -0
- package/renderer/components/docs-viewer/shared/markdown-renderer.js +423 -0
- package/renderer/components/docs-viewer/shared/method-badge.js +23 -0
- package/renderer/components/docs-viewer/shared/schema-viewer.js +321 -0
- package/renderer/components/docs-viewer/sidebar/collection-tree.js +222 -0
- package/renderer/components/docs-viewer/sidebar/endpoint-options.js +512 -0
- package/renderer/components/docs-viewer/sidebar/index.js +196 -0
- package/renderer/components/docs-viewer/sidebar/right-sidebar.js +163 -0
- package/renderer/components/docs-viewer/sidebar/sidebar-group.js +87 -0
- package/renderer/components/docs-viewer/sidebar/sidebar-item.js +172 -0
- package/renderer/components/docs-viewer/sidebar/sidebar-section.js +31 -0
- package/renderer/components/theme-provider.js +10 -0
- package/renderer/components/theme-toggle.js +106 -0
- package/renderer/components/ui/badge.js +29 -0
- package/renderer/components/ui/button.js +40 -0
- package/renderer/components/ui/dialog.js +50 -0
- package/renderer/components/ui/dropdown-menu.js +143 -0
- package/renderer/components/ui/input.js +12 -0
- package/renderer/components/ui/label.js +13 -0
- package/renderer/components/ui/navigation-menu.js +83 -0
- package/renderer/components/ui/select.js +116 -0
- package/renderer/components/ui/spinner.js +92 -0
- package/renderer/components/ui/tabs.js +34 -0
- package/renderer/components/ui/tooltip.js +43 -0
- package/renderer/hooks/use-code-copy.js +76 -0
- package/renderer/hooks/use-openapi-title.js +33 -0
- package/renderer/lib/api-docs/agent/index.js +4 -0
- package/renderer/lib/api-docs/agent/indexer.js +254 -0
- package/renderer/lib/api-docs/agent/spec-summary.js +227 -0
- package/renderer/lib/api-docs/agent/types.js +5 -0
- package/renderer/lib/api-docs/auth/auth-context.js +157 -0
- package/renderer/lib/api-docs/auth/auth-storage.js +66 -0
- package/renderer/lib/api-docs/auth/crypto.js +64 -0
- package/renderer/lib/api-docs/auth/index.js +3 -0
- package/renderer/lib/api-docs/code-editor/db.js +145 -0
- package/renderer/lib/api-docs/code-editor/hooks.js +254 -0
- package/renderer/lib/api-docs/code-editor/{index.ts → index.js} +3 -4
- package/renderer/lib/api-docs/code-editor/mode-context.js +154 -0
- package/renderer/lib/api-docs/code-editor/types.js +53 -0
- package/renderer/lib/api-docs/codegen/definitions.js +258 -0
- package/renderer/lib/api-docs/codegen/har.js +171 -0
- package/renderer/lib/api-docs/codegen/index.js +118 -0
- package/renderer/lib/api-docs/factories.js +136 -0
- package/renderer/lib/api-docs/{index.ts → index.js} +5 -10
- package/renderer/lib/api-docs/mobile-context.js +79 -0
- package/renderer/lib/api-docs/navigation-context.js +62 -0
- package/renderer/lib/api-docs/parsers/graphql/index.js +50 -0
- package/renderer/lib/api-docs/parsers/graphql/parser.js +350 -0
- package/renderer/lib/api-docs/parsers/graphql/transformer.js +215 -0
- package/renderer/lib/api-docs/parsers/graphql/types.js +46 -0
- package/renderer/lib/api-docs/parsers/openapi/dereferencer.js +43 -0
- package/renderer/lib/api-docs/parsers/openapi/extractors/auth.js +486 -0
- package/renderer/lib/api-docs/parsers/openapi/extractors/body.js +295 -0
- package/renderer/lib/api-docs/parsers/openapi/extractors/index.js +132 -0
- package/renderer/lib/api-docs/parsers/openapi/index.js +127 -0
- package/renderer/lib/api-docs/parsers/openapi/transformer.js +192 -0
- package/renderer/lib/api-docs/parsers/openapi/validator.js +24 -0
- package/renderer/lib/api-docs/playground/context.js +65 -0
- package/renderer/lib/api-docs/playground/navigation-context.js +74 -0
- package/renderer/lib/api-docs/playground/request-builder.js +163 -0
- package/renderer/lib/api-docs/playground/request-runner.js +224 -0
- package/renderer/lib/api-docs/playground/types.js +5 -0
- package/renderer/lib/api-docs/types.js +23 -0
- package/renderer/lib/api-docs/utils.js +212 -0
- package/renderer/lib/cache.js +157 -0
- package/renderer/lib/docs/config/domain-schema.js +161 -0
- package/renderer/lib/docs/config/index.js +5 -0
- package/renderer/lib/docs/config/loader.js +113 -0
- package/renderer/lib/docs/config/schema.js +269 -0
- package/renderer/lib/docs/index.js +8 -0
- package/renderer/lib/docs/mdx/compiler.js +128 -0
- package/renderer/lib/docs/mdx/frontmatter.js +73 -0
- package/renderer/lib/docs/mdx/index.js +8 -0
- package/renderer/lib/docs/navigation/generator.js +269 -0
- package/renderer/lib/docs/navigation/index.js +4 -0
- package/renderer/lib/docs/navigation/types.js +9 -0
- package/renderer/lib/docs-navigation-context.js +40 -0
- package/renderer/lib/multi-tenant/context.js +80 -0
- package/renderer/lib/storage/blob.js +767 -0
- package/renderer/lib/utils/icons.js +30 -0
- package/renderer/lib/utils.js +5 -0
- package/renderer/next.config.js +62 -0
- package/renderer/tsconfig.json +23 -5
- package/renderer/app/api/assets/[...path]/route.ts +0 -123
- package/renderer/app/api/assets/route.ts +0 -124
- package/renderer/app/api/assets/upload/route.ts +0 -177
- package/renderer/app/api/auth-schemes/route.ts +0 -77
- package/renderer/app/api/chat/route.ts +0 -858
- package/renderer/app/api/codegen/route.ts +0 -72
- package/renderer/app/api/collections/route.ts +0 -1002
- package/renderer/app/api/debug/route.ts +0 -53
- package/renderer/app/api/deploy/route.ts +0 -234
- package/renderer/app/api/device/route.ts +0 -42
- package/renderer/app/api/docs/route.ts +0 -201
- package/renderer/app/api/domains/add/route.ts +0 -132
- package/renderer/app/api/domains/lookup/route.ts +0 -43
- package/renderer/app/api/domains/remove/route.ts +0 -100
- package/renderer/app/api/domains/status/route.ts +0 -158
- package/renderer/app/api/domains/verify/route.ts +0 -181
- package/renderer/app/api/keys/regenerate/route.ts +0 -80
- package/renderer/app/api/local-assets/[...path]/route.ts +0 -122
- package/renderer/app/api/openapi-spec/route.ts +0 -151
- package/renderer/app/api/projects/[slug]/route.ts +0 -153
- package/renderer/app/api/projects/[slug]/stats/route.ts +0 -96
- package/renderer/app/api/projects/register/route.ts +0 -152
- package/renderer/app/api/proxy/route.ts +0 -149
- package/renderer/app/api/proxy-stream/route.ts +0 -168
- package/renderer/app/api/redirects/route.ts +0 -47
- package/renderer/app/api/schema/route.ts +0 -73
- package/renderer/app/api/subdomains/check/route.ts +0 -172
- package/renderer/app/api/suggestions/route.ts +0 -144
- package/renderer/app/layout.tsx +0 -54
- package/renderer/app/llms-full.txt/route.ts +0 -346
- package/renderer/app/llms.txt/route.ts +0 -279
- package/renderer/app/page.tsx +0 -14
- package/renderer/app/robots.txt/route.ts +0 -84
- package/renderer/app/sitemap.xml/route.ts +0 -199
- package/renderer/components/docs/index.ts +0 -12
- package/renderer/components/docs/mdx/accordion.tsx +0 -169
- package/renderer/components/docs/mdx/badge.tsx +0 -132
- package/renderer/components/docs/mdx/callouts.tsx +0 -154
- package/renderer/components/docs/mdx/cards.tsx +0 -241
- package/renderer/components/docs/mdx/changelog.tsx +0 -120
- package/renderer/components/docs/mdx/code-block.tsx +0 -186
- package/renderer/components/docs/mdx/code-group.tsx +0 -421
- package/renderer/components/docs/mdx/file-embeds.tsx +0 -105
- package/renderer/components/docs/mdx/frame.tsx +0 -112
- package/renderer/components/docs/mdx/highlight.tsx +0 -151
- package/renderer/components/docs/mdx/iframe.tsx +0 -134
- package/renderer/components/docs/mdx/image.tsx +0 -235
- package/renderer/components/docs/mdx/index.ts +0 -237
- package/renderer/components/docs/mdx/landing.tsx +0 -684
- package/renderer/components/docs/mdx/mermaid.tsx +0 -240
- package/renderer/components/docs/mdx/param-field.tsx +0 -200
- package/renderer/components/docs/mdx/steps.tsx +0 -113
- package/renderer/components/docs/mdx/tabs.tsx +0 -86
- package/renderer/components/docs/mdx-renderer.tsx +0 -100
- package/renderer/components/docs/navigation/breadcrumbs.tsx +0 -76
- package/renderer/components/docs/navigation/index.ts +0 -8
- package/renderer/components/docs/navigation/page-nav.tsx +0 -64
- package/renderer/components/docs/navigation/sidebar.tsx +0 -515
- package/renderer/components/docs/navigation/toc.tsx +0 -113
- package/renderer/components/docs/notice.tsx +0 -105
- package/renderer/components/docs-header.tsx +0 -278
- package/renderer/components/docs-viewer/agent/agent-chat.tsx +0 -2076
- package/renderer/components/docs-viewer/agent/cards/debug-context-card.tsx +0 -90
- package/renderer/components/docs-viewer/agent/cards/endpoint-context-card.tsx +0 -49
- package/renderer/components/docs-viewer/agent/cards/index.tsx +0 -50
- package/renderer/components/docs-viewer/agent/cards/response-options-card.tsx +0 -212
- package/renderer/components/docs-viewer/agent/cards/types.ts +0 -84
- package/renderer/components/docs-viewer/agent/chat-message.tsx +0 -17
- package/renderer/components/docs-viewer/agent/index.tsx +0 -6
- package/renderer/components/docs-viewer/agent/messages/assistant-message.tsx +0 -119
- package/renderer/components/docs-viewer/agent/messages/chat-message.tsx +0 -46
- package/renderer/components/docs-viewer/agent/messages/index.ts +0 -17
- package/renderer/components/docs-viewer/agent/messages/tool-call-display.tsx +0 -721
- package/renderer/components/docs-viewer/agent/messages/types.ts +0 -61
- package/renderer/components/docs-viewer/agent/messages/typing-indicator.tsx +0 -24
- package/renderer/components/docs-viewer/agent/messages/user-message.tsx +0 -51
- package/renderer/components/docs-viewer/code-editor/notes-mode.tsx +0 -1283
- package/renderer/components/docs-viewer/content/changelog-page.tsx +0 -331
- package/renderer/components/docs-viewer/content/doc-page.tsx +0 -367
- package/renderer/components/docs-viewer/content/documentation-viewer.tsx +0 -17
- package/renderer/components/docs-viewer/content/index.tsx +0 -29
- package/renderer/components/docs-viewer/content/not-found-page.tsx +0 -330
- package/renderer/components/docs-viewer/content/request-details.tsx +0 -330
- package/renderer/components/docs-viewer/content/sections/auth.tsx +0 -69
- package/renderer/components/docs-viewer/content/sections/body.tsx +0 -66
- package/renderer/components/docs-viewer/content/sections/headers.tsx +0 -43
- package/renderer/components/docs-viewer/content/sections/overview.tsx +0 -40
- package/renderer/components/docs-viewer/content/sections/parameters.tsx +0 -43
- package/renderer/components/docs-viewer/content/sections/responses.tsx +0 -87
- package/renderer/components/docs-viewer/global-auth-modal.tsx +0 -352
- package/renderer/components/docs-viewer/index.tsx +0 -1662
- package/renderer/components/docs-viewer/playground/auth-editor.tsx +0 -280
- package/renderer/components/docs-viewer/playground/body-editor.tsx +0 -221
- package/renderer/components/docs-viewer/playground/code-editor.tsx +0 -224
- package/renderer/components/docs-viewer/playground/code-snippet.tsx +0 -387
- package/renderer/components/docs-viewer/playground/graphql-playground.tsx +0 -745
- package/renderer/components/docs-viewer/playground/index.tsx +0 -671
- package/renderer/components/docs-viewer/playground/key-value-editor.tsx +0 -261
- package/renderer/components/docs-viewer/playground/method-selector.tsx +0 -60
- package/renderer/components/docs-viewer/playground/request-builder.tsx +0 -179
- package/renderer/components/docs-viewer/playground/request-tabs.tsx +0 -237
- package/renderer/components/docs-viewer/playground/response-cards/idle-card.tsx +0 -21
- package/renderer/components/docs-viewer/playground/response-cards/index.tsx +0 -93
- package/renderer/components/docs-viewer/playground/response-cards/loading-card.tsx +0 -16
- package/renderer/components/docs-viewer/playground/response-cards/network-error-card.tsx +0 -23
- package/renderer/components/docs-viewer/playground/response-cards/response-body-card.tsx +0 -268
- package/renderer/components/docs-viewer/playground/response-cards/types.ts +0 -82
- package/renderer/components/docs-viewer/playground/response-viewer.tsx +0 -43
- package/renderer/components/docs-viewer/search/index.ts +0 -2
- package/renderer/components/docs-viewer/search/search-dialog.tsx +0 -331
- package/renderer/components/docs-viewer/search/use-search.ts +0 -117
- package/renderer/components/docs-viewer/shared/markdown-renderer.tsx +0 -431
- package/renderer/components/docs-viewer/shared/method-badge.tsx +0 -41
- package/renderer/components/docs-viewer/shared/schema-viewer.tsx +0 -349
- package/renderer/components/docs-viewer/sidebar/collection-tree.tsx +0 -259
- package/renderer/components/docs-viewer/sidebar/endpoint-options.tsx +0 -316
- package/renderer/components/docs-viewer/sidebar/index.tsx +0 -282
- package/renderer/components/docs-viewer/sidebar/right-sidebar.tsx +0 -202
- package/renderer/components/docs-viewer/sidebar/sidebar-group.tsx +0 -118
- package/renderer/components/docs-viewer/sidebar/sidebar-item.tsx +0 -212
- package/renderer/components/docs-viewer/sidebar/sidebar-section.tsx +0 -38
- package/renderer/components/theme-provider.tsx +0 -11
- package/renderer/components/theme-toggle.tsx +0 -76
- package/renderer/components/ui/badge.tsx +0 -46
- package/renderer/components/ui/button.tsx +0 -59
- package/renderer/components/ui/dialog.tsx +0 -118
- package/renderer/components/ui/dropdown-menu.tsx +0 -257
- package/renderer/components/ui/input.tsx +0 -21
- package/renderer/components/ui/label.tsx +0 -24
- package/renderer/components/ui/navigation-menu.tsx +0 -168
- package/renderer/components/ui/select.tsx +0 -190
- package/renderer/components/ui/spinner.tsx +0 -114
- package/renderer/components/ui/tabs.tsx +0 -66
- package/renderer/components/ui/tooltip.tsx +0 -61
- package/renderer/hooks/use-code-copy.ts +0 -88
- package/renderer/hooks/use-openapi-title.ts +0 -44
- package/renderer/lib/api-docs/agent/index.ts +0 -6
- package/renderer/lib/api-docs/agent/indexer.ts +0 -323
- package/renderer/lib/api-docs/agent/spec-summary.ts +0 -335
- package/renderer/lib/api-docs/agent/types.ts +0 -116
- package/renderer/lib/api-docs/auth/auth-context.tsx +0 -225
- package/renderer/lib/api-docs/auth/auth-storage.ts +0 -87
- package/renderer/lib/api-docs/auth/crypto.ts +0 -89
- package/renderer/lib/api-docs/auth/index.ts +0 -4
- package/renderer/lib/api-docs/code-editor/db.ts +0 -164
- package/renderer/lib/api-docs/code-editor/hooks.ts +0 -266
- package/renderer/lib/api-docs/code-editor/mode-context.tsx +0 -207
- package/renderer/lib/api-docs/code-editor/types.ts +0 -105
- package/renderer/lib/api-docs/codegen/definitions.ts +0 -297
- package/renderer/lib/api-docs/codegen/har.ts +0 -251
- package/renderer/lib/api-docs/codegen/index.ts +0 -159
- package/renderer/lib/api-docs/factories.ts +0 -170
- package/renderer/lib/api-docs/mobile-context.tsx +0 -112
- package/renderer/lib/api-docs/navigation-context.tsx +0 -88
- package/renderer/lib/api-docs/parsers/graphql/README.md +0 -129
- package/renderer/lib/api-docs/parsers/graphql/index.ts +0 -91
- package/renderer/lib/api-docs/parsers/graphql/parser.ts +0 -491
- package/renderer/lib/api-docs/parsers/graphql/transformer.ts +0 -246
- package/renderer/lib/api-docs/parsers/graphql/types.ts +0 -283
- package/renderer/lib/api-docs/parsers/openapi/README.md +0 -32
- package/renderer/lib/api-docs/parsers/openapi/dereferencer.ts +0 -60
- package/renderer/lib/api-docs/parsers/openapi/extractors/auth.ts +0 -574
- package/renderer/lib/api-docs/parsers/openapi/extractors/body.ts +0 -403
- package/renderer/lib/api-docs/parsers/openapi/extractors/index.ts +0 -232
- package/renderer/lib/api-docs/parsers/openapi/index.ts +0 -171
- package/renderer/lib/api-docs/parsers/openapi/transformer.ts +0 -278
- package/renderer/lib/api-docs/parsers/openapi/validator.ts +0 -31
- package/renderer/lib/api-docs/playground/context.tsx +0 -107
- package/renderer/lib/api-docs/playground/navigation-context.tsx +0 -124
- package/renderer/lib/api-docs/playground/request-builder.ts +0 -223
- package/renderer/lib/api-docs/playground/request-runner.ts +0 -282
- package/renderer/lib/api-docs/playground/types.ts +0 -35
- package/renderer/lib/api-docs/types.ts +0 -269
- package/renderer/lib/api-docs/utils.ts +0 -311
- package/renderer/lib/cache.ts +0 -193
- package/renderer/lib/docs/config/domain-schema.ts +0 -260
- package/renderer/lib/docs/config/index.ts +0 -43
- package/renderer/lib/docs/config/loader.ts +0 -142
- package/renderer/lib/docs/config/schema.ts +0 -308
- package/renderer/lib/docs/index.ts +0 -12
- package/renderer/lib/docs/mdx/compiler.ts +0 -176
- package/renderer/lib/docs/mdx/frontmatter.ts +0 -91
- package/renderer/lib/docs/mdx/index.ts +0 -26
- package/renderer/lib/docs/navigation/generator.ts +0 -348
- package/renderer/lib/docs/navigation/index.ts +0 -12
- package/renderer/lib/docs/navigation/types.ts +0 -123
- package/renderer/lib/docs-navigation-context.tsx +0 -80
- package/renderer/lib/multi-tenant/context.ts +0 -105
- package/renderer/lib/storage/blob.ts +0 -1083
- package/renderer/lib/utils/icons.ts +0 -48
- package/renderer/lib/utils.ts +0 -6
- package/renderer/next.config.ts +0 -76
|
@@ -0,0 +1,767 @@
|
|
|
1
|
+
import { put, list, del, head } from '@vercel/blob';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
// Check if we're in local development mode (no blob token)
|
|
5
|
+
const IS_LOCAL_DEV = !process.env.BLOB_READ_WRITE_TOKEN;
|
|
6
|
+
// Local storage directory for development
|
|
7
|
+
const LOCAL_STORAGE_DIR = path.join(process.cwd(), '.devdoc-storage');
|
|
8
|
+
/**
|
|
9
|
+
* Generate a unique project slug
|
|
10
|
+
*/ export function generateProjectSlug(name) {
|
|
11
|
+
const base = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').substring(0, 30);
|
|
12
|
+
const suffix = Math.random().toString(36).substring(2, 8);
|
|
13
|
+
return `${base}-${suffix}`;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Get the blob path for a project's content
|
|
17
|
+
*/ function getProjectBlobPath(slug) {
|
|
18
|
+
return `projects/${slug}/content.json`;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Get the blob path for a project's metadata
|
|
22
|
+
*/ function getProjectMetadataPath(slug) {
|
|
23
|
+
return `projects/${slug}/metadata.json`;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Get the blob path for individual files (reserved for future use)
|
|
27
|
+
*/ // eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
28
|
+
function _getFileBlobPath(slug, filePath) {
|
|
29
|
+
return `projects/${slug}/files/${filePath}`;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Store project content in Vercel Blob (or local filesystem in dev)
|
|
33
|
+
*/ export async function storeProjectContent(slug, name, docsJson, files) {
|
|
34
|
+
const now = new Date().toISOString();
|
|
35
|
+
const content = {
|
|
36
|
+
slug,
|
|
37
|
+
name,
|
|
38
|
+
docsJson: JSON.stringify(docsJson),
|
|
39
|
+
files,
|
|
40
|
+
createdAt: now,
|
|
41
|
+
updatedAt: now
|
|
42
|
+
};
|
|
43
|
+
// Use local filesystem in development
|
|
44
|
+
if (IS_LOCAL_DEV) {
|
|
45
|
+
const projectDir = path.join(LOCAL_STORAGE_DIR, 'projects', slug);
|
|
46
|
+
fs.mkdirSync(projectDir, {
|
|
47
|
+
recursive: true
|
|
48
|
+
});
|
|
49
|
+
const contentPath = path.join(projectDir, 'content.json');
|
|
50
|
+
fs.writeFileSync(contentPath, JSON.stringify(content, null, 2));
|
|
51
|
+
const metadata = {
|
|
52
|
+
slug,
|
|
53
|
+
name,
|
|
54
|
+
createdAt: now,
|
|
55
|
+
updatedAt: now,
|
|
56
|
+
blobUrl: `file://${contentPath}`
|
|
57
|
+
};
|
|
58
|
+
fs.writeFileSync(path.join(projectDir, 'metadata.json'), JSON.stringify(metadata, null, 2));
|
|
59
|
+
return {
|
|
60
|
+
url: `file://${contentPath}`,
|
|
61
|
+
slug
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
// Store main content bundle in Vercel Blob
|
|
65
|
+
const contentBlob = await put(getProjectBlobPath(slug), JSON.stringify(content), {
|
|
66
|
+
access: 'public',
|
|
67
|
+
contentType: 'application/json',
|
|
68
|
+
allowOverwrite: true
|
|
69
|
+
});
|
|
70
|
+
// Store metadata separately for quick lookups
|
|
71
|
+
const metadata = {
|
|
72
|
+
slug,
|
|
73
|
+
name,
|
|
74
|
+
createdAt: now,
|
|
75
|
+
updatedAt: now,
|
|
76
|
+
blobUrl: contentBlob.url
|
|
77
|
+
};
|
|
78
|
+
await put(getProjectMetadataPath(slug), JSON.stringify(metadata), {
|
|
79
|
+
access: 'public',
|
|
80
|
+
contentType: 'application/json',
|
|
81
|
+
allowOverwrite: true
|
|
82
|
+
});
|
|
83
|
+
return {
|
|
84
|
+
url: contentBlob.url,
|
|
85
|
+
slug
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Update existing project content
|
|
90
|
+
*/ export async function updateProjectContent(slug, docsJson, files) {
|
|
91
|
+
// Get existing content to preserve createdAt
|
|
92
|
+
const existing = await getProjectContent(slug);
|
|
93
|
+
const now = new Date().toISOString();
|
|
94
|
+
const content = {
|
|
95
|
+
slug,
|
|
96
|
+
name: existing?.name || slug,
|
|
97
|
+
docsJson: JSON.stringify(docsJson),
|
|
98
|
+
files,
|
|
99
|
+
createdAt: existing?.createdAt || now,
|
|
100
|
+
updatedAt: now
|
|
101
|
+
};
|
|
102
|
+
// Use local filesystem in development
|
|
103
|
+
if (IS_LOCAL_DEV) {
|
|
104
|
+
const projectDir = path.join(LOCAL_STORAGE_DIR, 'projects', slug);
|
|
105
|
+
fs.mkdirSync(projectDir, {
|
|
106
|
+
recursive: true
|
|
107
|
+
});
|
|
108
|
+
const contentPath = path.join(projectDir, 'content.json');
|
|
109
|
+
fs.writeFileSync(contentPath, JSON.stringify(content, null, 2));
|
|
110
|
+
const metadata = {
|
|
111
|
+
slug,
|
|
112
|
+
name: content.name,
|
|
113
|
+
createdAt: content.createdAt,
|
|
114
|
+
updatedAt: now,
|
|
115
|
+
blobUrl: `file://${contentPath}`
|
|
116
|
+
};
|
|
117
|
+
fs.writeFileSync(path.join(projectDir, 'metadata.json'), JSON.stringify(metadata, null, 2));
|
|
118
|
+
return {
|
|
119
|
+
url: `file://${contentPath}`
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
// Overwrite content in Vercel Blob
|
|
123
|
+
const contentBlob = await put(getProjectBlobPath(slug), JSON.stringify(content), {
|
|
124
|
+
access: 'public',
|
|
125
|
+
contentType: 'application/json',
|
|
126
|
+
allowOverwrite: true
|
|
127
|
+
});
|
|
128
|
+
// Update metadata
|
|
129
|
+
const metadata = {
|
|
130
|
+
slug,
|
|
131
|
+
name: content.name,
|
|
132
|
+
createdAt: content.createdAt,
|
|
133
|
+
updatedAt: now,
|
|
134
|
+
blobUrl: contentBlob.url
|
|
135
|
+
};
|
|
136
|
+
await put(getProjectMetadataPath(slug), JSON.stringify(metadata), {
|
|
137
|
+
access: 'public',
|
|
138
|
+
contentType: 'application/json',
|
|
139
|
+
allowOverwrite: true
|
|
140
|
+
});
|
|
141
|
+
return {
|
|
142
|
+
url: contentBlob.url
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Get project content from Vercel Blob (or local filesystem in dev)
|
|
147
|
+
*/ export async function getProjectContent(slug) {
|
|
148
|
+
try {
|
|
149
|
+
// Use local filesystem in development
|
|
150
|
+
if (IS_LOCAL_DEV) {
|
|
151
|
+
const contentPath = path.join(LOCAL_STORAGE_DIR, 'projects', slug, 'content.json');
|
|
152
|
+
if (!fs.existsSync(contentPath)) {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
const data = fs.readFileSync(contentPath, 'utf-8');
|
|
156
|
+
return JSON.parse(data);
|
|
157
|
+
}
|
|
158
|
+
const blobPath = getProjectBlobPath(slug);
|
|
159
|
+
const blobInfo = await head(blobPath);
|
|
160
|
+
if (!blobInfo) {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
const response = await fetch(blobInfo.url);
|
|
164
|
+
if (!response.ok) {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
const content = await response.json();
|
|
168
|
+
return content;
|
|
169
|
+
} catch (error) {
|
|
170
|
+
console.error(`[Blob] Error fetching project ${slug}:`, error);
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Get project metadata (quick lookup without full content)
|
|
176
|
+
*/ export async function getProjectMetadata(slug) {
|
|
177
|
+
try {
|
|
178
|
+
const blobPath = getProjectMetadataPath(slug);
|
|
179
|
+
const blobInfo = await head(blobPath);
|
|
180
|
+
if (!blobInfo) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
const response = await fetch(blobInfo.url);
|
|
184
|
+
if (!response.ok) {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
const metadata = await response.json();
|
|
188
|
+
return metadata;
|
|
189
|
+
} catch (error) {
|
|
190
|
+
console.error(`[Blob] Error fetching metadata for ${slug}:`, error);
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Get a specific file from project content
|
|
196
|
+
*/ export async function getProjectFile(slug, filePath) {
|
|
197
|
+
const content = await getProjectContent(slug);
|
|
198
|
+
if (!content) {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
const file = content.files.find((f)=>f.path === filePath);
|
|
202
|
+
return file?.content || null;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Get docs.json for a project
|
|
206
|
+
*/ export async function getProjectDocsJson(slug) {
|
|
207
|
+
const content = await getProjectContent(slug);
|
|
208
|
+
if (!content) {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
try {
|
|
212
|
+
return JSON.parse(content.docsJson);
|
|
213
|
+
} catch {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Check if a project exists
|
|
219
|
+
*/ export async function projectExists(slug) {
|
|
220
|
+
try {
|
|
221
|
+
// Use local filesystem in development
|
|
222
|
+
if (IS_LOCAL_DEV) {
|
|
223
|
+
const projectDir = path.join(LOCAL_STORAGE_DIR, 'projects', slug);
|
|
224
|
+
// Check for either metadata.json (full project) or apikey.json (registered project)
|
|
225
|
+
const metadataPath = path.join(projectDir, 'metadata.json');
|
|
226
|
+
const apiKeyPath = path.join(projectDir, 'apikey.json');
|
|
227
|
+
return fs.existsSync(metadataPath) || fs.existsSync(apiKeyPath);
|
|
228
|
+
}
|
|
229
|
+
// In production, check both metadata and apikey paths
|
|
230
|
+
const metadataPath = getProjectMetadataPath(slug);
|
|
231
|
+
const apiKeyPath = getApiKeyBlobPath(slug);
|
|
232
|
+
const [metadataInfo, apiKeyInfo] = await Promise.all([
|
|
233
|
+
head(metadataPath).catch(()=>null),
|
|
234
|
+
head(apiKeyPath).catch(()=>null)
|
|
235
|
+
]);
|
|
236
|
+
return metadataInfo !== null || apiKeyInfo !== null;
|
|
237
|
+
} catch {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Delete a project and all its content
|
|
243
|
+
*/ export async function deleteProject(slug) {
|
|
244
|
+
try {
|
|
245
|
+
// List all blobs for this project
|
|
246
|
+
const { blobs } = await list({
|
|
247
|
+
prefix: `projects/${slug}/`
|
|
248
|
+
});
|
|
249
|
+
// Delete all blobs
|
|
250
|
+
for (const blob of blobs){
|
|
251
|
+
await del(blob.url);
|
|
252
|
+
}
|
|
253
|
+
} catch (error) {
|
|
254
|
+
console.error(`[Blob] Error deleting project ${slug}:`, error);
|
|
255
|
+
throw error;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* List all projects (for admin purposes)
|
|
260
|
+
*/ export async function listProjects() {
|
|
261
|
+
try {
|
|
262
|
+
const { blobs } = await list({
|
|
263
|
+
prefix: 'projects/'
|
|
264
|
+
});
|
|
265
|
+
// Filter for metadata files only
|
|
266
|
+
const metadataBlobs = blobs.filter((b)=>b.pathname.endsWith('/metadata.json'));
|
|
267
|
+
const projects = [];
|
|
268
|
+
for (const blob of metadataBlobs){
|
|
269
|
+
try {
|
|
270
|
+
const response = await fetch(blob.url);
|
|
271
|
+
if (response.ok) {
|
|
272
|
+
const metadata = await response.json();
|
|
273
|
+
projects.push(metadata);
|
|
274
|
+
}
|
|
275
|
+
} catch {
|
|
276
|
+
// Skip invalid entries
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return projects;
|
|
280
|
+
} catch (error) {
|
|
281
|
+
console.error('[Blob] Error listing projects:', error);
|
|
282
|
+
return [];
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Get the blob path for a project's API key
|
|
287
|
+
*/ function getApiKeyBlobPath(slug) {
|
|
288
|
+
return `projects/${slug}/apikey.json`;
|
|
289
|
+
}
|
|
290
|
+
const REGISTRY_PATH = 'registry/domains.json';
|
|
291
|
+
const LOCAL_REGISTRY_PATH = path.join(LOCAL_STORAGE_DIR, 'registry', 'domains.json');
|
|
292
|
+
/**
|
|
293
|
+
* Get the domain registry (cached in memory for performance)
|
|
294
|
+
*/ let registryCache = null;
|
|
295
|
+
let registryCacheTime = 0;
|
|
296
|
+
const CACHE_TTL = 5000 // 5 seconds
|
|
297
|
+
;
|
|
298
|
+
async function getRegistry() {
|
|
299
|
+
const now = Date.now();
|
|
300
|
+
// Return cached if fresh
|
|
301
|
+
if (registryCache && now - registryCacheTime < CACHE_TTL) {
|
|
302
|
+
return registryCache;
|
|
303
|
+
}
|
|
304
|
+
try {
|
|
305
|
+
if (IS_LOCAL_DEV) {
|
|
306
|
+
if (fs.existsSync(LOCAL_REGISTRY_PATH)) {
|
|
307
|
+
const data = fs.readFileSync(LOCAL_REGISTRY_PATH, 'utf-8');
|
|
308
|
+
registryCache = JSON.parse(data);
|
|
309
|
+
registryCacheTime = now;
|
|
310
|
+
return registryCache;
|
|
311
|
+
}
|
|
312
|
+
} else {
|
|
313
|
+
const blobInfo = await head(REGISTRY_PATH).catch(()=>null);
|
|
314
|
+
if (blobInfo) {
|
|
315
|
+
const response = await fetch(blobInfo.url);
|
|
316
|
+
if (response.ok) {
|
|
317
|
+
registryCache = await response.json();
|
|
318
|
+
registryCacheTime = now;
|
|
319
|
+
return registryCache;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
} catch (error) {
|
|
324
|
+
console.error('[Registry] Error loading registry:', error);
|
|
325
|
+
}
|
|
326
|
+
// Return empty registry if not found
|
|
327
|
+
return {
|
|
328
|
+
domains: {},
|
|
329
|
+
customDomains: {},
|
|
330
|
+
projectToCustomDomain: {},
|
|
331
|
+
apiKeys: {},
|
|
332
|
+
updatedAt: new Date().toISOString()
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Save the domain registry
|
|
337
|
+
*/ async function saveRegistry(registry) {
|
|
338
|
+
registry.updatedAt = new Date().toISOString();
|
|
339
|
+
if (IS_LOCAL_DEV) {
|
|
340
|
+
const dir = path.dirname(LOCAL_REGISTRY_PATH);
|
|
341
|
+
fs.mkdirSync(dir, {
|
|
342
|
+
recursive: true
|
|
343
|
+
});
|
|
344
|
+
fs.writeFileSync(LOCAL_REGISTRY_PATH, JSON.stringify(registry, null, 2));
|
|
345
|
+
} else {
|
|
346
|
+
await put(REGISTRY_PATH, JSON.stringify(registry), {
|
|
347
|
+
access: 'public',
|
|
348
|
+
contentType: 'application/json',
|
|
349
|
+
allowOverwrite: true
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
// Update cache
|
|
353
|
+
registryCache = registry;
|
|
354
|
+
registryCacheTime = Date.now();
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Check if a subdomain is registered (O(1) lookup)
|
|
358
|
+
*/ export async function isSubdomainRegistered(subdomain) {
|
|
359
|
+
const registry = await getRegistry();
|
|
360
|
+
return subdomain in registry.domains;
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Register a new subdomain in the registry
|
|
364
|
+
*/ export async function registerSubdomain(subdomain, projectId, name, apiKey) {
|
|
365
|
+
const registry = await getRegistry();
|
|
366
|
+
const now = new Date().toISOString();
|
|
367
|
+
registry.domains[subdomain] = {
|
|
368
|
+
subdomain,
|
|
369
|
+
projectId,
|
|
370
|
+
name,
|
|
371
|
+
createdAt: now
|
|
372
|
+
};
|
|
373
|
+
registry.apiKeys[apiKey] = subdomain;
|
|
374
|
+
await saveRegistry(registry);
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Validate API key and get subdomain (O(1) lookup)
|
|
378
|
+
*/ export async function validateApiKeyFromRegistry(apiKey) {
|
|
379
|
+
if (!apiKey || !apiKey.startsWith('sk_live_') || apiKey.length !== 40) {
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
const registry = await getRegistry();
|
|
383
|
+
return registry.apiKeys[apiKey] || null;
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Get domain entry from registry
|
|
387
|
+
*/ export async function getDomainEntry(subdomain) {
|
|
388
|
+
const registry = await getRegistry();
|
|
389
|
+
return registry.domains[subdomain] || null;
|
|
390
|
+
}
|
|
391
|
+
// =============================================================================
|
|
392
|
+
// Custom Domain Management - One custom domain per project (free)
|
|
393
|
+
// =============================================================================
|
|
394
|
+
/**
|
|
395
|
+
* Generate a verification token for domain ownership
|
|
396
|
+
*/ export function generateVerificationToken() {
|
|
397
|
+
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
|
398
|
+
let token = 'devdoc-verify=';
|
|
399
|
+
for(let i = 0; i < 24; i++){
|
|
400
|
+
token += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
401
|
+
}
|
|
402
|
+
return token;
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Check if a custom domain is already registered (O(1) lookup)
|
|
406
|
+
*/ export async function isCustomDomainRegistered(customDomain) {
|
|
407
|
+
const registry = await getRegistry();
|
|
408
|
+
return customDomain in (registry.customDomains || {});
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Check if a project already has a custom domain (O(1) lookup)
|
|
412
|
+
*/ export async function getProjectCustomDomain(projectSlug) {
|
|
413
|
+
const registry = await getRegistry();
|
|
414
|
+
const customDomain = registry.projectToCustomDomain?.[projectSlug];
|
|
415
|
+
if (!customDomain) return null;
|
|
416
|
+
return registry.customDomains?.[customDomain] || null;
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Get custom domain entry by domain name (O(1) lookup)
|
|
420
|
+
*/ export async function getCustomDomainEntry(customDomain) {
|
|
421
|
+
const registry = await getRegistry();
|
|
422
|
+
return registry.customDomains?.[customDomain] || null;
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Look up project slug from custom domain (O(1) lookup)
|
|
426
|
+
* Used by middleware for routing
|
|
427
|
+
*/ export async function lookupCustomDomain(customDomain) {
|
|
428
|
+
const registry = await getRegistry();
|
|
429
|
+
const entry = registry.customDomains?.[customDomain];
|
|
430
|
+
// Only return if domain is active
|
|
431
|
+
if (entry && entry.status === 'active') {
|
|
432
|
+
return entry;
|
|
433
|
+
}
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Add a custom domain to a project
|
|
438
|
+
* Returns error if project already has a custom domain (limit: 1 per project)
|
|
439
|
+
*/ export async function addCustomDomain(projectSlug, customDomain) {
|
|
440
|
+
const registry = await getRegistry();
|
|
441
|
+
// Initialize custom domain maps if they don't exist
|
|
442
|
+
if (!registry.customDomains) {
|
|
443
|
+
registry.customDomains = {};
|
|
444
|
+
}
|
|
445
|
+
if (!registry.projectToCustomDomain) {
|
|
446
|
+
registry.projectToCustomDomain = {};
|
|
447
|
+
}
|
|
448
|
+
// Check if project already has a custom domain (limit: 1 per project)
|
|
449
|
+
const existingDomain = registry.projectToCustomDomain[projectSlug];
|
|
450
|
+
if (existingDomain) {
|
|
451
|
+
return {
|
|
452
|
+
success: false,
|
|
453
|
+
error: `Project already has a custom domain: ${existingDomain}. Remove it first before adding a new one.`
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
// Check if this domain is already registered to another project
|
|
457
|
+
if (registry.customDomains[customDomain]) {
|
|
458
|
+
return {
|
|
459
|
+
success: false,
|
|
460
|
+
error: `Domain ${customDomain} is already registered to another project.`
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
// Verify the project exists
|
|
464
|
+
const projectExists = await getDomainEntry(projectSlug);
|
|
465
|
+
if (!projectExists) {
|
|
466
|
+
return {
|
|
467
|
+
success: false,
|
|
468
|
+
error: `Project ${projectSlug} not found. Deploy your project first.`
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
const now = new Date().toISOString();
|
|
472
|
+
const verificationToken = generateVerificationToken();
|
|
473
|
+
const entry = {
|
|
474
|
+
customDomain,
|
|
475
|
+
projectSlug,
|
|
476
|
+
status: 'pending',
|
|
477
|
+
verificationToken,
|
|
478
|
+
createdAt: now
|
|
479
|
+
};
|
|
480
|
+
// Add to both lookups
|
|
481
|
+
registry.customDomains[customDomain] = entry;
|
|
482
|
+
registry.projectToCustomDomain[projectSlug] = customDomain;
|
|
483
|
+
await saveRegistry(registry);
|
|
484
|
+
return {
|
|
485
|
+
success: true,
|
|
486
|
+
entry
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Update custom domain status
|
|
491
|
+
*/ export async function updateCustomDomainStatus(customDomain, status, additionalFields) {
|
|
492
|
+
const registry = await getRegistry();
|
|
493
|
+
if (!registry.customDomains?.[customDomain]) {
|
|
494
|
+
return false;
|
|
495
|
+
}
|
|
496
|
+
registry.customDomains[customDomain] = {
|
|
497
|
+
...registry.customDomains[customDomain],
|
|
498
|
+
status,
|
|
499
|
+
lastCheckedAt: new Date().toISOString(),
|
|
500
|
+
...additionalFields
|
|
501
|
+
};
|
|
502
|
+
// Set verifiedAt when transitioning to dns_verified
|
|
503
|
+
if (status === 'dns_verified' && !registry.customDomains[customDomain].verifiedAt) {
|
|
504
|
+
registry.customDomains[customDomain].verifiedAt = new Date().toISOString();
|
|
505
|
+
}
|
|
506
|
+
await saveRegistry(registry);
|
|
507
|
+
return true;
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Remove a custom domain from a project
|
|
511
|
+
*/ export async function removeCustomDomain(customDomain, projectSlug) {
|
|
512
|
+
const registry = await getRegistry();
|
|
513
|
+
// Check if domain exists and belongs to this project
|
|
514
|
+
const entry = registry.customDomains?.[customDomain];
|
|
515
|
+
if (!entry) {
|
|
516
|
+
return {
|
|
517
|
+
success: false,
|
|
518
|
+
error: `Domain ${customDomain} not found.`
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
if (entry.projectSlug !== projectSlug) {
|
|
522
|
+
return {
|
|
523
|
+
success: false,
|
|
524
|
+
error: `Domain ${customDomain} does not belong to this project.`
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
// Remove from both lookups
|
|
528
|
+
delete registry.customDomains[customDomain];
|
|
529
|
+
delete registry.projectToCustomDomain[projectSlug];
|
|
530
|
+
await saveRegistry(registry);
|
|
531
|
+
return {
|
|
532
|
+
success: true
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* List all custom domains (for admin purposes)
|
|
537
|
+
*/ export async function listCustomDomains() {
|
|
538
|
+
const registry = await getRegistry();
|
|
539
|
+
return Object.values(registry.customDomains || {});
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* Generate a secure API key
|
|
543
|
+
*/ export function generateApiKey() {
|
|
544
|
+
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
|
545
|
+
let key = 'sk_live_';
|
|
546
|
+
for(let i = 0; i < 32; i++){
|
|
547
|
+
key += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
548
|
+
}
|
|
549
|
+
return key;
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* Store API key for a project
|
|
553
|
+
*/ export async function storeProjectApiKey(slug, apiKey) {
|
|
554
|
+
const keyData = {
|
|
555
|
+
key: apiKey,
|
|
556
|
+
slug,
|
|
557
|
+
createdAt: new Date().toISOString()
|
|
558
|
+
};
|
|
559
|
+
// Use local filesystem in development
|
|
560
|
+
if (IS_LOCAL_DEV) {
|
|
561
|
+
const projectDir = path.join(LOCAL_STORAGE_DIR, 'projects', slug);
|
|
562
|
+
fs.mkdirSync(projectDir, {
|
|
563
|
+
recursive: true
|
|
564
|
+
});
|
|
565
|
+
fs.writeFileSync(path.join(projectDir, 'apikey.json'), JSON.stringify(keyData, null, 2));
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
await put(getApiKeyBlobPath(slug), JSON.stringify(keyData), {
|
|
569
|
+
access: 'public',
|
|
570
|
+
contentType: 'application/json',
|
|
571
|
+
allowOverwrite: true
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* Get API key data for a project
|
|
576
|
+
*/ export async function getProjectApiKey(slug) {
|
|
577
|
+
try {
|
|
578
|
+
// Use local filesystem in development
|
|
579
|
+
if (IS_LOCAL_DEV) {
|
|
580
|
+
const keyPath = path.join(LOCAL_STORAGE_DIR, 'projects', slug, 'apikey.json');
|
|
581
|
+
if (!fs.existsSync(keyPath)) {
|
|
582
|
+
return null;
|
|
583
|
+
}
|
|
584
|
+
const data = fs.readFileSync(keyPath, 'utf-8');
|
|
585
|
+
return JSON.parse(data);
|
|
586
|
+
}
|
|
587
|
+
const blobPath = getApiKeyBlobPath(slug);
|
|
588
|
+
const blobInfo = await head(blobPath);
|
|
589
|
+
if (!blobInfo) {
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
592
|
+
const response = await fetch(blobInfo.url);
|
|
593
|
+
if (!response.ok) {
|
|
594
|
+
return null;
|
|
595
|
+
}
|
|
596
|
+
return await response.json();
|
|
597
|
+
} catch (error) {
|
|
598
|
+
console.error(`[Blob] Error fetching API key for ${slug}:`, error);
|
|
599
|
+
return null;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Validate an API key and return the associated project slug
|
|
604
|
+
* O(1) lookup from registry
|
|
605
|
+
*/ export async function validateApiKey(apiKey) {
|
|
606
|
+
// API key format: sk_live_<32chars>
|
|
607
|
+
if (!apiKey || !apiKey.startsWith('sk_live_') || apiKey.length !== 40) {
|
|
608
|
+
return null;
|
|
609
|
+
}
|
|
610
|
+
try {
|
|
611
|
+
// O(1) registry lookup
|
|
612
|
+
const slug = await validateApiKeyFromRegistry(apiKey);
|
|
613
|
+
if (slug) {
|
|
614
|
+
// Update last used timestamp
|
|
615
|
+
await updateApiKeyLastUsed(slug);
|
|
616
|
+
}
|
|
617
|
+
return slug;
|
|
618
|
+
} catch (error) {
|
|
619
|
+
console.error('[Blob] Error validating API key:', error);
|
|
620
|
+
return null;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Update the lastUsedAt timestamp for an API key
|
|
625
|
+
*/ async function updateApiKeyLastUsed(slug) {
|
|
626
|
+
try {
|
|
627
|
+
if (IS_LOCAL_DEV) {
|
|
628
|
+
const keyPath = path.join(LOCAL_STORAGE_DIR, 'projects', slug, 'apikey.json');
|
|
629
|
+
if (fs.existsSync(keyPath)) {
|
|
630
|
+
const data = JSON.parse(fs.readFileSync(keyPath, 'utf-8'));
|
|
631
|
+
data.lastUsedAt = new Date().toISOString();
|
|
632
|
+
fs.writeFileSync(keyPath, JSON.stringify(data, null, 2));
|
|
633
|
+
}
|
|
634
|
+
} else {
|
|
635
|
+
const blobPath = getApiKeyBlobPath(slug);
|
|
636
|
+
const blobInfo = await head(blobPath).catch(()=>null);
|
|
637
|
+
if (blobInfo) {
|
|
638
|
+
const response = await fetch(blobInfo.url);
|
|
639
|
+
if (response.ok) {
|
|
640
|
+
const keyData = await response.json();
|
|
641
|
+
keyData.lastUsedAt = new Date().toISOString();
|
|
642
|
+
await put(blobPath, JSON.stringify(keyData), {
|
|
643
|
+
access: 'public',
|
|
644
|
+
contentType: 'application/json',
|
|
645
|
+
allowOverwrite: true
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
} catch {
|
|
651
|
+
// Non-critical, ignore errors
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Regenerate API key for a project (requires old key for auth)
|
|
656
|
+
*/ export async function regenerateApiKey(slug, oldApiKey) {
|
|
657
|
+
// Validate old key first
|
|
658
|
+
const validSlug = await validateApiKey(oldApiKey);
|
|
659
|
+
if (validSlug !== slug) {
|
|
660
|
+
return null;
|
|
661
|
+
}
|
|
662
|
+
// Generate new key
|
|
663
|
+
const newKey = generateApiKey();
|
|
664
|
+
// Update registry: remove old key, add new key
|
|
665
|
+
const registry = await getRegistry();
|
|
666
|
+
delete registry.apiKeys[oldApiKey];
|
|
667
|
+
registry.apiKeys[newKey] = slug;
|
|
668
|
+
await saveRegistry(registry);
|
|
669
|
+
// Also update project's apikey.json
|
|
670
|
+
await storeProjectApiKey(slug, newKey);
|
|
671
|
+
return newKey;
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* Get the blob path for a project's assets
|
|
675
|
+
*/ export function getAssetBlobPath(slug, fileName) {
|
|
676
|
+
return `projects/${slug}/assets/${fileName}`;
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* List all assets for a project
|
|
680
|
+
*/ export async function listProjectAssets(slug) {
|
|
681
|
+
try {
|
|
682
|
+
if (IS_LOCAL_DEV) {
|
|
683
|
+
const assetsDir = path.join(LOCAL_STORAGE_DIR, 'projects', slug, 'assets');
|
|
684
|
+
if (!fs.existsSync(assetsDir)) {
|
|
685
|
+
return [];
|
|
686
|
+
}
|
|
687
|
+
const files = fs.readdirSync(assetsDir);
|
|
688
|
+
return files.map((fileName)=>{
|
|
689
|
+
const filePath = path.join(assetsDir, fileName);
|
|
690
|
+
const stats = fs.statSync(filePath);
|
|
691
|
+
return {
|
|
692
|
+
path: `projects/${slug}/assets/${fileName}`,
|
|
693
|
+
url: `file://${filePath}`,
|
|
694
|
+
fileName,
|
|
695
|
+
size: stats.size,
|
|
696
|
+
contentType: getContentType(fileName),
|
|
697
|
+
uploadedAt: stats.mtime.toISOString()
|
|
698
|
+
};
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
const { blobs } = await list({
|
|
702
|
+
prefix: `projects/${slug}/assets/`
|
|
703
|
+
});
|
|
704
|
+
return blobs.map((blob)=>{
|
|
705
|
+
const fileName = blob.pathname.split('/').pop() || '';
|
|
706
|
+
return {
|
|
707
|
+
path: blob.pathname,
|
|
708
|
+
url: blob.url,
|
|
709
|
+
fileName,
|
|
710
|
+
size: blob.size,
|
|
711
|
+
contentType: getContentType(fileName),
|
|
712
|
+
uploadedAt: blob.uploadedAt.toISOString()
|
|
713
|
+
};
|
|
714
|
+
});
|
|
715
|
+
} catch (error) {
|
|
716
|
+
console.error(`[Blob] Error listing assets for ${slug}:`, error);
|
|
717
|
+
return [];
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
/**
|
|
721
|
+
* Delete an asset
|
|
722
|
+
*/ export async function deleteProjectAsset(slug, fileName) {
|
|
723
|
+
try {
|
|
724
|
+
if (IS_LOCAL_DEV) {
|
|
725
|
+
const filePath = path.join(LOCAL_STORAGE_DIR, 'projects', slug, 'assets', fileName);
|
|
726
|
+
if (fs.existsSync(filePath)) {
|
|
727
|
+
fs.unlinkSync(filePath);
|
|
728
|
+
return true;
|
|
729
|
+
}
|
|
730
|
+
return false;
|
|
731
|
+
}
|
|
732
|
+
const blobPath = getAssetBlobPath(slug, fileName);
|
|
733
|
+
const blobInfo = await head(blobPath).catch(()=>null);
|
|
734
|
+
if (blobInfo) {
|
|
735
|
+
await del(blobInfo.url);
|
|
736
|
+
return true;
|
|
737
|
+
}
|
|
738
|
+
return false;
|
|
739
|
+
} catch (error) {
|
|
740
|
+
console.error(`[Blob] Error deleting asset ${fileName} for ${slug}:`, error);
|
|
741
|
+
return false;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* Helper to get content type from file extension
|
|
746
|
+
*/ function getContentType(fileName) {
|
|
747
|
+
const ext = fileName.split('.').pop()?.toLowerCase();
|
|
748
|
+
const types = {
|
|
749
|
+
'jpg': 'image/jpeg',
|
|
750
|
+
'jpeg': 'image/jpeg',
|
|
751
|
+
'png': 'image/png',
|
|
752
|
+
'gif': 'image/gif',
|
|
753
|
+
'webp': 'image/webp',
|
|
754
|
+
'svg': 'image/svg+xml',
|
|
755
|
+
'ico': 'image/x-icon',
|
|
756
|
+
'pdf': 'application/pdf',
|
|
757
|
+
'mp4': 'video/mp4',
|
|
758
|
+
'webm': 'video/webm',
|
|
759
|
+
'mp3': 'audio/mpeg',
|
|
760
|
+
'wav': 'audio/wav',
|
|
761
|
+
'woff': 'font/woff',
|
|
762
|
+
'woff2': 'font/woff2',
|
|
763
|
+
'ttf': 'font/ttf',
|
|
764
|
+
'otf': 'font/otf'
|
|
765
|
+
};
|
|
766
|
+
return types[ext || ''] || 'application/octet-stream';
|
|
767
|
+
}
|