@commonpub/layer 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/app.vue +7 -0
- package/components/AnnouncementBand.vue +117 -0
- package/components/AppToast.vue +108 -0
- package/components/AuthorCard.vue +119 -0
- package/components/AuthorRow.vue +81 -0
- package/components/CommentSection.vue +330 -0
- package/components/ContentCard.vue +340 -0
- package/components/ContentPicker.vue +240 -0
- package/components/ContentStarterForm.vue +214 -0
- package/components/ContentTypeBadge.vue +46 -0
- package/components/CountdownTimer.vue +68 -0
- package/components/CpubEditor.vue +87 -0
- package/components/DiscussionItem.vue +191 -0
- package/components/EditorPropertiesPanel.vue +393 -0
- package/components/EngagementBar.vue +131 -0
- package/components/FederatedContentCard.vue +291 -0
- package/components/FeedItem.vue +283 -0
- package/components/FilterChip.vue +21 -0
- package/components/HeatmapGrid.vue +92 -0
- package/components/ImageUpload.vue +219 -0
- package/components/MemberCard.vue +163 -0
- package/components/MessageThread.vue +120 -0
- package/components/NotificationItem.vue +103 -0
- package/components/ProgressTracker.vue +41 -0
- package/components/PublishErrorsModal.vue +116 -0
- package/components/RemoteActorCard.vue +206 -0
- package/components/RemoteUserSearch.vue +117 -0
- package/components/SearchFilters.vue +188 -0
- package/components/SearchSidebar.vue +181 -0
- package/components/SectionHeader.vue +17 -0
- package/components/ShareToHubModal.vue +189 -0
- package/components/SiteLogo.vue +21 -0
- package/components/SkillBar.vue +57 -0
- package/components/SortSelect.vue +30 -0
- package/components/StatBar.vue +14 -0
- package/components/TOCNav.vue +69 -0
- package/components/TimelineItem.vue +82 -0
- package/components/VideoCard.vue +106 -0
- package/components/blocks/BlockBuildStepView.vue +92 -0
- package/components/blocks/BlockCalloutView.vue +82 -0
- package/components/blocks/BlockCheckpointView.vue +50 -0
- package/components/blocks/BlockCodeView.vue +212 -0
- package/components/blocks/BlockContentRenderer.vue +143 -0
- package/components/blocks/BlockDividerView.vue +11 -0
- package/components/blocks/BlockDownloadsView.vue +126 -0
- package/components/blocks/BlockEmbedView.vue +61 -0
- package/components/blocks/BlockGalleryView.vue +57 -0
- package/components/blocks/BlockHeadingView.vue +29 -0
- package/components/blocks/BlockImageView.vue +34 -0
- package/components/blocks/BlockMarkdownView.vue +118 -0
- package/components/blocks/BlockMathView.vue +45 -0
- package/components/blocks/BlockPartsListView.vue +104 -0
- package/components/blocks/BlockQuizView.vue +239 -0
- package/components/blocks/BlockQuoteView.vue +41 -0
- package/components/blocks/BlockSectionHeaderView.vue +58 -0
- package/components/blocks/BlockSliderView.vue +236 -0
- package/components/blocks/BlockTextView.vue +41 -0
- package/components/blocks/BlockToolListView.vue +87 -0
- package/components/blocks/BlockVideoView.vue +89 -0
- package/components/editors/ArticleEditor.vue +545 -0
- package/components/editors/BlockCanvas.vue +487 -0
- package/components/editors/BlockInsertZone.vue +84 -0
- package/components/editors/BlockPicker.vue +285 -0
- package/components/editors/BlockWrapper.vue +192 -0
- package/components/editors/BlogEditor.vue +567 -0
- package/components/editors/EditorBlocks.vue +248 -0
- package/components/editors/EditorSection.vue +81 -0
- package/components/editors/EditorShell.vue +168 -0
- package/components/editors/EditorTagInput.vue +114 -0
- package/components/editors/EditorVisibility.vue +110 -0
- package/components/editors/ExplainerEditor.vue +503 -0
- package/components/editors/MarkdownImportDialog.vue +249 -0
- package/components/editors/ProjectEditor.vue +446 -0
- package/components/editors/blocks/BuildStepBlock.vue +102 -0
- package/components/editors/blocks/CalloutBlock.vue +122 -0
- package/components/editors/blocks/CheckpointBlock.vue +27 -0
- package/components/editors/blocks/CodeBlock.vue +177 -0
- package/components/editors/blocks/DividerBlock.vue +22 -0
- package/components/editors/blocks/DownloadsBlock.vue +41 -0
- package/components/editors/blocks/EmbedBlock.vue +20 -0
- package/components/editors/blocks/GalleryBlock.vue +236 -0
- package/components/editors/blocks/HeadingBlock.vue +96 -0
- package/components/editors/blocks/ImageBlock.vue +271 -0
- package/components/editors/blocks/MarkdownBlock.vue +258 -0
- package/components/editors/blocks/MathBlock.vue +37 -0
- package/components/editors/blocks/PartsListBlock.vue +358 -0
- package/components/editors/blocks/QuizBlock.vue +47 -0
- package/components/editors/blocks/QuoteBlock.vue +101 -0
- package/components/editors/blocks/SectionHeaderBlock.vue +130 -0
- package/components/editors/blocks/SliderBlock.vue +318 -0
- package/components/editors/blocks/TextBlock.vue +201 -0
- package/components/editors/blocks/ToolListBlock.vue +70 -0
- package/components/editors/blocks/VideoBlock.vue +22 -0
- package/components/hub/HubDiscussions.vue +47 -0
- package/components/hub/HubFeed.vue +199 -0
- package/components/hub/HubHero.vue +185 -0
- package/components/hub/HubLayout.vue +103 -0
- package/components/hub/HubMembers.vue +40 -0
- package/components/hub/HubProducts.vue +93 -0
- package/components/hub/HubProjects.vue +207 -0
- package/components/hub/HubSidebar.vue +12 -0
- package/components/hub/HubSidebarCard.vue +35 -0
- package/components/views/ArticleView.vue +771 -0
- package/components/views/BlogView.vue +667 -0
- package/components/views/ExplainerView.vue +688 -0
- package/components/views/ProjectView.vue +1500 -0
- package/composables/useApiError.ts +39 -0
- package/composables/useAuth.ts +87 -0
- package/composables/useBlockEditor.ts +187 -0
- package/composables/useContentSave.ts +253 -0
- package/composables/useContentTypes.ts +37 -0
- package/composables/useEngagement.ts +196 -0
- package/composables/useFeatures.ts +33 -0
- package/composables/useFederation.ts +72 -0
- package/composables/useJsonLd.ts +183 -0
- package/composables/useMarkdownImport.ts +77 -0
- package/composables/useMirrorContent.ts +105 -0
- package/composables/useNotifications.ts +73 -0
- package/composables/usePublishValidation.ts +65 -0
- package/composables/useSanitize.ts +34 -0
- package/composables/useSiteName.ts +4 -0
- package/composables/useTheme.ts +34 -0
- package/composables/useToast.ts +35 -0
- package/error.vue +129 -0
- package/layouts/admin.vue +213 -0
- package/layouts/auth.vue +63 -0
- package/layouts/default.vue +269 -0
- package/layouts/editor.vue +129 -0
- package/middleware/auth.ts +6 -0
- package/nuxt.config.ts +83 -0
- package/package.json +59 -0
- package/pages/[type]/[slug]/edit.vue +676 -0
- package/pages/[type]/[slug]/index.vue +313 -0
- package/pages/[type]/index.vue +118 -0
- package/pages/about.vue +100 -0
- package/pages/admin/audit.vue +66 -0
- package/pages/admin/content.vue +116 -0
- package/pages/admin/federation.vue +446 -0
- package/pages/admin/index.vue +62 -0
- package/pages/admin/reports.vue +88 -0
- package/pages/admin/settings.vue +167 -0
- package/pages/admin/users.vue +145 -0
- package/pages/auth/forgot-password.vue +103 -0
- package/pages/auth/login.vue +216 -0
- package/pages/auth/oauth/authorize.vue +178 -0
- package/pages/auth/register.vue +246 -0
- package/pages/auth/reset-password.vue +124 -0
- package/pages/auth/verify-email.vue +80 -0
- package/pages/cert/[code].vue +154 -0
- package/pages/contests/[slug]/edit.vue +153 -0
- package/pages/contests/[slug]/index.vue +556 -0
- package/pages/contests/[slug]/judge.vue +160 -0
- package/pages/contests/create.vue +192 -0
- package/pages/contests/index.vue +69 -0
- package/pages/create.vue +219 -0
- package/pages/dashboard.vue +521 -0
- package/pages/docs/[siteSlug]/[...pagePath].vue +704 -0
- package/pages/docs/[siteSlug]/edit.vue +480 -0
- package/pages/docs/[siteSlug]/index.vue +380 -0
- package/pages/docs/create.vue +60 -0
- package/pages/docs/index.vue +181 -0
- package/pages/explore.vue +422 -0
- package/pages/federated-hubs/[id]/index.vue +385 -0
- package/pages/federated-hubs/[id]/posts/[postId].vue +309 -0
- package/pages/federation/index.vue +157 -0
- package/pages/federation/search.vue +43 -0
- package/pages/federation/users/[handle].vue +221 -0
- package/pages/feed.vue +135 -0
- package/pages/hubs/[slug]/index.vue +513 -0
- package/pages/hubs/[slug]/members.vue +134 -0
- package/pages/hubs/[slug]/posts/[postId].vue +352 -0
- package/pages/hubs/[slug]/settings.vue +254 -0
- package/pages/hubs/create.vue +201 -0
- package/pages/hubs/index.vue +207 -0
- package/pages/index.vue +1005 -0
- package/pages/learn/[slug]/[lessonSlug]/edit.vue +413 -0
- package/pages/learn/[slug]/[lessonSlug]/index.vue +438 -0
- package/pages/learn/[slug]/edit.vue +414 -0
- package/pages/learn/[slug]/index.vue +341 -0
- package/pages/learn/create.vue +71 -0
- package/pages/learn/index.vue +360 -0
- package/pages/messages/[conversationId].vue +113 -0
- package/pages/messages/index.vue +303 -0
- package/pages/mirror/[id].vue +115 -0
- package/pages/notifications.vue +91 -0
- package/pages/products/[slug].vue +128 -0
- package/pages/products/index.vue +122 -0
- package/pages/search.vue +692 -0
- package/pages/settings/account.vue +170 -0
- package/pages/settings/appearance.vue +80 -0
- package/pages/settings/index.vue +81 -0
- package/pages/settings/notifications.vue +68 -0
- package/pages/settings/profile.vue +838 -0
- package/pages/tags/[slug].vue +111 -0
- package/pages/tags/index.vue +73 -0
- package/pages/u/[username]/followers.vue +86 -0
- package/pages/u/[username]/following.vue +94 -0
- package/pages/u/[username]/index.vue +837 -0
- package/pages/videos/[id].vue +212 -0
- package/pages/videos/index.vue +327 -0
- package/pages/videos/submit.vue +112 -0
- package/plugins/auth.ts +23 -0
- package/server/api/admin/audit.get.ts +17 -0
- package/server/api/admin/content/[id].delete.ts +15 -0
- package/server/api/admin/content/[id].patch.ts +37 -0
- package/server/api/admin/federation/activity.get.ts +31 -0
- package/server/api/admin/federation/clients.get.ts +9 -0
- package/server/api/admin/federation/clients.post.ts +16 -0
- package/server/api/admin/federation/hub-mirrors/index.get.ts +10 -0
- package/server/api/admin/federation/hub-mirrors/index.post.ts +42 -0
- package/server/api/admin/federation/mirrors/[id]/backfill.post.ts +39 -0
- package/server/api/admin/federation/mirrors/[id].delete.ts +11 -0
- package/server/api/admin/federation/mirrors/[id].get.ts +15 -0
- package/server/api/admin/federation/mirrors/[id].put.ts +22 -0
- package/server/api/admin/federation/mirrors/index.get.ts +9 -0
- package/server/api/admin/federation/mirrors/index.post.ts +30 -0
- package/server/api/admin/federation/pending.get.ts +22 -0
- package/server/api/admin/federation/refederate.post.ts +92 -0
- package/server/api/admin/federation/repair-types.post.ts +57 -0
- package/server/api/admin/federation/retry.post.ts +41 -0
- package/server/api/admin/federation/stats.get.ts +26 -0
- package/server/api/admin/reports/[id]/resolve.post.ts +12 -0
- package/server/api/admin/reports.get.ts +17 -0
- package/server/api/admin/settings.get.ts +22 -0
- package/server/api/admin/settings.put.ts +11 -0
- package/server/api/admin/stats.get.ts +9 -0
- package/server/api/admin/users/[id]/role.put.ts +12 -0
- package/server/api/admin/users/[id]/status.put.ts +12 -0
- package/server/api/admin/users/[id].delete.ts +10 -0
- package/server/api/admin/users.get.ts +18 -0
- package/server/api/auth/federated/callback.get.ts +67 -0
- package/server/api/auth/federated/login.post.ts +60 -0
- package/server/api/auth/oauth2/authorize.get.ts +30 -0
- package/server/api/auth/oauth2/authorize.post.ts +51 -0
- package/server/api/auth/oauth2/register.post.ts +41 -0
- package/server/api/auth/oauth2/token.post.ts +48 -0
- package/server/api/cert/[code].get.ts +13 -0
- package/server/api/content/[id]/build.post.ts +9 -0
- package/server/api/content/[id]/fork.post.ts +10 -0
- package/server/api/content/[id]/index.delete.ts +18 -0
- package/server/api/content/[id]/index.get.ts +15 -0
- package/server/api/content/[id]/index.put.ts +23 -0
- package/server/api/content/[id]/products/[productId].delete.ts +28 -0
- package/server/api/content/[id]/products-sync.post.ts +29 -0
- package/server/api/content/[id]/products.get.ts +9 -0
- package/server/api/content/[id]/products.post.ts +31 -0
- package/server/api/content/[id]/publish.post.ts +17 -0
- package/server/api/content/[id]/report.post.ts +17 -0
- package/server/api/content/[id]/versions.get.ts +9 -0
- package/server/api/content/[id]/view.post.ts +34 -0
- package/server/api/content/index.get.ts +23 -0
- package/server/api/content/index.post.ts +11 -0
- package/server/api/contests/[slug]/entries.get.ts +18 -0
- package/server/api/contests/[slug]/entries.post.ts +23 -0
- package/server/api/contests/[slug]/index.delete.ts +21 -0
- package/server/api/contests/[slug]/index.get.ts +11 -0
- package/server/api/contests/[slug]/index.put.ts +15 -0
- package/server/api/contests/[slug]/judge.post.ts +12 -0
- package/server/api/contests/[slug]/transition.post.ts +24 -0
- package/server/api/contests/index.get.ts +10 -0
- package/server/api/contests/index.post.ts +28 -0
- package/server/api/docs/[siteSlug]/index.delete.ts +14 -0
- package/server/api/docs/[siteSlug]/index.get.ts +17 -0
- package/server/api/docs/[siteSlug]/index.put.ts +20 -0
- package/server/api/docs/[siteSlug]/nav.get.ts +26 -0
- package/server/api/docs/[siteSlug]/pages/[pageId].delete.ts +14 -0
- package/server/api/docs/[siteSlug]/pages/[pageId].get.ts +31 -0
- package/server/api/docs/[siteSlug]/pages/[pageId].put.ts +15 -0
- package/server/api/docs/[siteSlug]/pages/index.get.ts +34 -0
- package/server/api/docs/[siteSlug]/pages/index.post.ts +28 -0
- package/server/api/docs/[siteSlug]/pages/reorder.post.ts +26 -0
- package/server/api/docs/[siteSlug]/search.get.ts +20 -0
- package/server/api/docs/[siteSlug]/versions.post.ts +11 -0
- package/server/api/docs/index.get.ts +6 -0
- package/server/api/docs/index.post.ts +10 -0
- package/server/api/federated-hubs/[id]/posts/[postId].get.ts +16 -0
- package/server/api/federated-hubs/[id]/posts.get.ts +15 -0
- package/server/api/federated-hubs/[id].get.ts +16 -0
- package/server/api/federation/boost.post.ts +21 -0
- package/server/api/federation/content/[id].get.ts +15 -0
- package/server/api/federation/follow.post.ts +16 -0
- package/server/api/federation/health.get.ts +56 -0
- package/server/api/federation/hub-follow.post.ts +27 -0
- package/server/api/federation/hub-post-like.post.ts +115 -0
- package/server/api/federation/hub-post-likes.get.ts +24 -0
- package/server/api/federation/hub-post-reply.post.ts +42 -0
- package/server/api/federation/hub-post.post.ts +33 -0
- package/server/api/federation/like.post.ts +21 -0
- package/server/api/federation/remote-actor.get.ts +22 -0
- package/server/api/federation/reply.post.ts +22 -0
- package/server/api/federation/search.post.ts +17 -0
- package/server/api/federation/timeline.get.ts +22 -0
- package/server/api/federation/unfollow.post.ts +17 -0
- package/server/api/files/[id].delete.ts +35 -0
- package/server/api/files/mine.get.ts +31 -0
- package/server/api/files/upload-from-url.post.ts +68 -0
- package/server/api/files/upload.post.ts +105 -0
- package/server/api/health.get.ts +4 -0
- package/server/api/hubs/[slug]/bans/[userId].delete.ts +13 -0
- package/server/api/hubs/[slug]/bans.get.ts +20 -0
- package/server/api/hubs/[slug]/bans.post.ts +23 -0
- package/server/api/hubs/[slug]/feed.xml.get.ts +60 -0
- package/server/api/hubs/[slug]/gallery.get.ts +21 -0
- package/server/api/hubs/[slug]/index.delete.ts +18 -0
- package/server/api/hubs/[slug]/index.get.ts +14 -0
- package/server/api/hubs/[slug]/index.put.ts +22 -0
- package/server/api/hubs/[slug]/invites.get.ts +20 -0
- package/server/api/hubs/[slug]/invites.post.ts +27 -0
- package/server/api/hubs/[slug]/join.post.ts +17 -0
- package/server/api/hubs/[slug]/leave.post.ts +13 -0
- package/server/api/hubs/[slug]/members/[userId].delete.ts +13 -0
- package/server/api/hubs/[slug]/members/[userId].put.ts +16 -0
- package/server/api/hubs/[slug]/members.get.ts +20 -0
- package/server/api/hubs/[slug]/posts/[postId]/like.post.ts +21 -0
- package/server/api/hubs/[slug]/posts/[postId]/lock.post.ts +17 -0
- package/server/api/hubs/[slug]/posts/[postId]/pin.post.ts +17 -0
- package/server/api/hubs/[slug]/posts/[postId]/replies.get.ts +16 -0
- package/server/api/hubs/[slug]/posts/[postId]/replies.post.ts +21 -0
- package/server/api/hubs/[slug]/posts/[postId].delete.ts +23 -0
- package/server/api/hubs/[slug]/posts/[postId].get.ts +16 -0
- package/server/api/hubs/[slug]/posts/index.get.ts +16 -0
- package/server/api/hubs/[slug]/posts/index.post.ts +36 -0
- package/server/api/hubs/[slug]/products.get.ts +26 -0
- package/server/api/hubs/[slug]/products.post.ts +18 -0
- package/server/api/hubs/[slug]/share.post.ts +39 -0
- package/server/api/hubs/index.get.ts +15 -0
- package/server/api/hubs/index.post.ts +11 -0
- package/server/api/image-proxy.get.ts +91 -0
- package/server/api/learn/[slug]/[lessonSlug]/complete.post.ts +13 -0
- package/server/api/learn/[slug]/[lessonSlug]/index.get.ts +68 -0
- package/server/api/learn/[slug]/enroll.post.ts +12 -0
- package/server/api/learn/[slug]/index.delete.ts +12 -0
- package/server/api/learn/[slug]/index.get.ts +14 -0
- package/server/api/learn/[slug]/index.put.ts +19 -0
- package/server/api/learn/[slug]/lessons/[lessonId].delete.ts +14 -0
- package/server/api/learn/[slug]/lessons/[lessonId].put.ts +24 -0
- package/server/api/learn/[slug]/lessons.post.ts +10 -0
- package/server/api/learn/[slug]/modules/[moduleId].delete.ts +14 -0
- package/server/api/learn/[slug]/modules/[moduleId].put.ts +15 -0
- package/server/api/learn/[slug]/modules.post.ts +14 -0
- package/server/api/learn/[slug]/publish.post.ts +13 -0
- package/server/api/learn/[slug]/unenroll.post.ts +12 -0
- package/server/api/learn/certificates.get.ts +9 -0
- package/server/api/learn/enrollments.get.ts +9 -0
- package/server/api/learn/index.get.ts +17 -0
- package/server/api/learn/index.post.ts +11 -0
- package/server/api/me.get.ts +13 -0
- package/server/api/messages/[conversationId]/info.get.ts +43 -0
- package/server/api/messages/[conversationId]/stream.get.ts +73 -0
- package/server/api/messages/[conversationId].get.ts +13 -0
- package/server/api/messages/[conversationId].post.ts +12 -0
- package/server/api/messages/index.get.ts +39 -0
- package/server/api/messages/index.post.ts +58 -0
- package/server/api/notifications/[id].delete.ts +11 -0
- package/server/api/notifications/count.get.ts +10 -0
- package/server/api/notifications/index.get.ts +24 -0
- package/server/api/notifications/read.post.ts +15 -0
- package/server/api/notifications/stream.get.ts +59 -0
- package/server/api/openapi.get.ts +5 -0
- package/server/api/products/[id].delete.ts +15 -0
- package/server/api/products/[id].put.ts +18 -0
- package/server/api/products/[slug]/content.get.ts +21 -0
- package/server/api/products/[slug].get.ts +16 -0
- package/server/api/products/index.get.ts +28 -0
- package/server/api/profile.get.ts +15 -0
- package/server/api/profile.put.ts +24 -0
- package/server/api/resolve-identity.post.ts +34 -0
- package/server/api/search/index.get.ts +24 -0
- package/server/api/search/trending.get.ts +22 -0
- package/server/api/social/bookmark.get.ts +16 -0
- package/server/api/social/bookmark.post.ts +15 -0
- package/server/api/social/bookmarks.get.ts +16 -0
- package/server/api/social/comments/[id].delete.ts +13 -0
- package/server/api/social/comments.get.ts +18 -0
- package/server/api/social/comments.post.ts +11 -0
- package/server/api/social/like.get.ts +17 -0
- package/server/api/social/like.post.ts +35 -0
- package/server/api/stats.get.ts +8 -0
- package/server/api/user/hubs.get.ts +22 -0
- package/server/api/users/[username]/content.get.ts +21 -0
- package/server/api/users/[username]/feed.xml.get.ts +65 -0
- package/server/api/users/[username]/follow.delete.ts +15 -0
- package/server/api/users/[username]/follow.post.ts +15 -0
- package/server/api/users/[username]/followers.get.ts +22 -0
- package/server/api/users/[username]/following.get.ts +22 -0
- package/server/api/users/[username]/learning.get.ts +18 -0
- package/server/api/users/[username].get.ts +25 -0
- package/server/api/users/index.get.ts +78 -0
- package/server/api/videos/[id].get.ts +18 -0
- package/server/api/videos/categories/[id].delete.ts +15 -0
- package/server/api/videos/categories/[id].put.ts +17 -0
- package/server/api/videos/categories.get.ts +7 -0
- package/server/api/videos/categories.post.ts +11 -0
- package/server/api/videos/index.get.ts +20 -0
- package/server/api/videos/index.post.ts +11 -0
- package/server/middleware/auth.ts +180 -0
- package/server/middleware/features.ts +31 -0
- package/server/middleware/security.ts +54 -0
- package/server/plugins/auto-admin.ts +69 -0
- package/server/plugins/federation-delivery.ts +74 -0
- package/server/routes/.well-known/nodeinfo.ts +15 -0
- package/server/routes/.well-known/webfinger.ts +86 -0
- package/server/routes/actor/followers.ts +34 -0
- package/server/routes/actor/following.ts +34 -0
- package/server/routes/actor/outbox.ts +31 -0
- package/server/routes/actor.ts +30 -0
- package/server/routes/feed.xml.ts +59 -0
- package/server/routes/hubs/[slug]/followers.ts +39 -0
- package/server/routes/hubs/[slug]/inbox.ts +35 -0
- package/server/routes/hubs/[slug]/outbox.ts +43 -0
- package/server/routes/hubs/[slug]/posts/[postId].ts +63 -0
- package/server/routes/hubs/[slug].ts +29 -0
- package/server/routes/inbox.ts +34 -0
- package/server/routes/nodeinfo/2.1.ts +27 -0
- package/server/routes/robots.txt.ts +19 -0
- package/server/routes/sitemap.xml.ts +105 -0
- package/server/routes/users/[username]/followers.ts +33 -0
- package/server/routes/users/[username]/following.ts +33 -0
- package/server/routes/users/[username]/inbox.ts +34 -0
- package/server/routes/users/[username]/outbox.ts +35 -0
- package/server/routes/users/[username].ts +62 -0
- package/server/utils/auth.ts +36 -0
- package/server/utils/db.ts +34 -0
- package/server/utils/errors.ts +24 -0
- package/server/utils/inbox.ts +122 -0
- package/server/utils/validate.ts +82 -0
- package/theme/base.css +283 -0
- package/theme/components.css +322 -0
- package/theme/dark.css +87 -0
- package/theme/editor-panels.css +63 -0
- package/theme/forms.css +216 -0
- package/theme/generics.css +68 -0
- package/theme/layouts.css +415 -0
- package/theme/prose.css +342 -0
- package/types/hub.ts +61 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { processInboxActivity } from '@commonpub/protocol';
|
|
2
|
+
import { createInboxHandlers } from '@commonpub/server';
|
|
3
|
+
import { verifyInboxRequest, extractDomain } from '../utils/inbox';
|
|
4
|
+
|
|
5
|
+
export default defineEventHandler(async (event) => {
|
|
6
|
+
const config = useConfig();
|
|
7
|
+
if (!config.features.federation) {
|
|
8
|
+
throw createError({ statusCode: 404, statusMessage: 'Not Found' });
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (getMethod(event) !== 'POST') {
|
|
12
|
+
throw createError({ statusCode: 405, statusMessage: 'Method Not Allowed' });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Verify signature, domain, date freshness, body size
|
|
16
|
+
const { body } = await verifyInboxRequest(event, 'shared-inbox');
|
|
17
|
+
|
|
18
|
+
const db = useDB();
|
|
19
|
+
const runtimeConfig = useRuntimeConfig();
|
|
20
|
+
const domain = extractDomain((runtimeConfig.public?.siteUrl as string) || `https://${config.instance.domain}`);
|
|
21
|
+
const callbacks = createInboxHandlers({ db, domain });
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const result = await processInboxActivity(body, callbacks);
|
|
25
|
+
if (!result.success) {
|
|
26
|
+
throw createError({ statusCode: 400, statusMessage: result.error ?? 'Invalid activity' });
|
|
27
|
+
}
|
|
28
|
+
return { status: 'accepted' };
|
|
29
|
+
} catch (err: unknown) {
|
|
30
|
+
if ((err as { statusCode?: number }).statusCode) throw err;
|
|
31
|
+
console.error('[shared-inbox]', err);
|
|
32
|
+
throw createError({ statusCode: 400, statusMessage: 'Invalid activity' });
|
|
33
|
+
}
|
|
34
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// NodeInfo 2.1 response
|
|
2
|
+
import { buildNodeInfoResponse } from '@commonpub/protocol';
|
|
3
|
+
import { getPlatformStats } from '@commonpub/server';
|
|
4
|
+
|
|
5
|
+
export default defineEventHandler(async () => {
|
|
6
|
+
const config = useConfig();
|
|
7
|
+
const db = useDB();
|
|
8
|
+
|
|
9
|
+
let userCount = 0;
|
|
10
|
+
let localPostCount = 0;
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const stats = await getPlatformStats(db);
|
|
14
|
+
userCount = stats.users.total ?? 0;
|
|
15
|
+
localPostCount = stats.content.total ?? 0;
|
|
16
|
+
} catch {
|
|
17
|
+
// DB may not be available, return zeros
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return buildNodeInfoResponse({
|
|
21
|
+
config,
|
|
22
|
+
version: '0.0.1',
|
|
23
|
+
userCount,
|
|
24
|
+
activeMonthCount: userCount,
|
|
25
|
+
localPostCount,
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export default defineEventHandler((event) => {
|
|
2
|
+
const config = useRuntimeConfig();
|
|
3
|
+
const siteUrl = config.public.siteUrl as string;
|
|
4
|
+
|
|
5
|
+
const content = `User-agent: *
|
|
6
|
+
Allow: /
|
|
7
|
+
Disallow: /api/
|
|
8
|
+
Disallow: /admin/
|
|
9
|
+
Disallow: /settings/
|
|
10
|
+
Disallow: /messages/
|
|
11
|
+
Disallow: /create/
|
|
12
|
+
|
|
13
|
+
Sitemap: ${siteUrl}/sitemap.xml
|
|
14
|
+
`;
|
|
15
|
+
|
|
16
|
+
setResponseHeader(event, 'Content-Type', 'text/plain; charset=utf-8');
|
|
17
|
+
setResponseHeader(event, 'Cache-Control', 'public, max-age=86400');
|
|
18
|
+
return content;
|
|
19
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { eq } from 'drizzle-orm';
|
|
2
|
+
import { contentItems, users } from '@commonpub/schema';
|
|
3
|
+
import { listHubs, listPaths } from '@commonpub/server';
|
|
4
|
+
|
|
5
|
+
function escapeXml(str: string): string {
|
|
6
|
+
return str
|
|
7
|
+
.replace(/&/g, '&')
|
|
8
|
+
.replace(/</g, '<')
|
|
9
|
+
.replace(/>/g, '>')
|
|
10
|
+
.replace(/"/g, '"')
|
|
11
|
+
.replace(/'/g, ''');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default defineEventHandler(async (event) => {
|
|
15
|
+
const db = useDB();
|
|
16
|
+
const config = useRuntimeConfig();
|
|
17
|
+
const siteUrl = config.public.siteUrl as string;
|
|
18
|
+
|
|
19
|
+
// Published content
|
|
20
|
+
const publishedContent = await db
|
|
21
|
+
.select({
|
|
22
|
+
type: contentItems.type,
|
|
23
|
+
slug: contentItems.slug,
|
|
24
|
+
updatedAt: contentItems.updatedAt,
|
|
25
|
+
})
|
|
26
|
+
.from(contentItems)
|
|
27
|
+
.where(eq(contentItems.status, 'published'));
|
|
28
|
+
|
|
29
|
+
// Users with public profiles
|
|
30
|
+
const publicUsers = await db
|
|
31
|
+
.select({
|
|
32
|
+
username: users.username,
|
|
33
|
+
updatedAt: users.updatedAt,
|
|
34
|
+
})
|
|
35
|
+
.from(users)
|
|
36
|
+
.where(eq(users.status, 'active'));
|
|
37
|
+
|
|
38
|
+
// Hubs
|
|
39
|
+
const { items: hubs } = await listHubs(db, { limit: 100 });
|
|
40
|
+
|
|
41
|
+
// Learning paths
|
|
42
|
+
const { items: paths } = await listPaths(db, { status: 'published', limit: 100 });
|
|
43
|
+
|
|
44
|
+
const urls: Array<{ loc: string; lastmod: string; priority: string; changefreq: string }> = [];
|
|
45
|
+
|
|
46
|
+
// Static pages
|
|
47
|
+
urls.push({ loc: siteUrl, lastmod: new Date().toISOString(), priority: '1.0', changefreq: 'daily' });
|
|
48
|
+
urls.push({ loc: `${siteUrl}/search`, lastmod: new Date().toISOString(), priority: '0.5', changefreq: 'weekly' });
|
|
49
|
+
|
|
50
|
+
// Content pages
|
|
51
|
+
for (const item of publishedContent) {
|
|
52
|
+
urls.push({
|
|
53
|
+
loc: `${siteUrl}/${item.type}/${item.slug}`,
|
|
54
|
+
lastmod: new Date(item.updatedAt).toISOString(),
|
|
55
|
+
priority: '0.8',
|
|
56
|
+
changefreq: 'weekly',
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// User profiles
|
|
61
|
+
for (const user of publicUsers) {
|
|
62
|
+
urls.push({
|
|
63
|
+
loc: `${siteUrl}/u/${user.username}`,
|
|
64
|
+
lastmod: new Date(user.updatedAt).toISOString(),
|
|
65
|
+
priority: '0.6',
|
|
66
|
+
changefreq: 'weekly',
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Hub pages (skip federated hubs — they have canonical URLs on the origin)
|
|
71
|
+
for (const hub of hubs) {
|
|
72
|
+
if ('source' in hub && hub.source === 'federated') continue;
|
|
73
|
+
const localHub = hub as { slug: string; createdAt: Date; updatedAt?: Date };
|
|
74
|
+
urls.push({
|
|
75
|
+
loc: `${siteUrl}/hubs/${localHub.slug}`,
|
|
76
|
+
lastmod: new Date(localHub.updatedAt ?? localHub.createdAt ?? new Date()).toISOString(),
|
|
77
|
+
priority: '0.7',
|
|
78
|
+
changefreq: 'weekly',
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Learning paths
|
|
83
|
+
for (const path of paths) {
|
|
84
|
+
urls.push({
|
|
85
|
+
loc: `${siteUrl}/learn/${path.slug}`,
|
|
86
|
+
lastmod: new Date((path as any).updatedAt ?? (path as any).createdAt ?? new Date()).toISOString(),
|
|
87
|
+
priority: '0.7',
|
|
88
|
+
changefreq: 'monthly',
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
93
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
94
|
+
${urls.map((u) => ` <url>
|
|
95
|
+
<loc>${escapeXml(u.loc)}</loc>
|
|
96
|
+
<lastmod>${u.lastmod}</lastmod>
|
|
97
|
+
<changefreq>${u.changefreq}</changefreq>
|
|
98
|
+
<priority>${u.priority}</priority>
|
|
99
|
+
</url>`).join('\n')}
|
|
100
|
+
</urlset>`;
|
|
101
|
+
|
|
102
|
+
setResponseHeader(event, 'Content-Type', 'application/xml; charset=utf-8');
|
|
103
|
+
setResponseHeader(event, 'Cache-Control', 'public, max-age=3600, stale-while-revalidate=1800');
|
|
104
|
+
return xml;
|
|
105
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { getUserByUsername, getFollowers } from '@commonpub/server';
|
|
2
|
+
|
|
3
|
+
export default defineEventHandler(async (event) => {
|
|
4
|
+
const username = getRouterParam(event, 'username')!;
|
|
5
|
+
const db = useDB();
|
|
6
|
+
const config = useConfig();
|
|
7
|
+
|
|
8
|
+
const profile = await getUserByUsername(db, username);
|
|
9
|
+
if (!profile) {
|
|
10
|
+
throw createError({ statusCode: 404, statusMessage: 'Actor not found' });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const domain = config.instance.domain;
|
|
14
|
+
const actorUri = `https://${domain}/users/${username}`;
|
|
15
|
+
|
|
16
|
+
let followers: string[] = [];
|
|
17
|
+
try {
|
|
18
|
+
const result = await getFollowers(db, actorUri);
|
|
19
|
+
followers = result.map((f) => f.followerActorUri);
|
|
20
|
+
} catch {
|
|
21
|
+
// May not have federation tables
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
setResponseHeader(event, 'content-type', 'application/activity+json');
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
'@context': 'https://www.w3.org/ns/activitystreams',
|
|
28
|
+
id: `${actorUri}/followers`,
|
|
29
|
+
type: 'OrderedCollection',
|
|
30
|
+
totalItems: followers.length,
|
|
31
|
+
orderedItems: followers,
|
|
32
|
+
};
|
|
33
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { getUserByUsername, getFollowing } from '@commonpub/server';
|
|
2
|
+
|
|
3
|
+
export default defineEventHandler(async (event) => {
|
|
4
|
+
const username = getRouterParam(event, 'username')!;
|
|
5
|
+
const db = useDB();
|
|
6
|
+
const config = useConfig();
|
|
7
|
+
|
|
8
|
+
const profile = await getUserByUsername(db, username);
|
|
9
|
+
if (!profile) {
|
|
10
|
+
throw createError({ statusCode: 404, statusMessage: 'Actor not found' });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const domain = config.instance.domain;
|
|
14
|
+
const actorUri = `https://${domain}/users/${username}`;
|
|
15
|
+
|
|
16
|
+
let following: string[] = [];
|
|
17
|
+
try {
|
|
18
|
+
const result = await getFollowing(db, actorUri);
|
|
19
|
+
following = result.map((f) => f.followingActorUri);
|
|
20
|
+
} catch {
|
|
21
|
+
// May not have federation tables
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
setResponseHeader(event, 'content-type', 'application/activity+json');
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
'@context': 'https://www.w3.org/ns/activitystreams',
|
|
28
|
+
id: `${actorUri}/following`,
|
|
29
|
+
type: 'OrderedCollection',
|
|
30
|
+
totalItems: following.length,
|
|
31
|
+
orderedItems: following,
|
|
32
|
+
};
|
|
33
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { processInboxActivity } from '@commonpub/protocol';
|
|
2
|
+
import { createInboxHandlers } from '@commonpub/server';
|
|
3
|
+
import { verifyInboxRequest, extractDomain } from '../../../utils/inbox';
|
|
4
|
+
|
|
5
|
+
export default defineEventHandler(async (event) => {
|
|
6
|
+
const config = useConfig();
|
|
7
|
+
if (!config.features.federation) {
|
|
8
|
+
throw createError({ statusCode: 404, statusMessage: 'Not Found' });
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (getMethod(event) !== 'POST') {
|
|
12
|
+
throw createError({ statusCode: 405, statusMessage: 'Method Not Allowed' });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Verify signature, domain, date freshness, body size
|
|
16
|
+
const { body } = await verifyInboxRequest(event, 'user-inbox');
|
|
17
|
+
|
|
18
|
+
const db = useDB();
|
|
19
|
+
const runtimeConfig = useRuntimeConfig();
|
|
20
|
+
const domain = extractDomain((runtimeConfig.public?.siteUrl as string) || `https://${config.instance.domain}`);
|
|
21
|
+
const callbacks = createInboxHandlers({ db, domain });
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const result = await processInboxActivity(body, callbacks);
|
|
25
|
+
if (!result.success) {
|
|
26
|
+
throw createError({ statusCode: 400, statusMessage: result.error ?? 'Invalid activity' });
|
|
27
|
+
}
|
|
28
|
+
return { status: 'accepted' };
|
|
29
|
+
} catch (err: unknown) {
|
|
30
|
+
if ((err as { statusCode?: number }).statusCode) throw err;
|
|
31
|
+
console.error('[user-inbox]', err);
|
|
32
|
+
throw createError({ statusCode: 400, statusMessage: 'Invalid activity' });
|
|
33
|
+
}
|
|
34
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { generateOutboxCollection, generateOutboxPage } from '@commonpub/protocol';
|
|
2
|
+
import { getUserByUsername, countOutboxItems, getOutboxPage } from '@commonpub/server';
|
|
3
|
+
|
|
4
|
+
const PAGE_SIZE = 20;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* User actor outbox — serves paginated outbound activities for a specific user.
|
|
8
|
+
* Returns Create/Update/Delete activities that were successfully delivered.
|
|
9
|
+
*/
|
|
10
|
+
export default defineEventHandler(async (event) => {
|
|
11
|
+
const username = getRouterParam(event, 'username')!;
|
|
12
|
+
const db = useDB();
|
|
13
|
+
const config = useConfig();
|
|
14
|
+
|
|
15
|
+
const profile = await getUserByUsername(db, username);
|
|
16
|
+
if (!profile) {
|
|
17
|
+
throw createError({ statusCode: 404, statusMessage: 'Actor not found' });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const domain = config.instance.domain;
|
|
21
|
+
const actorUri = `https://${domain}/users/${username}`;
|
|
22
|
+
const query = getQuery(event);
|
|
23
|
+
const page = query.page ? parseInt(String(query.page), 10) : 0;
|
|
24
|
+
|
|
25
|
+
setResponseHeader(event, 'content-type', 'application/activity+json');
|
|
26
|
+
|
|
27
|
+
const totalItems = await countOutboxItems(db, actorUri);
|
|
28
|
+
|
|
29
|
+
if (!page || isNaN(page)) {
|
|
30
|
+
return generateOutboxCollection(totalItems, domain, username);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const items = await getOutboxPage(db, actorUri, page, PAGE_SIZE);
|
|
34
|
+
return generateOutboxPage(items as never[], domain, username, page, PAGE_SIZE, totalItems);
|
|
35
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { getUserByUsername, getOrCreateActorKeypair } from '@commonpub/server';
|
|
2
|
+
|
|
3
|
+
export default defineEventHandler(async (event) => {
|
|
4
|
+
const username = getRouterParam(event, 'username')!;
|
|
5
|
+
const accept = getRequestHeader(event, 'accept') || '';
|
|
6
|
+
|
|
7
|
+
// Only serve AP actor for ActivityPub clients
|
|
8
|
+
if (!accept.includes('application/activity+json') && !accept.includes('application/ld+json')) {
|
|
9
|
+
// Redirect browsers to the profile page
|
|
10
|
+
return sendRedirect(event, `/u/${username}`);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const db = useDB();
|
|
14
|
+
const config = useConfig();
|
|
15
|
+
const profile = await getUserByUsername(db, username);
|
|
16
|
+
|
|
17
|
+
if (!profile) {
|
|
18
|
+
throw createError({ statusCode: 404, statusMessage: 'Actor not found' });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const domain = config.instance.domain;
|
|
22
|
+
const actorUri = `https://${domain}/users/${username}`;
|
|
23
|
+
|
|
24
|
+
let publicKeyPem = '';
|
|
25
|
+
try {
|
|
26
|
+
const keypair = await getOrCreateActorKeypair(db, profile.id);
|
|
27
|
+
publicKeyPem = keypair.publicKeyPem;
|
|
28
|
+
} catch {
|
|
29
|
+
// Key generation may fail if crypto is unavailable
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
setResponseHeader(event, 'content-type', 'application/activity+json');
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
'@context': [
|
|
36
|
+
'https://www.w3.org/ns/activitystreams',
|
|
37
|
+
'https://w3id.org/security/v1',
|
|
38
|
+
],
|
|
39
|
+
id: actorUri,
|
|
40
|
+
type: 'Person',
|
|
41
|
+
preferredUsername: username,
|
|
42
|
+
name: profile.displayName || username,
|
|
43
|
+
summary: profile.bio || '',
|
|
44
|
+
inbox: `${actorUri}/inbox`,
|
|
45
|
+
outbox: `${actorUri}/outbox`,
|
|
46
|
+
followers: `${actorUri}/followers`,
|
|
47
|
+
following: `${actorUri}/following`,
|
|
48
|
+
url: `https://${domain}/u/${username}`,
|
|
49
|
+
endpoints: {
|
|
50
|
+
sharedInbox: `https://${domain}/inbox`,
|
|
51
|
+
},
|
|
52
|
+
...(publicKeyPem
|
|
53
|
+
? {
|
|
54
|
+
publicKey: {
|
|
55
|
+
id: `${actorUri}#main-key`,
|
|
56
|
+
owner: actorUri,
|
|
57
|
+
publicKeyPem,
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
: {}),
|
|
61
|
+
};
|
|
62
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Auth helper — extracts authenticated user from event context
|
|
2
|
+
import type { H3Event } from 'h3';
|
|
3
|
+
|
|
4
|
+
export interface AuthUser {
|
|
5
|
+
id: string;
|
|
6
|
+
username: string;
|
|
7
|
+
role: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function requireAuth(event: H3Event): AuthUser {
|
|
11
|
+
const auth = event.context.auth;
|
|
12
|
+
if (!auth?.user) {
|
|
13
|
+
const cookie = getRequestHeader(event, 'cookie') || '';
|
|
14
|
+
const hasSessionCookie = cookie.includes('better-auth.session_token');
|
|
15
|
+
throw createError({
|
|
16
|
+
statusCode: 401,
|
|
17
|
+
statusMessage: hasSessionCookie
|
|
18
|
+
? 'Session expired or invalid. Please log in again.'
|
|
19
|
+
: 'Not logged in. Please log in to continue.',
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
return auth.user as AuthUser;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function requireAdmin(event: H3Event): AuthUser {
|
|
26
|
+
const user = requireAuth(event);
|
|
27
|
+
if (user.role !== 'admin') {
|
|
28
|
+
throw createError({ statusCode: 403, statusMessage: 'Admin access required' });
|
|
29
|
+
}
|
|
30
|
+
return user;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function getOptionalUser(event: H3Event): AuthUser | null {
|
|
34
|
+
const auth = event.context.auth;
|
|
35
|
+
return (auth?.user as AuthUser) ?? null;
|
|
36
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Singleton Drizzle DB instance for Nitro server
|
|
2
|
+
import { drizzle } from 'drizzle-orm/node-postgres';
|
|
3
|
+
// @ts-expect-error no types for pg
|
|
4
|
+
import pg from 'pg';
|
|
5
|
+
import * as schema from '@commonpub/schema';
|
|
6
|
+
import type { DB } from '@commonpub/server';
|
|
7
|
+
|
|
8
|
+
let db: DB | null = null;
|
|
9
|
+
|
|
10
|
+
export function useDB(): DB {
|
|
11
|
+
if (db) return db;
|
|
12
|
+
|
|
13
|
+
const config = useRuntimeConfig();
|
|
14
|
+
const databaseUrl = config.databaseUrl as string;
|
|
15
|
+
|
|
16
|
+
if (!databaseUrl) {
|
|
17
|
+
throw new Error('DATABASE_URL is not configured. Set NUXT_DATABASE_URL environment variable.');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Guard against default auth secret in production
|
|
21
|
+
if (process.env.NODE_ENV === 'production' && config.authSecret === 'dev-secret-change-me') {
|
|
22
|
+
throw new Error('NUXT_AUTH_SECRET must be set in production. Do not use the default dev secret.');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const pool = new pg.Pool({
|
|
26
|
+
connectionString: databaseUrl,
|
|
27
|
+
max: 20,
|
|
28
|
+
idleTimeoutMillis: 30_000,
|
|
29
|
+
connectionTimeoutMillis: 5_000,
|
|
30
|
+
});
|
|
31
|
+
db = drizzle(pool, { schema });
|
|
32
|
+
|
|
33
|
+
return db;
|
|
34
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Consistent error helpers for Nitro API routes
|
|
2
|
+
|
|
3
|
+
export function validationError(errors: Record<string, string[]>): never {
|
|
4
|
+
throw createError({
|
|
5
|
+
statusCode: 400,
|
|
6
|
+
statusMessage: 'Validation failed',
|
|
7
|
+
data: { errors },
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function notFound(entity: string): never {
|
|
12
|
+
throw createError({
|
|
13
|
+
statusCode: 404,
|
|
14
|
+
statusMessage: `${entity} not found`,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function forbidden(message = 'Permission denied'): never {
|
|
19
|
+
throw createError({ statusCode: 403, statusMessage: message });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function badRequest(message: string): never {
|
|
23
|
+
throw createError({ statusCode: 400, statusMessage: message });
|
|
24
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared inbox verification utilities.
|
|
3
|
+
* Handles HTTP Signature verification, actor domain validation,
|
|
4
|
+
* body size limits, and Date header freshness checks.
|
|
5
|
+
*/
|
|
6
|
+
import { verifyHttpSignature, resolveActor } from '@commonpub/protocol';
|
|
7
|
+
import type { H3Event } from 'h3';
|
|
8
|
+
|
|
9
|
+
/** Maximum allowed body size for inbox POSTs (1 MB) */
|
|
10
|
+
const MAX_BODY_SIZE = 1_048_576;
|
|
11
|
+
|
|
12
|
+
/** Maximum allowed clock skew for Date header (5 minutes) */
|
|
13
|
+
const MAX_DATE_SKEW_MS = 5 * 60 * 1000;
|
|
14
|
+
|
|
15
|
+
function extractKeyId(signatureHeader: string): string | null {
|
|
16
|
+
const match = signatureHeader.match(/keyId="([^"]+)"/);
|
|
17
|
+
return match ? match[1] : null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Extract clean domain from a URL string */
|
|
21
|
+
export function extractDomain(url: string): string {
|
|
22
|
+
try {
|
|
23
|
+
const parsed = new URL(url);
|
|
24
|
+
if (parsed.hostname) return parsed.hostname;
|
|
25
|
+
} catch { /* fall through */ }
|
|
26
|
+
return url.replace(/^https?:\/\//, '').replace(/[:/].*$/, '');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface VerifiedInbox {
|
|
30
|
+
actorUri: string;
|
|
31
|
+
body: Record<string, unknown>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Verify an inbound AP activity request.
|
|
36
|
+
* Checks: body size, signature presence, actor resolution, domain match,
|
|
37
|
+
* Date freshness, and HTTP Signature cryptographic verification.
|
|
38
|
+
*
|
|
39
|
+
* Throws H3Error on any failure.
|
|
40
|
+
*/
|
|
41
|
+
export async function verifyInboxRequest(event: H3Event, label: string): Promise<VerifiedInbox> {
|
|
42
|
+
// 1. Body size check
|
|
43
|
+
const contentLength = getHeader(event, 'content-length');
|
|
44
|
+
if (contentLength && parseInt(contentLength, 10) > MAX_BODY_SIZE) {
|
|
45
|
+
throw createError({ statusCode: 413, statusMessage: 'Payload too large' });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 2. Require Signature header
|
|
49
|
+
const signatureHeader = getHeader(event, 'signature');
|
|
50
|
+
if (!signatureHeader) {
|
|
51
|
+
throw createError({ statusCode: 401, statusMessage: 'Missing HTTP Signature' });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 3. Extract and validate keyId
|
|
55
|
+
const keyId = extractKeyId(signatureHeader);
|
|
56
|
+
if (!keyId) {
|
|
57
|
+
throw createError({ statusCode: 401, statusMessage: 'Invalid Signature header: missing keyId' });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const actorUri = keyId.replace(/#.*$/, '');
|
|
61
|
+
|
|
62
|
+
// 4. Resolve actor and public key
|
|
63
|
+
const actor = await resolveActor(actorUri, fetch);
|
|
64
|
+
if (!actor?.publicKey?.publicKeyPem) {
|
|
65
|
+
throw createError({ statusCode: 401, statusMessage: 'Could not resolve actor public key' });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 5. Actor domain validation — keyId domain must match resolved actor id domain
|
|
69
|
+
try {
|
|
70
|
+
const keyIdDomain = new URL(actorUri).hostname;
|
|
71
|
+
const actorIdDomain = new URL(actor.id ?? actorUri).hostname;
|
|
72
|
+
if (keyIdDomain !== actorIdDomain) {
|
|
73
|
+
console.warn(`[${label}] Domain mismatch: keyId=${keyIdDomain}, actor.id=${actorIdDomain}`);
|
|
74
|
+
throw createError({ statusCode: 401, statusMessage: 'Actor domain does not match keyId domain' });
|
|
75
|
+
}
|
|
76
|
+
} catch (err) {
|
|
77
|
+
if ((err as { statusCode?: number }).statusCode) throw err;
|
|
78
|
+
throw createError({ statusCode: 401, statusMessage: 'Invalid actor URI' });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 6. Date header freshness check
|
|
82
|
+
const dateHeader = getHeader(event, 'date');
|
|
83
|
+
if (dateHeader) {
|
|
84
|
+
const requestDate = new Date(dateHeader).getTime();
|
|
85
|
+
if (!isNaN(requestDate)) {
|
|
86
|
+
const skew = Math.abs(Date.now() - requestDate);
|
|
87
|
+
if (skew > MAX_DATE_SKEW_MS) {
|
|
88
|
+
console.warn(`[${label}] Date header too old/new: skew=${Math.round(skew / 1000)}s from ${actorUri}`);
|
|
89
|
+
throw createError({ statusCode: 401, statusMessage: 'Request date too far from server time' });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 7. Read body and reconstruct Request for signature verification
|
|
95
|
+
const body = await readBody(event);
|
|
96
|
+
const bodyStr = typeof body === 'string' ? body : JSON.stringify(body);
|
|
97
|
+
|
|
98
|
+
// Body size check on actual content (in case Content-Length was missing/wrong)
|
|
99
|
+
if (bodyStr.length > MAX_BODY_SIZE) {
|
|
100
|
+
throw createError({ statusCode: 413, statusMessage: 'Payload too large' });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const url = getRequestURL(event);
|
|
104
|
+
const headers = new Headers();
|
|
105
|
+
for (const [key, value] of Object.entries(getHeaders(event))) {
|
|
106
|
+
if (value) headers.set(key, Array.isArray(value) ? value[0]! : value);
|
|
107
|
+
}
|
|
108
|
+
const verifyRequest = new Request(url.toString(), {
|
|
109
|
+
method: 'POST',
|
|
110
|
+
headers,
|
|
111
|
+
body: bodyStr,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// 8. Verify HTTP Signature cryptographically
|
|
115
|
+
const signatureValid = await verifyHttpSignature(verifyRequest, actor.publicKey.publicKeyPem);
|
|
116
|
+
if (!signatureValid) {
|
|
117
|
+
console.warn(`[${label}] HTTP Signature verification failed for ${actorUri}`);
|
|
118
|
+
throw createError({ statusCode: 401, statusMessage: 'Invalid HTTP Signature' });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return { actorUri, body };
|
|
122
|
+
}
|