@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,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admin bootstrap plugin.
|
|
3
|
+
*
|
|
4
|
+
* In development: promotes the first registered user to admin.
|
|
5
|
+
* In production: promotes ADMIN_BOOTSTRAP_USER (env var) if no admin exists.
|
|
6
|
+
*
|
|
7
|
+
* This solves the bootstrap problem where no admin exists to promote users.
|
|
8
|
+
* The production path only runs ONCE (when admin count is 0) and requires
|
|
9
|
+
* an explicit env var, so it's safe to leave enabled.
|
|
10
|
+
*/
|
|
11
|
+
import { users } from '@commonpub/schema';
|
|
12
|
+
import { eq, asc, count } from 'drizzle-orm';
|
|
13
|
+
|
|
14
|
+
export default defineNitroPlugin((nitro) => {
|
|
15
|
+
// Run after a short delay so the DB pool is ready
|
|
16
|
+
setTimeout(async () => {
|
|
17
|
+
try {
|
|
18
|
+
const db = useDB();
|
|
19
|
+
|
|
20
|
+
// Count admin users
|
|
21
|
+
const [adminCount] = await db
|
|
22
|
+
.select({ count: count() })
|
|
23
|
+
.from(users)
|
|
24
|
+
.where(eq(users.role, 'admin'));
|
|
25
|
+
|
|
26
|
+
if (adminCount && adminCount.count > 0) return;
|
|
27
|
+
|
|
28
|
+
// No admins exist — bootstrap one
|
|
29
|
+
|
|
30
|
+
// Production: promote specific user from env var
|
|
31
|
+
const bootstrapUsername = process.env.ADMIN_BOOTSTRAP_USER;
|
|
32
|
+
if (process.env.NODE_ENV === 'production' && !bootstrapUsername) return;
|
|
33
|
+
|
|
34
|
+
let targetUser: { id: string; username: string } | undefined;
|
|
35
|
+
|
|
36
|
+
if (bootstrapUsername) {
|
|
37
|
+
// Promote the specified user
|
|
38
|
+
const [found] = await db
|
|
39
|
+
.select({ id: users.id, username: users.username })
|
|
40
|
+
.from(users)
|
|
41
|
+
.where(eq(users.username, bootstrapUsername))
|
|
42
|
+
.limit(1);
|
|
43
|
+
targetUser = found;
|
|
44
|
+
if (!targetUser) {
|
|
45
|
+
console.warn(`[auto-admin] ADMIN_BOOTSTRAP_USER="${bootstrapUsername}" not found`);
|
|
46
|
+
}
|
|
47
|
+
} else {
|
|
48
|
+
// Dev mode: promote first registered user
|
|
49
|
+
const [firstUser] = await db
|
|
50
|
+
.select({ id: users.id, username: users.username })
|
|
51
|
+
.from(users)
|
|
52
|
+
.orderBy(asc(users.createdAt))
|
|
53
|
+
.limit(1);
|
|
54
|
+
targetUser = firstUser;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!targetUser) return;
|
|
58
|
+
|
|
59
|
+
await db
|
|
60
|
+
.update(users)
|
|
61
|
+
.set({ role: 'admin' })
|
|
62
|
+
.where(eq(users.id, targetUser.id));
|
|
63
|
+
|
|
64
|
+
console.log(`[auto-admin] Promoted "${targetUser.username}" to admin`);
|
|
65
|
+
} catch {
|
|
66
|
+
// DB not ready yet — safe to ignore, will work on next restart
|
|
67
|
+
}
|
|
68
|
+
}, 2000);
|
|
69
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Federation activity delivery worker.
|
|
3
|
+
* Runs on a configurable interval to deliver pending outbound AP activities.
|
|
4
|
+
* Also runs daily cleanup of old delivered activities.
|
|
5
|
+
*/
|
|
6
|
+
import { deliverPendingActivities, cleanupDeliveredActivities } from '@commonpub/server';
|
|
7
|
+
|
|
8
|
+
export default defineNitroPlugin((nitro) => {
|
|
9
|
+
if (process.env.NODE_ENV === 'test') return;
|
|
10
|
+
|
|
11
|
+
let interval: ReturnType<typeof setInterval> | null = null;
|
|
12
|
+
let cleanupCounter = 0;
|
|
13
|
+
|
|
14
|
+
const startupTimer = setTimeout(() => {
|
|
15
|
+
try {
|
|
16
|
+
const config = useConfig();
|
|
17
|
+
if (!config.features.federation) {
|
|
18
|
+
console.log('[federation] Federation disabled — delivery worker not started');
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const runtimeConfig = useRuntimeConfig();
|
|
23
|
+
const siteUrl = (runtimeConfig.public?.siteUrl as string) || `https://${config.instance.domain}`;
|
|
24
|
+
const domain = siteUrl.replace(/^https?:\/\//, '').replace(/[:/].*$/, '');
|
|
25
|
+
|
|
26
|
+
const fedConfig = config.federation ?? {};
|
|
27
|
+
const intervalMs = fedConfig.deliveryIntervalMs ?? 30_000;
|
|
28
|
+
const batchSize = fedConfig.deliveryBatchSize ?? 20;
|
|
29
|
+
const maxRetries = fedConfig.maxDeliveryRetries ?? 6;
|
|
30
|
+
const retentionDays = fedConfig.activityRetentionDays ?? 90;
|
|
31
|
+
|
|
32
|
+
console.log(`[federation] Activity delivery worker started (domain: ${domain}, interval: ${intervalMs}ms, batch: ${batchSize})`);
|
|
33
|
+
|
|
34
|
+
runDelivery(domain, batchSize, maxRetries, retentionDays);
|
|
35
|
+
|
|
36
|
+
interval = setInterval(() => runDelivery(domain, batchSize, maxRetries, retentionDays), intervalMs);
|
|
37
|
+
} catch (err) {
|
|
38
|
+
console.error('[federation] Delivery worker failed to start:', err instanceof Error ? err.message : err);
|
|
39
|
+
}
|
|
40
|
+
}, 5_000);
|
|
41
|
+
|
|
42
|
+
async function runDelivery(domain: string, batchSize: number, maxRetries: number, retentionDays: number) {
|
|
43
|
+
try {
|
|
44
|
+
const db = useDB();
|
|
45
|
+
const result = await deliverPendingActivities(db, domain, { batchSize, maxRetries });
|
|
46
|
+
if (result.delivered > 0 || result.failed > 0) {
|
|
47
|
+
console.log(`[federation] Delivered: ${result.delivered}, Failed: ${result.failed}`);
|
|
48
|
+
}
|
|
49
|
+
if (result.errors.length > 0) {
|
|
50
|
+
console.warn('[federation] Delivery errors:', result.errors.slice(0, 3).join('; '));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
cleanupCounter++;
|
|
54
|
+
const cleanupEvery = Math.max(1, Math.round((24 * 60 * 60 * 1000) / (30_000)));
|
|
55
|
+
if (cleanupCounter >= cleanupEvery) {
|
|
56
|
+
cleanupCounter = 0;
|
|
57
|
+
const deleted = await cleanupDeliveredActivities(db, retentionDays);
|
|
58
|
+
if (deleted > 0) {
|
|
59
|
+
console.log(`[federation] Cleaned up ${deleted} old delivered activities`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
} catch (err) {
|
|
63
|
+
console.error('[federation] Delivery worker error:', err instanceof Error ? err.message : err);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
nitro.hooks.hook('close', () => {
|
|
68
|
+
clearTimeout(startupTimer);
|
|
69
|
+
if (interval) {
|
|
70
|
+
clearInterval(interval);
|
|
71
|
+
interval = null;
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// NodeInfo discovery endpoint
|
|
2
|
+
// Returns links to the NodeInfo document
|
|
3
|
+
|
|
4
|
+
export default defineEventHandler((event) => {
|
|
5
|
+
const origin = getRequestURL(event).origin;
|
|
6
|
+
|
|
7
|
+
return {
|
|
8
|
+
links: [
|
|
9
|
+
{
|
|
10
|
+
rel: 'http://nodeinfo.diaspora.software/ns/schema/2.1',
|
|
11
|
+
href: `${origin}/nodeinfo/2.1`,
|
|
12
|
+
},
|
|
13
|
+
],
|
|
14
|
+
};
|
|
15
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// WebFinger endpoint — RFC 7033
|
|
2
|
+
import { parseWebFingerResource, buildWebFingerResponse } from '@commonpub/protocol';
|
|
3
|
+
import { getUserByUsername, getHubBySlug } from '@commonpub/server';
|
|
4
|
+
|
|
5
|
+
export default defineEventHandler(async (event) => {
|
|
6
|
+
const query = getQuery(event);
|
|
7
|
+
const resource = query.resource as string | undefined;
|
|
8
|
+
|
|
9
|
+
if (!resource) {
|
|
10
|
+
throw createError({
|
|
11
|
+
statusCode: 400,
|
|
12
|
+
statusMessage: 'Missing resource parameter',
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const parsed = parseWebFingerResource(resource);
|
|
17
|
+
if (!parsed) {
|
|
18
|
+
throw createError({
|
|
19
|
+
statusCode: 400,
|
|
20
|
+
statusMessage: 'Invalid resource format. Expected acct:user@domain',
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const requestUrl = getRequestURL(event);
|
|
25
|
+
const instanceDomain = requestUrl.host;
|
|
26
|
+
|
|
27
|
+
if (parsed.domain !== instanceDomain) {
|
|
28
|
+
throw createError({
|
|
29
|
+
statusCode: 404,
|
|
30
|
+
statusMessage: 'Resource not found on this instance',
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const config = useConfig();
|
|
35
|
+
|
|
36
|
+
// CORS — WebFinger must be accessible from browser-based AP clients
|
|
37
|
+
setResponseHeader(event, 'access-control-allow-origin', '*');
|
|
38
|
+
|
|
39
|
+
// Instance actor lookup: acct:domain@domain → /actor Service
|
|
40
|
+
if (parsed.username === instanceDomain || parsed.username === config.instance.domain) {
|
|
41
|
+
const actorUri = `https://${instanceDomain}/actor`;
|
|
42
|
+
setResponseHeader(event, 'content-type', 'application/jrd+json');
|
|
43
|
+
return buildWebFingerResponse({
|
|
44
|
+
username: instanceDomain,
|
|
45
|
+
domain: instanceDomain,
|
|
46
|
+
actorUri,
|
|
47
|
+
oauthEndpoint: `https://${instanceDomain}/api/auth/oauth2/authorize`,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Look up user in database
|
|
52
|
+
const db = useDB();
|
|
53
|
+
const profile = await getUserByUsername(db, parsed.username);
|
|
54
|
+
|
|
55
|
+
// If not a user, try hub lookup (Group actor)
|
|
56
|
+
if (!profile && config.features.federation && config.features.federateHubs) {
|
|
57
|
+
const hub = await getHubBySlug(db, parsed.username);
|
|
58
|
+
if (hub) {
|
|
59
|
+
const hubActorUri = `https://${instanceDomain}/hubs/${parsed.username}`;
|
|
60
|
+
setResponseHeader(event, 'content-type', 'application/jrd+json');
|
|
61
|
+
return buildWebFingerResponse({
|
|
62
|
+
username: parsed.username,
|
|
63
|
+
domain: instanceDomain,
|
|
64
|
+
actorUri: hubActorUri,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!profile) {
|
|
70
|
+
throw createError({
|
|
71
|
+
statusCode: 404,
|
|
72
|
+
statusMessage: 'User not found',
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const actorUri = `https://${instanceDomain}/users/${parsed.username}`;
|
|
77
|
+
|
|
78
|
+
setResponseHeader(event, 'content-type', 'application/jrd+json');
|
|
79
|
+
|
|
80
|
+
return buildWebFingerResponse({
|
|
81
|
+
username: parsed.username,
|
|
82
|
+
domain: instanceDomain,
|
|
83
|
+
actorUri,
|
|
84
|
+
oauthEndpoint: `https://${instanceDomain}/api/auth/oauth2/authorize`,
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { getFollowers } from '@commonpub/server';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Instance actor followers collection.
|
|
5
|
+
* Returns remote actors that follow this instance (for mirroring).
|
|
6
|
+
*/
|
|
7
|
+
export default defineEventHandler(async (event) => {
|
|
8
|
+
const config = useConfig();
|
|
9
|
+
if (!config.features.federation) {
|
|
10
|
+
throw createError({ statusCode: 404, statusMessage: 'Not Found' });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const db = useDB();
|
|
14
|
+
const domain = config.instance.domain;
|
|
15
|
+
const actorUri = `https://${domain}/actor`;
|
|
16
|
+
|
|
17
|
+
let followers: string[] = [];
|
|
18
|
+
try {
|
|
19
|
+
const result = await getFollowers(db, actorUri);
|
|
20
|
+
followers = result.map((f) => f.followerActorUri);
|
|
21
|
+
} catch {
|
|
22
|
+
// Federation tables may not exist yet
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
setResponseHeader(event, 'content-type', 'application/activity+json');
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
'@context': 'https://www.w3.org/ns/activitystreams',
|
|
29
|
+
id: `${actorUri}/followers`,
|
|
30
|
+
type: 'OrderedCollection',
|
|
31
|
+
totalItems: followers.length,
|
|
32
|
+
orderedItems: followers,
|
|
33
|
+
};
|
|
34
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { getFollowing } from '@commonpub/server';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Instance actor following collection.
|
|
5
|
+
* Returns remote actors that this instance follows (mirrors).
|
|
6
|
+
*/
|
|
7
|
+
export default defineEventHandler(async (event) => {
|
|
8
|
+
const config = useConfig();
|
|
9
|
+
if (!config.features.federation) {
|
|
10
|
+
throw createError({ statusCode: 404, statusMessage: 'Not Found' });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const db = useDB();
|
|
14
|
+
const domain = config.instance.domain;
|
|
15
|
+
const actorUri = `https://${domain}/actor`;
|
|
16
|
+
|
|
17
|
+
let following: string[] = [];
|
|
18
|
+
try {
|
|
19
|
+
const result = await getFollowing(db, actorUri);
|
|
20
|
+
following = result.map((f) => f.followingActorUri);
|
|
21
|
+
} catch {
|
|
22
|
+
// Federation tables may not exist yet
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
setResponseHeader(event, 'content-type', 'application/activity+json');
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
'@context': 'https://www.w3.org/ns/activitystreams',
|
|
29
|
+
id: `${actorUri}/following`,
|
|
30
|
+
type: 'OrderedCollection',
|
|
31
|
+
totalItems: following.length,
|
|
32
|
+
orderedItems: following,
|
|
33
|
+
};
|
|
34
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { generateOutboxCollection, generateOutboxPage } from '@commonpub/protocol';
|
|
2
|
+
import { countInstanceOutboxItems, getInstanceOutboxPage } from '@commonpub/server';
|
|
3
|
+
|
|
4
|
+
const PAGE_SIZE = 20;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Instance actor outbox — serves paginated outbound Create activities
|
|
8
|
+
* from all users on this instance. Used by backfill crawlers.
|
|
9
|
+
*/
|
|
10
|
+
export default defineEventHandler(async (event) => {
|
|
11
|
+
const config = useConfig();
|
|
12
|
+
if (!config.features.federation) {
|
|
13
|
+
throw createError({ statusCode: 404, statusMessage: 'Not Found' });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const db = useDB();
|
|
17
|
+
const domain = config.instance.domain;
|
|
18
|
+
const query = getQuery(event);
|
|
19
|
+
const page = query.page ? parseInt(String(query.page), 10) : 0;
|
|
20
|
+
|
|
21
|
+
setResponseHeader(event, 'content-type', 'application/activity+json');
|
|
22
|
+
|
|
23
|
+
const totalItems = await countInstanceOutboxItems(db, domain);
|
|
24
|
+
|
|
25
|
+
if (!page || isNaN(page)) {
|
|
26
|
+
return generateOutboxCollection(totalItems, domain, null);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const items = await getInstanceOutboxPage(db, domain, page, PAGE_SIZE);
|
|
30
|
+
return generateOutboxPage(items as never[], domain, null, page, PAGE_SIZE, totalItems);
|
|
31
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { getOrCreateInstanceKeypair, buildInstanceActor } from '@commonpub/server';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Instance actor endpoint — returns the AP Service object for this instance.
|
|
5
|
+
* Used by other instances for mirroring (Follow the instance actor to receive all content).
|
|
6
|
+
* Content-negotiated: AP clients get JSON-LD, browsers get redirected to /about.
|
|
7
|
+
*/
|
|
8
|
+
export default defineEventHandler(async (event) => {
|
|
9
|
+
const config = useConfig();
|
|
10
|
+
if (!config.features.federation) {
|
|
11
|
+
throw createError({ statusCode: 404, statusMessage: 'Not Found' });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const accept = getRequestHeader(event, 'accept') ?? '';
|
|
15
|
+
const isAPRequest =
|
|
16
|
+
accept.includes('application/activity+json') ||
|
|
17
|
+
accept.includes('application/ld+json');
|
|
18
|
+
|
|
19
|
+
if (!isAPRequest) {
|
|
20
|
+
// Browser request — redirect to about page
|
|
21
|
+
return sendRedirect(event, '/about', 302);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const db = useDB();
|
|
25
|
+
const { publicKeyPem } = await getOrCreateInstanceKeypair(db);
|
|
26
|
+
const actor = buildInstanceActor(config.instance.domain, publicKeyPem);
|
|
27
|
+
|
|
28
|
+
setResponseHeader(event, 'content-type', 'application/activity+json');
|
|
29
|
+
return actor;
|
|
30
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { listContent } from '@commonpub/server';
|
|
2
|
+
|
|
3
|
+
function escapeXml(str: string): string {
|
|
4
|
+
return str
|
|
5
|
+
.replace(/&/g, '&')
|
|
6
|
+
.replace(/</g, '<')
|
|
7
|
+
.replace(/>/g, '>')
|
|
8
|
+
.replace(/"/g, '"')
|
|
9
|
+
.replace(/'/g, ''');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default defineEventHandler(async (event) => {
|
|
13
|
+
const db = useDB();
|
|
14
|
+
const config = useRuntimeConfig();
|
|
15
|
+
const siteUrl = config.public.siteUrl as string;
|
|
16
|
+
const siteName = config.public.siteName as string;
|
|
17
|
+
const siteDescription = config.public.siteDescription as string;
|
|
18
|
+
|
|
19
|
+
const { items } = await listContent(db, {
|
|
20
|
+
status: 'published',
|
|
21
|
+
sort: 'recent',
|
|
22
|
+
limit: 50,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const lastBuildDate = items.length > 0
|
|
26
|
+
? new Date(items[0].publishedAt ?? items[0].createdAt).toUTCString()
|
|
27
|
+
: new Date().toUTCString();
|
|
28
|
+
|
|
29
|
+
const rssItems = items.map((item) => {
|
|
30
|
+
const link = `${siteUrl}/${item.type}/${item.slug}`;
|
|
31
|
+
const pubDate = new Date(item.publishedAt ?? item.createdAt).toUTCString();
|
|
32
|
+
return ` <item>
|
|
33
|
+
<title>${escapeXml(item.title)}</title>
|
|
34
|
+
<link>${escapeXml(link)}</link>
|
|
35
|
+
<guid isPermaLink="true">${escapeXml(link)}</guid>
|
|
36
|
+
<pubDate>${pubDate}</pubDate>
|
|
37
|
+
<description>${escapeXml(item.description ?? '')}</description>
|
|
38
|
+
<author>${escapeXml(item.author.displayName ?? item.author.username)}</author>
|
|
39
|
+
<category>${escapeXml(item.type)}</category>
|
|
40
|
+
</item>`;
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
44
|
+
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
|
45
|
+
<channel>
|
|
46
|
+
<title>${escapeXml(siteName)}</title>
|
|
47
|
+
<link>${escapeXml(siteUrl)}</link>
|
|
48
|
+
<description>${escapeXml(siteDescription)}</description>
|
|
49
|
+
<language>en</language>
|
|
50
|
+
<lastBuildDate>${lastBuildDate}</lastBuildDate>
|
|
51
|
+
<atom:link href="${escapeXml(siteUrl)}/feed.xml" rel="self" type="application/rss+xml"/>
|
|
52
|
+
${rssItems.join('\n')}
|
|
53
|
+
</channel>
|
|
54
|
+
</rss>`;
|
|
55
|
+
|
|
56
|
+
setResponseHeader(event, 'Content-Type', 'application/rss+xml; charset=utf-8');
|
|
57
|
+
setResponseHeader(event, 'Cache-Control', 'public, max-age=600, stale-while-revalidate=300');
|
|
58
|
+
return xml;
|
|
59
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { getHubBySlug, getHubFederatedFollowers, getHubActorUri } from '@commonpub/server';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Hub Group actor followers collection.
|
|
5
|
+
* Returns an OrderedCollection of remote actors following this hub.
|
|
6
|
+
*/
|
|
7
|
+
export default defineEventHandler(async (event) => {
|
|
8
|
+
const config = useConfig();
|
|
9
|
+
if (!config.features.federation || !config.features.federateHubs) {
|
|
10
|
+
throw createError({ statusCode: 404, statusMessage: 'Not Found' });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const slug = getRouterParam(event, 'slug');
|
|
14
|
+
if (!slug) {
|
|
15
|
+
throw createError({ statusCode: 404, statusMessage: 'Not Found' });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const db = useDB();
|
|
19
|
+
const domain = config.instance.domain;
|
|
20
|
+
|
|
21
|
+
const hub = await getHubBySlug(db, slug);
|
|
22
|
+
if (!hub) {
|
|
23
|
+
throw createError({ statusCode: 404, statusMessage: 'Hub not found' });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const hubActorUri = getHubActorUri(domain, slug);
|
|
27
|
+
const followers = await getHubFederatedFollowers(db, hub.id);
|
|
28
|
+
const followerUris = followers.map((f) => f.followerActorUri);
|
|
29
|
+
|
|
30
|
+
setResponseHeader(event, 'content-type', 'application/activity+json');
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
'@context': 'https://www.w3.org/ns/activitystreams',
|
|
34
|
+
id: `${hubActorUri}/followers`,
|
|
35
|
+
type: 'OrderedCollection',
|
|
36
|
+
totalItems: followerUris.length,
|
|
37
|
+
orderedItems: followerUris,
|
|
38
|
+
};
|
|
39
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { processInboxActivity } from '@commonpub/protocol';
|
|
2
|
+
import { createInboxHandlers } from '@commonpub/server';
|
|
3
|
+
import { verifyInboxRequest } from '../../../utils/inbox';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Hub-specific inbox endpoint (FEP-1b12).
|
|
7
|
+
* Receives activities directed at the hub Group actor.
|
|
8
|
+
* Delegates to the same inbox handlers as the shared inbox.
|
|
9
|
+
*/
|
|
10
|
+
export default defineEventHandler(async (event) => {
|
|
11
|
+
const config = useConfig();
|
|
12
|
+
if (!config.features.federation || !config.features.federateHubs) {
|
|
13
|
+
throw createError({ statusCode: 404, statusMessage: 'Not Found' });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (event.method !== 'POST') {
|
|
17
|
+
throw createError({ statusCode: 405, statusMessage: 'Method Not Allowed' });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Verify signature, domain, date freshness, body size
|
|
21
|
+
const { body } = await verifyInboxRequest(event, 'hub-inbox');
|
|
22
|
+
|
|
23
|
+
const db = useDB();
|
|
24
|
+
const domain = config.instance.domain;
|
|
25
|
+
const slug = getRouterParam(event, 'slug');
|
|
26
|
+
const handlers = createInboxHandlers({ db, domain, hubContext: slug ? { hubSlug: slug } : undefined });
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
await processInboxActivity(body, handlers);
|
|
30
|
+
return { status: 'accepted' };
|
|
31
|
+
} catch (err) {
|
|
32
|
+
console.error('[hub-inbox] Activity processing error:', err);
|
|
33
|
+
throw createError({ statusCode: 400, statusMessage: 'Activity processing failed' });
|
|
34
|
+
}
|
|
35
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { generateOutboxCollection, generateOutboxPage } from '@commonpub/protocol';
|
|
2
|
+
import { getHubBySlug, countHubOutboxItems, getHubOutboxPage, getHubActorUri } from '@commonpub/server';
|
|
3
|
+
|
|
4
|
+
const PAGE_SIZE = 20;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Hub Group actor outbox — serves paginated outbound Announce activities.
|
|
8
|
+
*/
|
|
9
|
+
export default defineEventHandler(async (event) => {
|
|
10
|
+
const config = useConfig();
|
|
11
|
+
if (!config.features.federation || !config.features.federateHubs) {
|
|
12
|
+
throw createError({ statusCode: 404, statusMessage: 'Not Found' });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const slug = getRouterParam(event, 'slug');
|
|
16
|
+
if (!slug) {
|
|
17
|
+
throw createError({ statusCode: 404, statusMessage: 'Not Found' });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const db = useDB();
|
|
21
|
+
const domain = config.instance.domain;
|
|
22
|
+
|
|
23
|
+
const hub = await getHubBySlug(db, slug);
|
|
24
|
+
if (!hub) {
|
|
25
|
+
throw createError({ statusCode: 404, statusMessage: 'Hub not found' });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const hubActorUri = getHubActorUri(domain, slug);
|
|
29
|
+
const outboxUri = `${hubActorUri}/outbox`;
|
|
30
|
+
const query = getQuery(event);
|
|
31
|
+
const page = query.page ? parseInt(String(query.page), 10) : 0;
|
|
32
|
+
|
|
33
|
+
setResponseHeader(event, 'content-type', 'application/activity+json');
|
|
34
|
+
|
|
35
|
+
const totalItems = await countHubOutboxItems(db, hubActorUri);
|
|
36
|
+
|
|
37
|
+
if (!page || isNaN(page)) {
|
|
38
|
+
return generateOutboxCollection(totalItems, domain, null, outboxUri);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const items = await getHubOutboxPage(db, hubActorUri, page, PAGE_SIZE);
|
|
42
|
+
return generateOutboxPage(items as never[], domain, null, page, PAGE_SIZE, totalItems, outboxUri);
|
|
43
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { getHubBySlug, getHubActorUri, getHubPostNoteUri } from '@commonpub/server';
|
|
2
|
+
import { AP_CONTEXT, AP_PUBLIC, escapeHtmlForAP } from '@commonpub/protocol';
|
|
3
|
+
import { hubPosts, users } from '@commonpub/schema';
|
|
4
|
+
import { eq } from 'drizzle-orm';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Hub post AP Note endpoint.
|
|
8
|
+
* Serves the Note JSON-LD when requested with AP Accept header.
|
|
9
|
+
* Remote instances dereference this URI when processing Announce activities.
|
|
10
|
+
*/
|
|
11
|
+
export default defineEventHandler(async (event) => {
|
|
12
|
+
const accept = getRequestHeader(event, 'accept') ?? '';
|
|
13
|
+
const isAPRequest =
|
|
14
|
+
accept.includes('application/activity+json') ||
|
|
15
|
+
accept.includes('application/ld+json');
|
|
16
|
+
|
|
17
|
+
if (!isAPRequest) return;
|
|
18
|
+
|
|
19
|
+
const config = useConfig();
|
|
20
|
+
if (!config.features.federation || !config.features.federateHubs) return;
|
|
21
|
+
|
|
22
|
+
const slug = getRouterParam(event, 'slug');
|
|
23
|
+
const postId = getRouterParam(event, 'postId');
|
|
24
|
+
if (!slug || !postId) return;
|
|
25
|
+
|
|
26
|
+
const db = useDB();
|
|
27
|
+
const domain = config.instance.domain;
|
|
28
|
+
|
|
29
|
+
const hub = await getHubBySlug(db, slug);
|
|
30
|
+
if (!hub) return;
|
|
31
|
+
|
|
32
|
+
const [post] = await db
|
|
33
|
+
.select({
|
|
34
|
+
id: hubPosts.id,
|
|
35
|
+
content: hubPosts.content,
|
|
36
|
+
createdAt: hubPosts.createdAt,
|
|
37
|
+
authorUsername: users.username,
|
|
38
|
+
})
|
|
39
|
+
.from(hubPosts)
|
|
40
|
+
.innerJoin(users, eq(hubPosts.authorId, users.id))
|
|
41
|
+
.where(eq(hubPosts.id, postId))
|
|
42
|
+
.limit(1);
|
|
43
|
+
|
|
44
|
+
if (!post) return;
|
|
45
|
+
|
|
46
|
+
const hubActorUri = getHubActorUri(domain, slug);
|
|
47
|
+
const noteUri = getHubPostNoteUri(domain, slug, postId);
|
|
48
|
+
const actorUri = `https://${domain}/users/${post.authorUsername}`;
|
|
49
|
+
|
|
50
|
+
setResponseHeader(event, 'content-type', 'application/activity+json');
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
'@context': AP_CONTEXT,
|
|
54
|
+
type: 'Note',
|
|
55
|
+
id: noteUri,
|
|
56
|
+
attributedTo: actorUri,
|
|
57
|
+
content: escapeHtmlForAP(post.content),
|
|
58
|
+
published: post.createdAt.toISOString(),
|
|
59
|
+
to: [AP_PUBLIC],
|
|
60
|
+
cc: [`${hubActorUri}/followers`],
|
|
61
|
+
context: hubActorUri,
|
|
62
|
+
};
|
|
63
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { buildHubGroupActor } from '@commonpub/server';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Hub Group actor endpoint. Returns AP Group JSON-LD for federation.
|
|
5
|
+
* Only responds to ActivityPub clients (Accept: application/activity+json).
|
|
6
|
+
* Browser requests pass through to the Nuxt page handler.
|
|
7
|
+
*/
|
|
8
|
+
export default defineEventHandler(async (event) => {
|
|
9
|
+
const accept = getRequestHeader(event, 'accept') ?? '';
|
|
10
|
+
const isAPRequest =
|
|
11
|
+
accept.includes('application/activity+json') ||
|
|
12
|
+
accept.includes('application/ld+json');
|
|
13
|
+
|
|
14
|
+
// Only handle AP requests — let browser requests fall through to the page handler
|
|
15
|
+
if (!isAPRequest) return;
|
|
16
|
+
|
|
17
|
+
const config = useConfig();
|
|
18
|
+
if (!config.features.federation || !config.features.federateHubs) return;
|
|
19
|
+
|
|
20
|
+
const slug = getRouterParam(event, 'slug');
|
|
21
|
+
if (!slug) return;
|
|
22
|
+
|
|
23
|
+
const db = useDB();
|
|
24
|
+
const actor = await buildHubGroupActor(db, slug, config.instance.domain);
|
|
25
|
+
if (!actor) return;
|
|
26
|
+
|
|
27
|
+
setResponseHeader(event, 'content-type', 'application/activity+json');
|
|
28
|
+
return actor;
|
|
29
|
+
});
|