@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.
Files changed (436) hide show
  1. package/LICENSE +661 -0
  2. package/app.vue +7 -0
  3. package/components/AnnouncementBand.vue +117 -0
  4. package/components/AppToast.vue +108 -0
  5. package/components/AuthorCard.vue +119 -0
  6. package/components/AuthorRow.vue +81 -0
  7. package/components/CommentSection.vue +330 -0
  8. package/components/ContentCard.vue +340 -0
  9. package/components/ContentPicker.vue +240 -0
  10. package/components/ContentStarterForm.vue +214 -0
  11. package/components/ContentTypeBadge.vue +46 -0
  12. package/components/CountdownTimer.vue +68 -0
  13. package/components/CpubEditor.vue +87 -0
  14. package/components/DiscussionItem.vue +191 -0
  15. package/components/EditorPropertiesPanel.vue +393 -0
  16. package/components/EngagementBar.vue +131 -0
  17. package/components/FederatedContentCard.vue +291 -0
  18. package/components/FeedItem.vue +283 -0
  19. package/components/FilterChip.vue +21 -0
  20. package/components/HeatmapGrid.vue +92 -0
  21. package/components/ImageUpload.vue +219 -0
  22. package/components/MemberCard.vue +163 -0
  23. package/components/MessageThread.vue +120 -0
  24. package/components/NotificationItem.vue +103 -0
  25. package/components/ProgressTracker.vue +41 -0
  26. package/components/PublishErrorsModal.vue +116 -0
  27. package/components/RemoteActorCard.vue +206 -0
  28. package/components/RemoteUserSearch.vue +117 -0
  29. package/components/SearchFilters.vue +188 -0
  30. package/components/SearchSidebar.vue +181 -0
  31. package/components/SectionHeader.vue +17 -0
  32. package/components/ShareToHubModal.vue +189 -0
  33. package/components/SiteLogo.vue +21 -0
  34. package/components/SkillBar.vue +57 -0
  35. package/components/SortSelect.vue +30 -0
  36. package/components/StatBar.vue +14 -0
  37. package/components/TOCNav.vue +69 -0
  38. package/components/TimelineItem.vue +82 -0
  39. package/components/VideoCard.vue +106 -0
  40. package/components/blocks/BlockBuildStepView.vue +92 -0
  41. package/components/blocks/BlockCalloutView.vue +82 -0
  42. package/components/blocks/BlockCheckpointView.vue +50 -0
  43. package/components/blocks/BlockCodeView.vue +212 -0
  44. package/components/blocks/BlockContentRenderer.vue +143 -0
  45. package/components/blocks/BlockDividerView.vue +11 -0
  46. package/components/blocks/BlockDownloadsView.vue +126 -0
  47. package/components/blocks/BlockEmbedView.vue +61 -0
  48. package/components/blocks/BlockGalleryView.vue +57 -0
  49. package/components/blocks/BlockHeadingView.vue +29 -0
  50. package/components/blocks/BlockImageView.vue +34 -0
  51. package/components/blocks/BlockMarkdownView.vue +118 -0
  52. package/components/blocks/BlockMathView.vue +45 -0
  53. package/components/blocks/BlockPartsListView.vue +104 -0
  54. package/components/blocks/BlockQuizView.vue +239 -0
  55. package/components/blocks/BlockQuoteView.vue +41 -0
  56. package/components/blocks/BlockSectionHeaderView.vue +58 -0
  57. package/components/blocks/BlockSliderView.vue +236 -0
  58. package/components/blocks/BlockTextView.vue +41 -0
  59. package/components/blocks/BlockToolListView.vue +87 -0
  60. package/components/blocks/BlockVideoView.vue +89 -0
  61. package/components/editors/ArticleEditor.vue +545 -0
  62. package/components/editors/BlockCanvas.vue +487 -0
  63. package/components/editors/BlockInsertZone.vue +84 -0
  64. package/components/editors/BlockPicker.vue +285 -0
  65. package/components/editors/BlockWrapper.vue +192 -0
  66. package/components/editors/BlogEditor.vue +567 -0
  67. package/components/editors/EditorBlocks.vue +248 -0
  68. package/components/editors/EditorSection.vue +81 -0
  69. package/components/editors/EditorShell.vue +168 -0
  70. package/components/editors/EditorTagInput.vue +114 -0
  71. package/components/editors/EditorVisibility.vue +110 -0
  72. package/components/editors/ExplainerEditor.vue +503 -0
  73. package/components/editors/MarkdownImportDialog.vue +249 -0
  74. package/components/editors/ProjectEditor.vue +446 -0
  75. package/components/editors/blocks/BuildStepBlock.vue +102 -0
  76. package/components/editors/blocks/CalloutBlock.vue +122 -0
  77. package/components/editors/blocks/CheckpointBlock.vue +27 -0
  78. package/components/editors/blocks/CodeBlock.vue +177 -0
  79. package/components/editors/blocks/DividerBlock.vue +22 -0
  80. package/components/editors/blocks/DownloadsBlock.vue +41 -0
  81. package/components/editors/blocks/EmbedBlock.vue +20 -0
  82. package/components/editors/blocks/GalleryBlock.vue +236 -0
  83. package/components/editors/blocks/HeadingBlock.vue +96 -0
  84. package/components/editors/blocks/ImageBlock.vue +271 -0
  85. package/components/editors/blocks/MarkdownBlock.vue +258 -0
  86. package/components/editors/blocks/MathBlock.vue +37 -0
  87. package/components/editors/blocks/PartsListBlock.vue +358 -0
  88. package/components/editors/blocks/QuizBlock.vue +47 -0
  89. package/components/editors/blocks/QuoteBlock.vue +101 -0
  90. package/components/editors/blocks/SectionHeaderBlock.vue +130 -0
  91. package/components/editors/blocks/SliderBlock.vue +318 -0
  92. package/components/editors/blocks/TextBlock.vue +201 -0
  93. package/components/editors/blocks/ToolListBlock.vue +70 -0
  94. package/components/editors/blocks/VideoBlock.vue +22 -0
  95. package/components/hub/HubDiscussions.vue +47 -0
  96. package/components/hub/HubFeed.vue +199 -0
  97. package/components/hub/HubHero.vue +185 -0
  98. package/components/hub/HubLayout.vue +103 -0
  99. package/components/hub/HubMembers.vue +40 -0
  100. package/components/hub/HubProducts.vue +93 -0
  101. package/components/hub/HubProjects.vue +207 -0
  102. package/components/hub/HubSidebar.vue +12 -0
  103. package/components/hub/HubSidebarCard.vue +35 -0
  104. package/components/views/ArticleView.vue +771 -0
  105. package/components/views/BlogView.vue +667 -0
  106. package/components/views/ExplainerView.vue +688 -0
  107. package/components/views/ProjectView.vue +1500 -0
  108. package/composables/useApiError.ts +39 -0
  109. package/composables/useAuth.ts +87 -0
  110. package/composables/useBlockEditor.ts +187 -0
  111. package/composables/useContentSave.ts +253 -0
  112. package/composables/useContentTypes.ts +37 -0
  113. package/composables/useEngagement.ts +196 -0
  114. package/composables/useFeatures.ts +33 -0
  115. package/composables/useFederation.ts +72 -0
  116. package/composables/useJsonLd.ts +183 -0
  117. package/composables/useMarkdownImport.ts +77 -0
  118. package/composables/useMirrorContent.ts +105 -0
  119. package/composables/useNotifications.ts +73 -0
  120. package/composables/usePublishValidation.ts +65 -0
  121. package/composables/useSanitize.ts +34 -0
  122. package/composables/useSiteName.ts +4 -0
  123. package/composables/useTheme.ts +34 -0
  124. package/composables/useToast.ts +35 -0
  125. package/error.vue +129 -0
  126. package/layouts/admin.vue +213 -0
  127. package/layouts/auth.vue +63 -0
  128. package/layouts/default.vue +269 -0
  129. package/layouts/editor.vue +129 -0
  130. package/middleware/auth.ts +6 -0
  131. package/nuxt.config.ts +83 -0
  132. package/package.json +59 -0
  133. package/pages/[type]/[slug]/edit.vue +676 -0
  134. package/pages/[type]/[slug]/index.vue +313 -0
  135. package/pages/[type]/index.vue +118 -0
  136. package/pages/about.vue +100 -0
  137. package/pages/admin/audit.vue +66 -0
  138. package/pages/admin/content.vue +116 -0
  139. package/pages/admin/federation.vue +446 -0
  140. package/pages/admin/index.vue +62 -0
  141. package/pages/admin/reports.vue +88 -0
  142. package/pages/admin/settings.vue +167 -0
  143. package/pages/admin/users.vue +145 -0
  144. package/pages/auth/forgot-password.vue +103 -0
  145. package/pages/auth/login.vue +216 -0
  146. package/pages/auth/oauth/authorize.vue +178 -0
  147. package/pages/auth/register.vue +246 -0
  148. package/pages/auth/reset-password.vue +124 -0
  149. package/pages/auth/verify-email.vue +80 -0
  150. package/pages/cert/[code].vue +154 -0
  151. package/pages/contests/[slug]/edit.vue +153 -0
  152. package/pages/contests/[slug]/index.vue +556 -0
  153. package/pages/contests/[slug]/judge.vue +160 -0
  154. package/pages/contests/create.vue +192 -0
  155. package/pages/contests/index.vue +69 -0
  156. package/pages/create.vue +219 -0
  157. package/pages/dashboard.vue +521 -0
  158. package/pages/docs/[siteSlug]/[...pagePath].vue +704 -0
  159. package/pages/docs/[siteSlug]/edit.vue +480 -0
  160. package/pages/docs/[siteSlug]/index.vue +380 -0
  161. package/pages/docs/create.vue +60 -0
  162. package/pages/docs/index.vue +181 -0
  163. package/pages/explore.vue +422 -0
  164. package/pages/federated-hubs/[id]/index.vue +385 -0
  165. package/pages/federated-hubs/[id]/posts/[postId].vue +309 -0
  166. package/pages/federation/index.vue +157 -0
  167. package/pages/federation/search.vue +43 -0
  168. package/pages/federation/users/[handle].vue +221 -0
  169. package/pages/feed.vue +135 -0
  170. package/pages/hubs/[slug]/index.vue +513 -0
  171. package/pages/hubs/[slug]/members.vue +134 -0
  172. package/pages/hubs/[slug]/posts/[postId].vue +352 -0
  173. package/pages/hubs/[slug]/settings.vue +254 -0
  174. package/pages/hubs/create.vue +201 -0
  175. package/pages/hubs/index.vue +207 -0
  176. package/pages/index.vue +1005 -0
  177. package/pages/learn/[slug]/[lessonSlug]/edit.vue +413 -0
  178. package/pages/learn/[slug]/[lessonSlug]/index.vue +438 -0
  179. package/pages/learn/[slug]/edit.vue +414 -0
  180. package/pages/learn/[slug]/index.vue +341 -0
  181. package/pages/learn/create.vue +71 -0
  182. package/pages/learn/index.vue +360 -0
  183. package/pages/messages/[conversationId].vue +113 -0
  184. package/pages/messages/index.vue +303 -0
  185. package/pages/mirror/[id].vue +115 -0
  186. package/pages/notifications.vue +91 -0
  187. package/pages/products/[slug].vue +128 -0
  188. package/pages/products/index.vue +122 -0
  189. package/pages/search.vue +692 -0
  190. package/pages/settings/account.vue +170 -0
  191. package/pages/settings/appearance.vue +80 -0
  192. package/pages/settings/index.vue +81 -0
  193. package/pages/settings/notifications.vue +68 -0
  194. package/pages/settings/profile.vue +838 -0
  195. package/pages/tags/[slug].vue +111 -0
  196. package/pages/tags/index.vue +73 -0
  197. package/pages/u/[username]/followers.vue +86 -0
  198. package/pages/u/[username]/following.vue +94 -0
  199. package/pages/u/[username]/index.vue +837 -0
  200. package/pages/videos/[id].vue +212 -0
  201. package/pages/videos/index.vue +327 -0
  202. package/pages/videos/submit.vue +112 -0
  203. package/plugins/auth.ts +23 -0
  204. package/server/api/admin/audit.get.ts +17 -0
  205. package/server/api/admin/content/[id].delete.ts +15 -0
  206. package/server/api/admin/content/[id].patch.ts +37 -0
  207. package/server/api/admin/federation/activity.get.ts +31 -0
  208. package/server/api/admin/federation/clients.get.ts +9 -0
  209. package/server/api/admin/federation/clients.post.ts +16 -0
  210. package/server/api/admin/federation/hub-mirrors/index.get.ts +10 -0
  211. package/server/api/admin/federation/hub-mirrors/index.post.ts +42 -0
  212. package/server/api/admin/federation/mirrors/[id]/backfill.post.ts +39 -0
  213. package/server/api/admin/federation/mirrors/[id].delete.ts +11 -0
  214. package/server/api/admin/federation/mirrors/[id].get.ts +15 -0
  215. package/server/api/admin/federation/mirrors/[id].put.ts +22 -0
  216. package/server/api/admin/federation/mirrors/index.get.ts +9 -0
  217. package/server/api/admin/federation/mirrors/index.post.ts +30 -0
  218. package/server/api/admin/federation/pending.get.ts +22 -0
  219. package/server/api/admin/federation/refederate.post.ts +92 -0
  220. package/server/api/admin/federation/repair-types.post.ts +57 -0
  221. package/server/api/admin/federation/retry.post.ts +41 -0
  222. package/server/api/admin/federation/stats.get.ts +26 -0
  223. package/server/api/admin/reports/[id]/resolve.post.ts +12 -0
  224. package/server/api/admin/reports.get.ts +17 -0
  225. package/server/api/admin/settings.get.ts +22 -0
  226. package/server/api/admin/settings.put.ts +11 -0
  227. package/server/api/admin/stats.get.ts +9 -0
  228. package/server/api/admin/users/[id]/role.put.ts +12 -0
  229. package/server/api/admin/users/[id]/status.put.ts +12 -0
  230. package/server/api/admin/users/[id].delete.ts +10 -0
  231. package/server/api/admin/users.get.ts +18 -0
  232. package/server/api/auth/federated/callback.get.ts +67 -0
  233. package/server/api/auth/federated/login.post.ts +60 -0
  234. package/server/api/auth/oauth2/authorize.get.ts +30 -0
  235. package/server/api/auth/oauth2/authorize.post.ts +51 -0
  236. package/server/api/auth/oauth2/register.post.ts +41 -0
  237. package/server/api/auth/oauth2/token.post.ts +48 -0
  238. package/server/api/cert/[code].get.ts +13 -0
  239. package/server/api/content/[id]/build.post.ts +9 -0
  240. package/server/api/content/[id]/fork.post.ts +10 -0
  241. package/server/api/content/[id]/index.delete.ts +18 -0
  242. package/server/api/content/[id]/index.get.ts +15 -0
  243. package/server/api/content/[id]/index.put.ts +23 -0
  244. package/server/api/content/[id]/products/[productId].delete.ts +28 -0
  245. package/server/api/content/[id]/products-sync.post.ts +29 -0
  246. package/server/api/content/[id]/products.get.ts +9 -0
  247. package/server/api/content/[id]/products.post.ts +31 -0
  248. package/server/api/content/[id]/publish.post.ts +17 -0
  249. package/server/api/content/[id]/report.post.ts +17 -0
  250. package/server/api/content/[id]/versions.get.ts +9 -0
  251. package/server/api/content/[id]/view.post.ts +34 -0
  252. package/server/api/content/index.get.ts +23 -0
  253. package/server/api/content/index.post.ts +11 -0
  254. package/server/api/contests/[slug]/entries.get.ts +18 -0
  255. package/server/api/contests/[slug]/entries.post.ts +23 -0
  256. package/server/api/contests/[slug]/index.delete.ts +21 -0
  257. package/server/api/contests/[slug]/index.get.ts +11 -0
  258. package/server/api/contests/[slug]/index.put.ts +15 -0
  259. package/server/api/contests/[slug]/judge.post.ts +12 -0
  260. package/server/api/contests/[slug]/transition.post.ts +24 -0
  261. package/server/api/contests/index.get.ts +10 -0
  262. package/server/api/contests/index.post.ts +28 -0
  263. package/server/api/docs/[siteSlug]/index.delete.ts +14 -0
  264. package/server/api/docs/[siteSlug]/index.get.ts +17 -0
  265. package/server/api/docs/[siteSlug]/index.put.ts +20 -0
  266. package/server/api/docs/[siteSlug]/nav.get.ts +26 -0
  267. package/server/api/docs/[siteSlug]/pages/[pageId].delete.ts +14 -0
  268. package/server/api/docs/[siteSlug]/pages/[pageId].get.ts +31 -0
  269. package/server/api/docs/[siteSlug]/pages/[pageId].put.ts +15 -0
  270. package/server/api/docs/[siteSlug]/pages/index.get.ts +34 -0
  271. package/server/api/docs/[siteSlug]/pages/index.post.ts +28 -0
  272. package/server/api/docs/[siteSlug]/pages/reorder.post.ts +26 -0
  273. package/server/api/docs/[siteSlug]/search.get.ts +20 -0
  274. package/server/api/docs/[siteSlug]/versions.post.ts +11 -0
  275. package/server/api/docs/index.get.ts +6 -0
  276. package/server/api/docs/index.post.ts +10 -0
  277. package/server/api/federated-hubs/[id]/posts/[postId].get.ts +16 -0
  278. package/server/api/federated-hubs/[id]/posts.get.ts +15 -0
  279. package/server/api/federated-hubs/[id].get.ts +16 -0
  280. package/server/api/federation/boost.post.ts +21 -0
  281. package/server/api/federation/content/[id].get.ts +15 -0
  282. package/server/api/federation/follow.post.ts +16 -0
  283. package/server/api/federation/health.get.ts +56 -0
  284. package/server/api/federation/hub-follow.post.ts +27 -0
  285. package/server/api/federation/hub-post-like.post.ts +115 -0
  286. package/server/api/federation/hub-post-likes.get.ts +24 -0
  287. package/server/api/federation/hub-post-reply.post.ts +42 -0
  288. package/server/api/federation/hub-post.post.ts +33 -0
  289. package/server/api/federation/like.post.ts +21 -0
  290. package/server/api/federation/remote-actor.get.ts +22 -0
  291. package/server/api/federation/reply.post.ts +22 -0
  292. package/server/api/federation/search.post.ts +17 -0
  293. package/server/api/federation/timeline.get.ts +22 -0
  294. package/server/api/federation/unfollow.post.ts +17 -0
  295. package/server/api/files/[id].delete.ts +35 -0
  296. package/server/api/files/mine.get.ts +31 -0
  297. package/server/api/files/upload-from-url.post.ts +68 -0
  298. package/server/api/files/upload.post.ts +105 -0
  299. package/server/api/health.get.ts +4 -0
  300. package/server/api/hubs/[slug]/bans/[userId].delete.ts +13 -0
  301. package/server/api/hubs/[slug]/bans.get.ts +20 -0
  302. package/server/api/hubs/[slug]/bans.post.ts +23 -0
  303. package/server/api/hubs/[slug]/feed.xml.get.ts +60 -0
  304. package/server/api/hubs/[slug]/gallery.get.ts +21 -0
  305. package/server/api/hubs/[slug]/index.delete.ts +18 -0
  306. package/server/api/hubs/[slug]/index.get.ts +14 -0
  307. package/server/api/hubs/[slug]/index.put.ts +22 -0
  308. package/server/api/hubs/[slug]/invites.get.ts +20 -0
  309. package/server/api/hubs/[slug]/invites.post.ts +27 -0
  310. package/server/api/hubs/[slug]/join.post.ts +17 -0
  311. package/server/api/hubs/[slug]/leave.post.ts +13 -0
  312. package/server/api/hubs/[slug]/members/[userId].delete.ts +13 -0
  313. package/server/api/hubs/[slug]/members/[userId].put.ts +16 -0
  314. package/server/api/hubs/[slug]/members.get.ts +20 -0
  315. package/server/api/hubs/[slug]/posts/[postId]/like.post.ts +21 -0
  316. package/server/api/hubs/[slug]/posts/[postId]/lock.post.ts +17 -0
  317. package/server/api/hubs/[slug]/posts/[postId]/pin.post.ts +17 -0
  318. package/server/api/hubs/[slug]/posts/[postId]/replies.get.ts +16 -0
  319. package/server/api/hubs/[slug]/posts/[postId]/replies.post.ts +21 -0
  320. package/server/api/hubs/[slug]/posts/[postId].delete.ts +23 -0
  321. package/server/api/hubs/[slug]/posts/[postId].get.ts +16 -0
  322. package/server/api/hubs/[slug]/posts/index.get.ts +16 -0
  323. package/server/api/hubs/[slug]/posts/index.post.ts +36 -0
  324. package/server/api/hubs/[slug]/products.get.ts +26 -0
  325. package/server/api/hubs/[slug]/products.post.ts +18 -0
  326. package/server/api/hubs/[slug]/share.post.ts +39 -0
  327. package/server/api/hubs/index.get.ts +15 -0
  328. package/server/api/hubs/index.post.ts +11 -0
  329. package/server/api/image-proxy.get.ts +91 -0
  330. package/server/api/learn/[slug]/[lessonSlug]/complete.post.ts +13 -0
  331. package/server/api/learn/[slug]/[lessonSlug]/index.get.ts +68 -0
  332. package/server/api/learn/[slug]/enroll.post.ts +12 -0
  333. package/server/api/learn/[slug]/index.delete.ts +12 -0
  334. package/server/api/learn/[slug]/index.get.ts +14 -0
  335. package/server/api/learn/[slug]/index.put.ts +19 -0
  336. package/server/api/learn/[slug]/lessons/[lessonId].delete.ts +14 -0
  337. package/server/api/learn/[slug]/lessons/[lessonId].put.ts +24 -0
  338. package/server/api/learn/[slug]/lessons.post.ts +10 -0
  339. package/server/api/learn/[slug]/modules/[moduleId].delete.ts +14 -0
  340. package/server/api/learn/[slug]/modules/[moduleId].put.ts +15 -0
  341. package/server/api/learn/[slug]/modules.post.ts +14 -0
  342. package/server/api/learn/[slug]/publish.post.ts +13 -0
  343. package/server/api/learn/[slug]/unenroll.post.ts +12 -0
  344. package/server/api/learn/certificates.get.ts +9 -0
  345. package/server/api/learn/enrollments.get.ts +9 -0
  346. package/server/api/learn/index.get.ts +17 -0
  347. package/server/api/learn/index.post.ts +11 -0
  348. package/server/api/me.get.ts +13 -0
  349. package/server/api/messages/[conversationId]/info.get.ts +43 -0
  350. package/server/api/messages/[conversationId]/stream.get.ts +73 -0
  351. package/server/api/messages/[conversationId].get.ts +13 -0
  352. package/server/api/messages/[conversationId].post.ts +12 -0
  353. package/server/api/messages/index.get.ts +39 -0
  354. package/server/api/messages/index.post.ts +58 -0
  355. package/server/api/notifications/[id].delete.ts +11 -0
  356. package/server/api/notifications/count.get.ts +10 -0
  357. package/server/api/notifications/index.get.ts +24 -0
  358. package/server/api/notifications/read.post.ts +15 -0
  359. package/server/api/notifications/stream.get.ts +59 -0
  360. package/server/api/openapi.get.ts +5 -0
  361. package/server/api/products/[id].delete.ts +15 -0
  362. package/server/api/products/[id].put.ts +18 -0
  363. package/server/api/products/[slug]/content.get.ts +21 -0
  364. package/server/api/products/[slug].get.ts +16 -0
  365. package/server/api/products/index.get.ts +28 -0
  366. package/server/api/profile.get.ts +15 -0
  367. package/server/api/profile.put.ts +24 -0
  368. package/server/api/resolve-identity.post.ts +34 -0
  369. package/server/api/search/index.get.ts +24 -0
  370. package/server/api/search/trending.get.ts +22 -0
  371. package/server/api/social/bookmark.get.ts +16 -0
  372. package/server/api/social/bookmark.post.ts +15 -0
  373. package/server/api/social/bookmarks.get.ts +16 -0
  374. package/server/api/social/comments/[id].delete.ts +13 -0
  375. package/server/api/social/comments.get.ts +18 -0
  376. package/server/api/social/comments.post.ts +11 -0
  377. package/server/api/social/like.get.ts +17 -0
  378. package/server/api/social/like.post.ts +35 -0
  379. package/server/api/stats.get.ts +8 -0
  380. package/server/api/user/hubs.get.ts +22 -0
  381. package/server/api/users/[username]/content.get.ts +21 -0
  382. package/server/api/users/[username]/feed.xml.get.ts +65 -0
  383. package/server/api/users/[username]/follow.delete.ts +15 -0
  384. package/server/api/users/[username]/follow.post.ts +15 -0
  385. package/server/api/users/[username]/followers.get.ts +22 -0
  386. package/server/api/users/[username]/following.get.ts +22 -0
  387. package/server/api/users/[username]/learning.get.ts +18 -0
  388. package/server/api/users/[username].get.ts +25 -0
  389. package/server/api/users/index.get.ts +78 -0
  390. package/server/api/videos/[id].get.ts +18 -0
  391. package/server/api/videos/categories/[id].delete.ts +15 -0
  392. package/server/api/videos/categories/[id].put.ts +17 -0
  393. package/server/api/videos/categories.get.ts +7 -0
  394. package/server/api/videos/categories.post.ts +11 -0
  395. package/server/api/videos/index.get.ts +20 -0
  396. package/server/api/videos/index.post.ts +11 -0
  397. package/server/middleware/auth.ts +180 -0
  398. package/server/middleware/features.ts +31 -0
  399. package/server/middleware/security.ts +54 -0
  400. package/server/plugins/auto-admin.ts +69 -0
  401. package/server/plugins/federation-delivery.ts +74 -0
  402. package/server/routes/.well-known/nodeinfo.ts +15 -0
  403. package/server/routes/.well-known/webfinger.ts +86 -0
  404. package/server/routes/actor/followers.ts +34 -0
  405. package/server/routes/actor/following.ts +34 -0
  406. package/server/routes/actor/outbox.ts +31 -0
  407. package/server/routes/actor.ts +30 -0
  408. package/server/routes/feed.xml.ts +59 -0
  409. package/server/routes/hubs/[slug]/followers.ts +39 -0
  410. package/server/routes/hubs/[slug]/inbox.ts +35 -0
  411. package/server/routes/hubs/[slug]/outbox.ts +43 -0
  412. package/server/routes/hubs/[slug]/posts/[postId].ts +63 -0
  413. package/server/routes/hubs/[slug].ts +29 -0
  414. package/server/routes/inbox.ts +34 -0
  415. package/server/routes/nodeinfo/2.1.ts +27 -0
  416. package/server/routes/robots.txt.ts +19 -0
  417. package/server/routes/sitemap.xml.ts +105 -0
  418. package/server/routes/users/[username]/followers.ts +33 -0
  419. package/server/routes/users/[username]/following.ts +33 -0
  420. package/server/routes/users/[username]/inbox.ts +34 -0
  421. package/server/routes/users/[username]/outbox.ts +35 -0
  422. package/server/routes/users/[username].ts +62 -0
  423. package/server/utils/auth.ts +36 -0
  424. package/server/utils/db.ts +34 -0
  425. package/server/utils/errors.ts +24 -0
  426. package/server/utils/inbox.ts +122 -0
  427. package/server/utils/validate.ts +82 -0
  428. package/theme/base.css +283 -0
  429. package/theme/components.css +322 -0
  430. package/theme/dark.css +87 -0
  431. package/theme/editor-panels.css +63 -0
  432. package/theme/forms.css +216 -0
  433. package/theme/generics.css +68 -0
  434. package/theme/layouts.css +415 -0
  435. package/theme/prose.css +342 -0
  436. package/types/hub.ts +61 -0
@@ -0,0 +1,11 @@
1
+ import { createContent } from '@commonpub/server';
2
+ import type { ContentDetail } from '@commonpub/server';
3
+ import { createContentSchema } from '@commonpub/schema';
4
+
5
+ export default defineEventHandler(async (event): Promise<ContentDetail> => {
6
+ const user = requireAuth(event);
7
+ const db = useDB();
8
+ const input = await parseBody(event, createContentSchema);
9
+
10
+ return createContent(db, user.id, input);
11
+ });
@@ -0,0 +1,18 @@
1
+ import { listContestEntries, getContestBySlug } from '@commonpub/server';
2
+ import type { ContestEntryItem } from '@commonpub/server';
3
+ import { z } from 'zod';
4
+
5
+ const entriesQuerySchema = z.object({
6
+ limit: z.coerce.number().int().min(1).max(100).optional(),
7
+ offset: z.coerce.number().int().min(0).optional(),
8
+ });
9
+
10
+ export default defineEventHandler(async (event): Promise<{ items: ContestEntryItem[]; total: number }> => {
11
+ requireFeature('contests');
12
+ const db = useDB();
13
+ const { slug } = parseParams(event, { slug: 'string' });
14
+ const query = parseQueryParams(event, entriesQuerySchema);
15
+ const contest = await getContestBySlug(db, slug);
16
+ if (!contest) throw createError({ statusCode: 404, statusMessage: 'Contest not found' });
17
+ return listContestEntries(db, contest.id, query);
18
+ });
@@ -0,0 +1,23 @@
1
+ import { submitContestEntry, getContestBySlug } from '@commonpub/server';
2
+ import type { ContestEntryItem } from '@commonpub/server';
3
+ import { z } from 'zod';
4
+
5
+ const submitEntrySchema = z.object({
6
+ contentId: z.string().uuid(),
7
+ });
8
+
9
+ export default defineEventHandler(async (event): Promise<ContestEntryItem> => {
10
+ requireFeature('contests');
11
+ const user = requireAuth(event);
12
+ const db = useDB();
13
+ const { slug } = parseParams(event, { slug: 'string' });
14
+ const contest = await getContestBySlug(db, slug);
15
+ if (!contest) throw createError({ statusCode: 404, statusMessage: 'Contest not found' });
16
+ const input = await parseBody(event, submitEntrySchema);
17
+
18
+ const entry = await submitContestEntry(db, contest.id, input.contentId, user.id);
19
+ if (!entry) {
20
+ throw createError({ statusCode: 400, statusMessage: 'Cannot submit entry. Contest may not be active, or content is not published or not owned by you.' });
21
+ }
22
+ return entry;
23
+ });
@@ -0,0 +1,21 @@
1
+ import { getContestBySlug, deleteContest } from '@commonpub/server';
2
+
3
+ export default defineEventHandler(async (event): Promise<{ deleted: boolean }> => {
4
+ requireFeature('contests');
5
+ const db = useDB();
6
+ const user = requireAuth(event);
7
+ const { slug } = parseParams(event, { slug: 'string' });
8
+
9
+
10
+ const contest = await getContestBySlug(db, slug);
11
+ if (!contest) {
12
+ throw createError({ statusCode: 404, statusMessage: 'Contest not found' });
13
+ }
14
+
15
+ const deleted = await deleteContest(db, contest.id, user.id);
16
+ if (!deleted) {
17
+ throw createError({ statusCode: 403, statusMessage: 'Not authorized to delete this contest' });
18
+ }
19
+
20
+ return { deleted: true };
21
+ });
@@ -0,0 +1,11 @@
1
+ import { getContestBySlug } from '@commonpub/server';
2
+ import type { ContestDetail } from '@commonpub/server';
3
+
4
+ export default defineEventHandler(async (event): Promise<ContestDetail> => {
5
+ requireFeature('contests');
6
+ const db = useDB();
7
+ const { slug } = parseParams(event, { slug: 'string' });
8
+ const contest = await getContestBySlug(db, slug);
9
+ if (!contest) throw createError({ statusCode: 404, statusMessage: 'Contest not found' });
10
+ return contest;
11
+ });
@@ -0,0 +1,15 @@
1
+ import { updateContest } from '@commonpub/server';
2
+ import type { ContestDetail } from '@commonpub/server';
3
+ import { updateContestSchema } from '@commonpub/schema';
4
+
5
+ export default defineEventHandler(async (event): Promise<ContestDetail> => {
6
+ requireFeature('contests');
7
+ const user = requireAuth(event);
8
+ const db = useDB();
9
+ const { slug } = parseParams(event, { slug: 'string' });
10
+ const input = await parseBody(event, updateContestSchema);
11
+
12
+ const result = await updateContest(db, slug, user.id, input);
13
+ if (!result) throw createError({ statusCode: 403, statusMessage: 'Not authorized or contest not found' });
14
+ return result;
15
+ });
@@ -0,0 +1,12 @@
1
+ import { judgeContestEntry } from '@commonpub/server';
2
+ import { judgeEntrySchema } from '@commonpub/schema';
3
+
4
+ export default defineEventHandler(async (event): Promise<{ success: boolean }> => {
5
+ requireFeature('contests');
6
+ const user = requireAuth(event);
7
+ const db = useDB();
8
+ const input = await parseBody(event, judgeEntrySchema);
9
+
10
+ await judgeContestEntry(db, input.entryId, input.score, user.id);
11
+ return { success: true };
12
+ });
@@ -0,0 +1,24 @@
1
+ import { getContestBySlug, transitionContestStatus } from '@commonpub/server';
2
+ import type { ContestStatus } from '@commonpub/server';
3
+ import { contestTransitionSchema } from '@commonpub/schema';
4
+
5
+ export default defineEventHandler(async (event): Promise<{ transitioned: boolean; newStatus: ContestStatus }> => {
6
+ requireFeature('contests');
7
+ const db = useDB();
8
+ const user = requireAuth(event);
9
+ const { slug } = parseParams(event, { slug: 'string' });
10
+ const input = await parseBody(event, contestTransitionSchema);
11
+
12
+ const contest = await getContestBySlug(db, slug);
13
+ if (!contest) {
14
+ throw createError({ statusCode: 404, statusMessage: 'Contest not found' });
15
+ }
16
+
17
+ const result = await transitionContestStatus(db, contest.id, user.id, input.status);
18
+
19
+ if (!result.transitioned) {
20
+ throw createError({ statusCode: 400, statusMessage: result.error || 'Transition failed' });
21
+ }
22
+
23
+ return { transitioned: true, newStatus: input.status };
24
+ });
@@ -0,0 +1,10 @@
1
+ import { listContests } from '@commonpub/server';
2
+ import type { PaginatedResponse, ContestListItem } from '@commonpub/server';
3
+ import { contestFiltersSchema } from '@commonpub/schema';
4
+
5
+ export default defineEventHandler(async (event): Promise<PaginatedResponse<ContestListItem>> => {
6
+ requireFeature('contests');
7
+ const db = useDB();
8
+ const filters = parseQueryParams(event, contestFiltersSchema);
9
+ return listContests(db, filters);
10
+ });
@@ -0,0 +1,28 @@
1
+ import { createContest } from '@commonpub/server';
2
+ import type { ContestDetail, CreateContestInput } from '@commonpub/server';
3
+ import { createContestSchema } from '@commonpub/schema';
4
+
5
+ function slugify(text: string): string {
6
+ return text
7
+ .toLowerCase()
8
+ .replace(/[^\w\s-]/g, '')
9
+ .replace(/\s+/g, '-')
10
+ .replace(/-+/g, '-')
11
+ .replace(/^-|-$/g, '')
12
+ .slice(0, 128);
13
+ }
14
+
15
+ export default defineEventHandler(async (event): Promise<ContestDetail> => {
16
+ requireFeature('contests');
17
+ const user = requireAuth(event);
18
+ const db = useDB();
19
+ const config = useConfig();
20
+ const input = await parseBody(event, createContestSchema);
21
+
22
+ const slug = slugify(input.title) || `contest-${Date.now()}`;
23
+
24
+ return createContest(db, { ...input, slug, createdBy: user.id } as CreateContestInput, {
25
+ userRole: user.role,
26
+ contestCreationPolicy: config.instance.contestCreation ?? 'staff',
27
+ });
28
+ });
@@ -0,0 +1,14 @@
1
+ import { getDocsSiteBySlug, deleteDocsSite } from '@commonpub/server';
2
+
3
+ export default defineEventHandler(async (event) => {
4
+ const user = requireAuth(event);
5
+ const db = useDB();
6
+ const { siteSlug } = parseParams(event, { siteSlug: 'string' });
7
+
8
+ const result = await getDocsSiteBySlug(db, siteSlug);
9
+ if (!result) {
10
+ throw createError({ statusCode: 404, statusMessage: 'Docs site not found' });
11
+ }
12
+
13
+ return deleteDocsSite(db, result.site.id, user.id);
14
+ });
@@ -0,0 +1,17 @@
1
+ import { getDocsSiteBySlug } from '@commonpub/server';
2
+
3
+ export default defineEventHandler(async (event) => {
4
+ const db = useDB();
5
+ const { siteSlug } = parseParams(event, { siteSlug: 'string' });
6
+
7
+ const result = await getDocsSiteBySlug(db, siteSlug);
8
+ if (!result) {
9
+ throw createError({ statusCode: 404, statusMessage: 'Docs site not found' });
10
+ }
11
+
12
+ // Flatten the nested { site, versions } shape so frontend can use site.name directly
13
+ return {
14
+ ...result.site,
15
+ versions: result.versions,
16
+ };
17
+ });
@@ -0,0 +1,20 @@
1
+ import { getDocsSiteBySlug, updateDocsSite } from '@commonpub/server';
2
+ import { updateDocsSiteSchema } from '@commonpub/schema';
3
+
4
+ export default defineEventHandler(async (event) => {
5
+ const user = requireAuth(event);
6
+ const db = useDB();
7
+ const { siteSlug } = parseParams(event, { siteSlug: 'string' });
8
+ const input = await parseBody(event, updateDocsSiteSchema);
9
+
10
+ const result = await getDocsSiteBySlug(db, siteSlug);
11
+ if (!result) {
12
+ throw createError({ statusCode: 404, statusMessage: 'Docs site not found' });
13
+ }
14
+
15
+ const updated = await updateDocsSite(db, result.site.id, user.id, input);
16
+ if (!updated) {
17
+ throw createError({ statusCode: 403, statusMessage: 'Not authorized to update this docs site' });
18
+ }
19
+ return updated;
20
+ });
@@ -0,0 +1,26 @@
1
+ import { getDocsSiteBySlug, listDocsPages } from '@commonpub/server';
2
+ import { z } from 'zod';
3
+
4
+ const navQuerySchema = z.object({
5
+ version: z.string().max(32).optional(),
6
+ });
7
+
8
+ export default defineEventHandler(async (event) => {
9
+ const db = useDB();
10
+ const { siteSlug } = parseParams(event, { siteSlug: 'string' });
11
+ const query = parseQueryParams(event, navQuerySchema);
12
+
13
+ const result = await getDocsSiteBySlug(db, siteSlug);
14
+ if (!result) throw createError({ statusCode: 404, statusMessage: 'Docs site not found' });
15
+
16
+ // Find the requested or default version
17
+ const version = query.version
18
+ ? result.versions?.find((v) => v.version === query.version)
19
+ : result.versions?.find((v) => v.isDefault) ?? result.versions?.[0];
20
+
21
+ if (!version) return [];
22
+
23
+ // Return pages directly as nav items
24
+ const pages = await listDocsPages(db, version.id);
25
+ return pages.map(p => ({ id: p.id, title: p.title, slug: p.slug, sortOrder: p.sortOrder, parentId: p.parentId }));
26
+ });
@@ -0,0 +1,14 @@
1
+ import { deleteDocsPage } from '@commonpub/server';
2
+
3
+ export default defineEventHandler(async (event) => {
4
+ const user = requireAuth(event);
5
+ const db = useDB();
6
+ const { pageId } = parseParams(event, { pageId: 'uuid' });
7
+
8
+ const result = await deleteDocsPage(db, pageId, user.id);
9
+ if (!result) {
10
+ throw createError({ statusCode: 404, statusMessage: 'Page not found or not authorized' });
11
+ }
12
+
13
+ return { success: true };
14
+ });
@@ -0,0 +1,31 @@
1
+ import { getDocsSiteBySlug, listDocsPages } from '@commonpub/server';
2
+ import { renderMarkdown } from '@commonpub/docs';
3
+
4
+ export default defineEventHandler(async (event) => {
5
+ const db = useDB();
6
+ const { siteSlug, pageId: pageSlug } = parseParams(event, { siteSlug: 'string', pageId: 'string' });
7
+ const query = getQuery(event) as { version?: string };
8
+
9
+ const result = await getDocsSiteBySlug(db, siteSlug);
10
+ if (!result) throw createError({ statusCode: 404, statusMessage: 'Docs site not found' });
11
+
12
+ const version = query.version
13
+ ? result.versions.find((v) => v.version === query.version)
14
+ : result.versions.find((v) => v.isDefault) ?? result.versions[0];
15
+
16
+ if (!version) throw createError({ statusCode: 404, statusMessage: 'No version found' });
17
+
18
+ const pages = await listDocsPages(db, version.id);
19
+ const page = pages.find((p) => p.slug === pageSlug);
20
+ if (!page) throw createError({ statusCode: 404, statusMessage: 'Page not found' });
21
+
22
+ // Render markdown to HTML with TOC extraction
23
+ const rendered = await renderMarkdown(page.content ?? '');
24
+
25
+ return {
26
+ ...page,
27
+ html: rendered.html,
28
+ toc: rendered.toc,
29
+ frontmatter: rendered.frontmatter,
30
+ };
31
+ });
@@ -0,0 +1,15 @@
1
+ import { updateDocsPage } from '@commonpub/server';
2
+ import { updateDocsPageSchema } from '@commonpub/schema';
3
+
4
+ export default defineEventHandler(async (event) => {
5
+ const user = requireAuth(event);
6
+ const db = useDB();
7
+ const { pageId } = parseParams(event, { pageId: 'uuid' });
8
+ const input = await parseBody(event, updateDocsPageSchema);
9
+
10
+ const page = await updateDocsPage(db, pageId, user.id, input);
11
+ if (!page) {
12
+ throw createError({ statusCode: 404, statusMessage: 'Page not found or not owned by you' });
13
+ }
14
+ return page;
15
+ });
@@ -0,0 +1,34 @@
1
+ import { getDocsSiteBySlug, listDocsPages } from '@commonpub/server';
2
+ import { z } from 'zod';
3
+
4
+ const pagesQuerySchema = z.object({
5
+ version: z.string().max(32).optional(),
6
+ });
7
+
8
+ export default defineEventHandler(async (event) => {
9
+ const db = useDB();
10
+ const { siteSlug } = parseParams(event, { siteSlug: 'string' });
11
+ const query = parseQueryParams(event, pagesQuerySchema);
12
+
13
+ const result = await getDocsSiteBySlug(db, siteSlug);
14
+ if (!result) {
15
+ throw createError({ statusCode: 404, statusMessage: 'Docs site not found' });
16
+ }
17
+
18
+ // Find the requested version or fall back to the default version
19
+ const requestedVersion = query.version;
20
+ let version = requestedVersion
21
+ ? result.versions.find((v) => v.version === requestedVersion)
22
+ : result.versions.find((v) => v.isDefault === true);
23
+
24
+ // Fall back to first version if no default found
25
+ if (!version && result.versions.length > 0) {
26
+ version = result.versions[0];
27
+ }
28
+
29
+ if (!version) {
30
+ throw createError({ statusCode: 404, statusMessage: 'No version found for docs site' });
31
+ }
32
+
33
+ return listDocsPages(db, version.id);
34
+ });
@@ -0,0 +1,28 @@
1
+ import { getDocsSiteBySlug, createDocsPage } from '@commonpub/server';
2
+ import { createDocsPageSchema } from '@commonpub/schema';
3
+
4
+ export default defineEventHandler(async (event) => {
5
+ const user = requireAuth(event);
6
+ const db = useDB();
7
+ const { siteSlug } = parseParams(event, { siteSlug: 'string' });
8
+ const input = await parseBody(event, createDocsPageSchema);
9
+
10
+ const data = { ...input };
11
+
12
+ // If no versionId provided, resolve from the site's default version
13
+ if (!data.versionId) {
14
+ const result = await getDocsSiteBySlug(db, siteSlug);
15
+ if (!result) {
16
+ throw createError({ statusCode: 404, statusMessage: 'Docs site not found' });
17
+ }
18
+
19
+ const defaultVersion = result.versions.find((v) => v.isDefault === true) ?? result.versions[0];
20
+ if (!defaultVersion) {
21
+ throw createError({ statusCode: 404, statusMessage: 'No version found for docs site' });
22
+ }
23
+
24
+ data.versionId = defaultVersion.id;
25
+ }
26
+
27
+ return createDocsPage(db, user.id, data as typeof data & { versionId: string });
28
+ });
@@ -0,0 +1,26 @@
1
+ import { reorderDocsPages, getDocsSiteBySlug } from '@commonpub/server';
2
+ import { z } from 'zod';
3
+
4
+ const reorderSchema = z.object({
5
+ pageIds: z.array(z.string().uuid()),
6
+ });
7
+
8
+ export default defineEventHandler(async (event) => {
9
+ const user = requireAuth(event);
10
+ const db = useDB();
11
+ const { siteSlug } = parseParams(event, { siteSlug: 'string' });
12
+ const body = await parseBody(event, reorderSchema);
13
+
14
+ const site = await getDocsSiteBySlug(db, siteSlug);
15
+ if (!site) throw createError({ statusCode: 404, statusMessage: 'Docs site not found' });
16
+
17
+ const version = site.versions.find((v) => v.isDefault) ?? site.versions[0];
18
+ if (!version) throw createError({ statusCode: 404, statusMessage: 'No version found' });
19
+
20
+ const result = await reorderDocsPages(db, version.id, user.id, body.pageIds);
21
+ if (!result) {
22
+ throw createError({ statusCode: 403, statusMessage: 'Not authorized' });
23
+ }
24
+
25
+ return { success: true };
26
+ });
@@ -0,0 +1,20 @@
1
+ import { searchDocsPages, getDocsSiteBySlug } from '@commonpub/server';
2
+ import { z } from 'zod';
3
+
4
+ const searchQuerySchema = z.object({
5
+ q: z.string().max(200).optional(),
6
+ });
7
+
8
+ export default defineEventHandler(async (event) => {
9
+ const db = useDB();
10
+ const { siteSlug } = parseParams(event, { siteSlug: 'string' });
11
+ const query = parseQueryParams(event, searchQuerySchema);
12
+
13
+ const result = await getDocsSiteBySlug(db, siteSlug);
14
+ if (!result) throw createError({ statusCode: 404, statusMessage: 'Docs site not found' });
15
+
16
+ const version = result.versions?.find((v) => v.isDefault) ?? result.versions?.[0];
17
+ if (!version) return [];
18
+
19
+ return searchDocsPages(db, result.site.id, version.id, query.q ?? '');
20
+ });
@@ -0,0 +1,11 @@
1
+ import { createDocsVersion } from '@commonpub/server';
2
+ import { createDocsVersionSchema } from '@commonpub/schema';
3
+
4
+ export default defineEventHandler(async (event) => {
5
+ const user = requireAuth(event);
6
+ const db = useDB();
7
+ const { siteSlug } = parseParams(event, { siteSlug: 'string' });
8
+ const input = await parseBody(event, createDocsVersionSchema);
9
+
10
+ return createDocsVersion(db, siteSlug, user.id, input);
11
+ });
@@ -0,0 +1,6 @@
1
+ import { listDocsSites } from '@commonpub/server';
2
+
3
+ export default defineEventHandler(async (_event) => {
4
+ const db = useDB();
5
+ return listDocsSites(db);
6
+ });
@@ -0,0 +1,10 @@
1
+ import { createDocsSite } from '@commonpub/server';
2
+ import { createDocsSiteSchema } from '@commonpub/schema';
3
+
4
+ export default defineEventHandler(async (event) => {
5
+ const user = requireAuth(event);
6
+ const db = useDB();
7
+ const input = await parseBody(event, createDocsSiteSchema);
8
+
9
+ return createDocsSite(db, user.id, input);
10
+ });
@@ -0,0 +1,16 @@
1
+ import { getFederatedHubPost } from '@commonpub/server';
2
+
3
+ export default defineEventHandler(async (event) => {
4
+ requireFeature('federation');
5
+ requireFeature('federateHubs');
6
+
7
+ const db = useDB();
8
+ const { postId } = parseParams(event, { postId: 'uuid' });
9
+
10
+ const post = await getFederatedHubPost(db, postId);
11
+ if (!post) {
12
+ throw createError({ statusCode: 404, statusMessage: 'Federated hub post not found' });
13
+ }
14
+
15
+ return post;
16
+ });
@@ -0,0 +1,15 @@
1
+ import { listFederatedHubPosts } from '@commonpub/server';
2
+
3
+ export default defineEventHandler(async (event) => {
4
+ requireFeature('federation');
5
+ requireFeature('federateHubs');
6
+
7
+ const db = useDB();
8
+ const { id } = parseParams(event, { id: 'uuid' });
9
+ const query = getQuery(event);
10
+
11
+ return listFederatedHubPosts(db, id, {
12
+ limit: query.limit ? Number(query.limit) : undefined,
13
+ offset: query.offset ? Number(query.offset) : undefined,
14
+ });
15
+ });
@@ -0,0 +1,16 @@
1
+ import { getFederatedHub } from '@commonpub/server';
2
+
3
+ export default defineEventHandler(async (event) => {
4
+ requireFeature('federation');
5
+ requireFeature('federateHubs');
6
+
7
+ const db = useDB();
8
+ const { id } = parseParams(event, { id: 'uuid' });
9
+
10
+ const hub = await getFederatedHub(db, id);
11
+ if (!hub) {
12
+ throw createError({ statusCode: 404, statusMessage: 'Federated hub not found' });
13
+ }
14
+
15
+ return hub;
16
+ });
@@ -0,0 +1,21 @@
1
+ import { boostRemoteContent } from '@commonpub/server';
2
+ import { z } from 'zod';
3
+
4
+ const boostSchema = z.object({
5
+ federatedContentId: z.string().uuid(),
6
+ });
7
+
8
+ export default defineEventHandler(async (event): Promise<{ success: boolean }> => {
9
+ requireFeature('federation');
10
+ const user = requireAuth(event);
11
+ const db = useDB();
12
+ const config = useConfig();
13
+ const { federatedContentId } = await parseBody(event, boostSchema);
14
+
15
+ const success = await boostRemoteContent(db, user.id, federatedContentId, config.instance.domain);
16
+ if (!success) {
17
+ throw createError({ statusCode: 404, statusMessage: 'Content not found' });
18
+ }
19
+
20
+ return { success };
21
+ });
@@ -0,0 +1,15 @@
1
+ import { getFederatedContent } from '@commonpub/server';
2
+ import type { FederatedContentItem } from '@commonpub/server';
3
+
4
+ export default defineEventHandler(async (event): Promise<FederatedContentItem> => {
5
+ requireFeature('federation');
6
+ const db = useDB();
7
+ const { id } = parseParams(event, { id: 'uuid' });
8
+
9
+ const content = await getFederatedContent(db, id);
10
+ if (!content) {
11
+ throw createError({ statusCode: 404, statusMessage: 'Federated content not found' });
12
+ }
13
+
14
+ return content;
15
+ });
@@ -0,0 +1,16 @@
1
+ import { sendFollow } from '@commonpub/server';
2
+ import { z } from 'zod';
3
+
4
+ const followSchema = z.object({
5
+ actorUri: z.string().url(),
6
+ });
7
+
8
+ export default defineEventHandler(async (event): Promise<{ id: string }> => {
9
+ requireFeature('federation');
10
+ const user = requireAuth(event);
11
+ const db = useDB();
12
+ const config = useConfig();
13
+ const { actorUri } = await parseBody(event, followSchema);
14
+
15
+ return sendFollow(db, user.id, actorUri, config.instance.domain);
16
+ });
@@ -0,0 +1,56 @@
1
+ import { activities, followRelationships, instanceMirrors, federatedContent } from '@commonpub/schema';
2
+ import { sql } from 'drizzle-orm';
3
+
4
+ /**
5
+ * GET /api/federation/health
6
+ * Public health check for federation status. No auth required.
7
+ */
8
+ export default defineEventHandler(async (event) => {
9
+ const config = useConfig();
10
+ if (!config.features.federation) {
11
+ return { enabled: false };
12
+ }
13
+
14
+ const db = useDB();
15
+
16
+ const [activityStats] = await db
17
+ .select({
18
+ pending: sql<number>`count(*) filter (where ${activities.direction} = 'outbound' and ${activities.status} = 'pending')`,
19
+ delivered: sql<number>`count(*) filter (where ${activities.direction} = 'outbound' and ${activities.status} = 'delivered')`,
20
+ failed: sql<number>`count(*) filter (where ${activities.direction} = 'outbound' and ${activities.status} = 'failed')`,
21
+ inbound: sql<number>`count(*) filter (where ${activities.direction} = 'inbound')`,
22
+ })
23
+ .from(activities);
24
+
25
+ const [followStats] = await db
26
+ .select({
27
+ followers: sql<number>`count(*) filter (where ${followRelationships.status} = 'accepted')`,
28
+ })
29
+ .from(followRelationships);
30
+
31
+ const [mirrorStats] = await db
32
+ .select({
33
+ active: sql<number>`count(*) filter (where ${instanceMirrors.status} = 'active')`,
34
+ })
35
+ .from(instanceMirrors);
36
+
37
+ const [contentStats] = await db
38
+ .select({
39
+ total: sql<number>`count(*)`,
40
+ })
41
+ .from(federatedContent);
42
+
43
+ return {
44
+ enabled: true,
45
+ domain: config.instance.domain,
46
+ activities: {
47
+ pending: Number(activityStats?.pending ?? 0),
48
+ delivered: Number(activityStats?.delivered ?? 0),
49
+ failed: Number(activityStats?.failed ?? 0),
50
+ inbound: Number(activityStats?.inbound ?? 0),
51
+ },
52
+ followers: Number(followStats?.followers ?? 0),
53
+ mirrors: Number(mirrorStats?.active ?? 0),
54
+ federatedContent: Number(contentStats?.total ?? 0),
55
+ };
56
+ });