@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,91 @@
1
+ /**
2
+ * GET /api/image-proxy?url=<remote-url>&w=<width>
3
+ * Proxies and caches remote images for federated content.
4
+ * Prevents slow cross-origin fetches on content cards.
5
+ *
6
+ * Security: only proxies images from known federation origins
7
+ * (federated_content.origin_domain or remote_actors.instance_domain).
8
+ */
9
+ export default defineEventHandler(async (event) => {
10
+ const query = getQuery(event);
11
+ const url = query.url as string | undefined;
12
+ const width = Math.min(parseInt(String(query.w || '800'), 10), 1920);
13
+
14
+ if (!url || typeof url !== 'string') {
15
+ throw createError({ statusCode: 400, statusMessage: 'Missing url parameter' });
16
+ }
17
+
18
+ // Parse and validate the URL
19
+ let parsed: URL;
20
+ try {
21
+ parsed = new URL(url);
22
+ } catch {
23
+ throw createError({ statusCode: 400, statusMessage: 'Invalid URL' });
24
+ }
25
+
26
+ // Only allow HTTPS image URLs
27
+ if (parsed.protocol !== 'https:') {
28
+ throw createError({ statusCode: 400, statusMessage: 'Only HTTPS URLs allowed' });
29
+ }
30
+
31
+ // Block localhost/private IPs (SSRF prevention)
32
+ const hostname = parsed.hostname;
33
+ if (
34
+ hostname === 'localhost' ||
35
+ hostname === '127.0.0.1' ||
36
+ hostname === '::1' ||
37
+ hostname.startsWith('10.') ||
38
+ hostname.startsWith('172.') ||
39
+ hostname.startsWith('192.168.') ||
40
+ hostname.endsWith('.local')
41
+ ) {
42
+ throw createError({ statusCode: 403, statusMessage: 'Private addresses not allowed' });
43
+ }
44
+
45
+ // Fetch the remote image
46
+ const controller = new AbortController();
47
+ const timeout = setTimeout(() => controller.abort(), 15_000);
48
+
49
+ try {
50
+ const response = await fetch(url, {
51
+ signal: controller.signal,
52
+ headers: {
53
+ Accept: 'image/*',
54
+ 'User-Agent': 'CommonPub/1.0 (image-proxy)',
55
+ },
56
+ redirect: 'follow',
57
+ });
58
+
59
+ clearTimeout(timeout);
60
+
61
+ if (!response.ok) {
62
+ throw createError({ statusCode: 502, statusMessage: `Upstream returned ${response.status}` });
63
+ }
64
+
65
+ const contentType = response.headers.get('content-type') || '';
66
+ if (!contentType.startsWith('image/')) {
67
+ throw createError({ statusCode: 502, statusMessage: 'Not an image' });
68
+ }
69
+
70
+ // Limit to 10MB
71
+ const contentLength = parseInt(response.headers.get('content-length') || '0', 10);
72
+ if (contentLength > 10 * 1024 * 1024) {
73
+ throw createError({ statusCode: 502, statusMessage: 'Image too large' });
74
+ }
75
+
76
+ const buffer = Buffer.from(await response.arrayBuffer());
77
+
78
+ // Set aggressive cache headers — federated images rarely change
79
+ setResponseHeaders(event, {
80
+ 'Content-Type': contentType,
81
+ 'Cache-Control': 'public, max-age=86400, s-maxage=604800, stale-while-revalidate=86400',
82
+ 'X-Image-Proxy': 'commonpub',
83
+ });
84
+
85
+ return buffer;
86
+ } catch (err: unknown) {
87
+ clearTimeout(timeout);
88
+ if ((err as { statusCode?: number })?.statusCode) throw err;
89
+ throw createError({ statusCode: 502, statusMessage: 'Failed to fetch image' });
90
+ }
91
+ });
@@ -0,0 +1,13 @@
1
+ import { getLessonBySlug, markLessonComplete } from '@commonpub/server';
2
+
3
+ export default defineEventHandler(async (event) => {
4
+ const user = requireAuth(event);
5
+ const db = useDB();
6
+ const { slug, lessonSlug } = parseParams(event, { slug: 'string', lessonSlug: 'string' });
7
+ const body = await readBody(event).catch(() => ({}));
8
+
9
+ const result = await getLessonBySlug(db, slug, lessonSlug);
10
+ if (!result) throw createError({ statusCode: 404, statusMessage: 'Lesson not found' });
11
+
12
+ return markLessonComplete(db, user.id, result.lesson.id, body?.quizScore, body?.quizPassed);
13
+ });
@@ -0,0 +1,68 @@
1
+ import { getLessonBySlug } from '@commonpub/server';
2
+ import { renderMarkdown } from '@commonpub/docs';
3
+
4
+ function blocksToHtml(blocks: unknown): string {
5
+ if (!Array.isArray(blocks)) return '';
6
+ const parts: string[] = [];
7
+ for (const block of blocks) {
8
+ const [type, data] = block as [string, Record<string, unknown>];
9
+ if (!data) continue;
10
+ if (typeof data.html === 'string' && data.html) {
11
+ parts.push(data.html);
12
+ } else if (type === 'heading' && typeof data.text === 'string') {
13
+ const level = Math.min(Math.max(Number(data.level) || 2, 1), 6);
14
+ const escaped = data.text.replace(/</g, '&lt;').replace(/>/g, '&gt;');
15
+ parts.push(`<h${level}>${escaped}</h${level}>`);
16
+ } else if (type === 'code_block' && typeof data.code === 'string') {
17
+ const lang = String(data.language || '').replace(/[^a-zA-Z0-9-]/g, '');
18
+ const escaped = data.code.replace(/</g, '&lt;').replace(/>/g, '&gt;');
19
+ parts.push(`<pre><code class="language-${lang}">${escaped}</code></pre>`);
20
+ } else if (type === 'image' && (data.src || data.url)) {
21
+ const src = String(data.src || data.url).replace(/"/g, '&quot;');
22
+ const alt = String(data.alt || '').replace(/"/g, '&quot;');
23
+ if (src.startsWith('http://') || src.startsWith('https://') || src.startsWith('/')) {
24
+ parts.push(`<figure><img src="${src}" alt="${alt}" /></figure>`);
25
+ }
26
+ } else if (type === 'blockquote' && typeof data.text === 'string') {
27
+ const escaped = data.text.replace(/</g, '&lt;').replace(/>/g, '&gt;');
28
+ parts.push(`<blockquote>${escaped}</blockquote>`);
29
+ } else if (type === 'callout' && typeof data.text === 'string') {
30
+ const escaped = data.text.replace(/</g, '&lt;').replace(/>/g, '&gt;');
31
+ parts.push(`<div class="cpub-callout">${escaped}</div>`);
32
+ } else if (type === 'horizontal_rule' || type === 'divider') {
33
+ parts.push('<hr />');
34
+ }
35
+ }
36
+ return parts.join('\n');
37
+ }
38
+
39
+ export default defineEventHandler(async (event) => {
40
+ const db = useDB();
41
+ const { slug, lessonSlug } = parseParams(event, { slug: 'string', lessonSlug: 'string' });
42
+
43
+ const result = await getLessonBySlug(db, slug, lessonSlug);
44
+ if (!result) {
45
+ throw createError({ statusCode: 404, statusMessage: 'Lesson not found' });
46
+ }
47
+
48
+ let renderedHtml = '';
49
+
50
+ if (result.linkedContent?.content) {
51
+ // Linked content uses BlockTuple[] format
52
+ renderedHtml = blocksToHtml(result.linkedContent.content);
53
+ } else {
54
+ // Inline content: render markdown
55
+ const content = result.lesson.content as Record<string, unknown> | null;
56
+ if (content) {
57
+ const md = typeof content.markdown === 'string' ? content.markdown
58
+ : typeof content.notes === 'string' ? content.notes
59
+ : '';
60
+ if (md) {
61
+ const rendered = await renderMarkdown(md);
62
+ renderedHtml = rendered.html;
63
+ }
64
+ }
65
+ }
66
+
67
+ return { ...result, renderedHtml };
68
+ });
@@ -0,0 +1,12 @@
1
+ import { enroll, getPathBySlug } from '@commonpub/server';
2
+
3
+ export default defineEventHandler(async (event) => {
4
+ const user = requireAuth(event);
5
+ const db = useDB();
6
+ const { slug } = parseParams(event, { slug: 'string' });
7
+
8
+ const path = await getPathBySlug(db, slug, user.id);
9
+ if (!path) throw createError({ statusCode: 404, statusMessage: 'Path not found' });
10
+
11
+ return enroll(db, user.id, path.id);
12
+ });
@@ -0,0 +1,12 @@
1
+ import { deletePath, getPathBySlug } from '@commonpub/server';
2
+
3
+ export default defineEventHandler(async (event): Promise<boolean> => {
4
+ const user = requireAuth(event);
5
+ const db = useDB();
6
+ const { slug } = parseParams(event, { slug: 'string' });
7
+
8
+ const path = await getPathBySlug(db, slug, user.id);
9
+ if (!path) throw createError({ statusCode: 404, statusMessage: 'Path not found' });
10
+
11
+ return deletePath(db, path.id, user.id);
12
+ });
@@ -0,0 +1,14 @@
1
+ import { getPathBySlug } from '@commonpub/server';
2
+ import type { LearningPathDetail } from '@commonpub/server';
3
+
4
+ export default defineEventHandler(async (event): Promise<LearningPathDetail> => {
5
+ const db = useDB();
6
+ const { slug } = parseParams(event, { slug: 'string' });
7
+ const user = getOptionalUser(event);
8
+
9
+ const path = await getPathBySlug(db, slug, user?.id);
10
+ if (!path) {
11
+ throw createError({ statusCode: 404, statusMessage: 'Learning path not found' });
12
+ }
13
+ return path;
14
+ });
@@ -0,0 +1,19 @@
1
+ import { getPathBySlug, updatePath } from '@commonpub/server';
2
+ import type { LearningPathDetail } from '@commonpub/server';
3
+ import { updateLearningPathSchema } from '@commonpub/schema';
4
+
5
+ export default defineEventHandler(async (event): Promise<LearningPathDetail> => {
6
+ const user = requireAuth(event);
7
+ const db = useDB();
8
+ const { slug } = parseParams(event, { slug: 'string' });
9
+ const input = await parseBody(event, updateLearningPathSchema);
10
+
11
+ const path = await getPathBySlug(db, slug, user.id);
12
+ if (!path) throw createError({ statusCode: 404, statusMessage: 'Path not found' });
13
+
14
+ const updated = await updatePath(db, path.id, user.id, input);
15
+ if (!updated) {
16
+ throw createError({ statusCode: 403, statusMessage: 'Not authorized to update this path' });
17
+ }
18
+ return updated;
19
+ });
@@ -0,0 +1,14 @@
1
+ import { deleteLesson } from '@commonpub/server';
2
+
3
+ export default defineEventHandler(async (event): Promise<{ success: boolean }> => {
4
+ const user = requireAuth(event);
5
+ const db = useDB();
6
+ const { lessonId } = parseParams(event, { lessonId: 'uuid' });
7
+
8
+ const deleted = await deleteLesson(db, lessonId, user.id);
9
+ if (!deleted) {
10
+ throw createError({ statusCode: 404, statusMessage: 'Lesson not found or not authorized' });
11
+ }
12
+
13
+ return { success: true };
14
+ });
@@ -0,0 +1,24 @@
1
+ import { updateLesson } from '@commonpub/server';
2
+ import { z } from 'zod';
3
+
4
+ const updateLessonSchema = z.object({
5
+ title: z.string().min(1).max(255).optional(),
6
+ type: z.enum(['article', 'video', 'quiz', 'project', 'explainer']).optional(),
7
+ content: z.unknown().optional(),
8
+ contentItemId: z.string().uuid().nullable().optional(),
9
+ durationMinutes: z.number().int().min(0).max(9999).optional(),
10
+ });
11
+
12
+ export default defineEventHandler(async (event) => {
13
+ const db = useDB();
14
+ const user = requireAuth(event);
15
+ const { lessonId } = parseParams(event, { lessonId: 'uuid' });
16
+ const input = await parseBody(event, updateLessonSchema);
17
+
18
+ const result = await updateLesson(db, lessonId, user.id, input);
19
+ if (!result) {
20
+ throw createError({ statusCode: 404, statusMessage: 'Lesson not found or not authorized' });
21
+ }
22
+
23
+ return result;
24
+ });
@@ -0,0 +1,10 @@
1
+ import { createLesson } from '@commonpub/server';
2
+ import { createLessonSchema } 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, createLessonSchema);
8
+
9
+ return createLesson(db, user.id, input);
10
+ });
@@ -0,0 +1,14 @@
1
+ import { deleteModule } from '@commonpub/server';
2
+
3
+ export default defineEventHandler(async (event): Promise<{ success: boolean }> => {
4
+ const user = requireAuth(event);
5
+ const db = useDB();
6
+ const { moduleId } = parseParams(event, { moduleId: 'uuid' });
7
+
8
+ const deleted = await deleteModule(db, moduleId, user.id);
9
+ if (!deleted) {
10
+ throw createError({ statusCode: 404, statusMessage: 'Module not found or not authorized' });
11
+ }
12
+
13
+ return { success: true };
14
+ });
@@ -0,0 +1,15 @@
1
+ import { updateModule } from '@commonpub/server';
2
+ import { updateModuleSchema } from '@commonpub/schema';
3
+
4
+ export default defineEventHandler(async (event) => {
5
+ const user = requireAuth(event);
6
+ const db = useDB();
7
+ const { moduleId } = parseParams(event, { moduleId: 'uuid' });
8
+ const input = await parseBody(event, updateModuleSchema);
9
+
10
+ const updated = await updateModule(db, moduleId, user.id, input);
11
+ if (!updated) {
12
+ throw createError({ statusCode: 404, statusMessage: 'Module not found or not authorized' });
13
+ }
14
+ return updated;
15
+ });
@@ -0,0 +1,14 @@
1
+ import { createModule, getPathBySlug } from '@commonpub/server';
2
+ import { createModuleSchema } from '@commonpub/schema';
3
+
4
+ export default defineEventHandler(async (event) => {
5
+ const user = requireAuth(event);
6
+ const db = useDB();
7
+ const { slug } = parseParams(event, { slug: 'string' });
8
+ const input = await parseBody(event, createModuleSchema);
9
+
10
+ const path = await getPathBySlug(db, slug, user.id);
11
+ if (!path) throw createError({ statusCode: 404, statusMessage: 'Path not found' });
12
+
13
+ return createModule(db, user.id, { ...input, pathId: path.id });
14
+ });
@@ -0,0 +1,13 @@
1
+ import { getPathBySlug, publishPath } from '@commonpub/server';
2
+ import type { LearningPathDetail } from '@commonpub/server';
3
+
4
+ export default defineEventHandler(async (event): Promise<LearningPathDetail | null> => {
5
+ const user = requireAuth(event);
6
+ const db = useDB();
7
+ const { slug } = parseParams(event, { slug: 'string' });
8
+
9
+ const path = await getPathBySlug(db, slug, user.id);
10
+ if (!path) throw createError({ statusCode: 404, statusMessage: 'Path not found' });
11
+
12
+ return publishPath(db, path.id, user.id);
13
+ });
@@ -0,0 +1,12 @@
1
+ import { getPathBySlug, unenroll } from '@commonpub/server';
2
+
3
+ export default defineEventHandler(async (event): Promise<boolean> => {
4
+ const user = requireAuth(event);
5
+ const db = useDB();
6
+ const { slug } = parseParams(event, { slug: 'string' });
7
+
8
+ const path = await getPathBySlug(db, slug, user.id);
9
+ if (!path) throw createError({ statusCode: 404, statusMessage: 'Path not found' });
10
+
11
+ return unenroll(db, user.id, path.id);
12
+ });
@@ -0,0 +1,9 @@
1
+ import { getUserCertificates } from '@commonpub/server';
2
+ import type { CertificateItem } from '@commonpub/server';
3
+
4
+ export default defineEventHandler(async (event): Promise<CertificateItem[]> => {
5
+ const user = requireAuth(event);
6
+ const db = useDB();
7
+
8
+ return getUserCertificates(db, user.id);
9
+ });
@@ -0,0 +1,9 @@
1
+ import { getUserEnrollments } from '@commonpub/server';
2
+ import type { EnrollmentItem } from '@commonpub/server';
3
+
4
+ export default defineEventHandler(async (event): Promise<EnrollmentItem[]> => {
5
+ const user = requireAuth(event);
6
+ const db = useDB();
7
+
8
+ return getUserEnrollments(db, user.id);
9
+ });
@@ -0,0 +1,17 @@
1
+ import { listPaths } from '@commonpub/server';
2
+ import type { PaginatedResponse, LearningPathListItem } from '@commonpub/server';
3
+ import { learningPathFiltersSchema } from '@commonpub/schema';
4
+
5
+ export default defineEventHandler(async (event): Promise<PaginatedResponse<LearningPathListItem>> => {
6
+ const db = useDB();
7
+ const user = getOptionalUser(event);
8
+ const filters = parseQueryParams(event, learningPathFiltersSchema);
9
+
10
+ // Allow author to see their own drafts (same pattern as content API)
11
+ const isOwnContent = filters.authorId && user?.id === filters.authorId;
12
+
13
+ return listPaths(db, {
14
+ ...filters,
15
+ status: isOwnContent ? filters.status : (filters.status ?? 'published'),
16
+ });
17
+ });
@@ -0,0 +1,11 @@
1
+ import { createPath } from '@commonpub/server';
2
+ import type { LearningPathDetail } from '@commonpub/server';
3
+ import { createLearningPathSchema } from '@commonpub/schema';
4
+
5
+ export default defineEventHandler(async (event): Promise<LearningPathDetail> => {
6
+ const user = requireAuth(event);
7
+ const db = useDB();
8
+ const input = await parseBody(event, createLearningPathSchema);
9
+
10
+ return createPath(db, user.id, input);
11
+ });
@@ -0,0 +1,13 @@
1
+ /**
2
+ * GET /api/me — Returns the current authenticated user with enriched fields (role, username, status).
3
+ *
4
+ * Unlike /api/auth/get-session which returns raw Better Auth data (missing custom columns),
5
+ * this endpoint reads from event.context.auth which has already been enriched by the auth middleware.
6
+ */
7
+ export default defineEventHandler((event) => {
8
+ const { user, session } = event.context.auth ?? {};
9
+ return {
10
+ user: user ?? null,
11
+ session: session ?? null,
12
+ };
13
+ });
@@ -0,0 +1,43 @@
1
+ import { conversations, users } from '@commonpub/schema';
2
+ import { eq, and, sql, inArray } from 'drizzle-orm';
3
+
4
+ export default defineEventHandler(async (event) => {
5
+ const db = useDB();
6
+ const user = requireAuth(event);
7
+ const { conversationId } = parseParams(event, { conversationId: 'uuid' });
8
+
9
+ const rows = await db
10
+ .select()
11
+ .from(conversations)
12
+ .where(
13
+ and(
14
+ eq(conversations.id, conversationId),
15
+ sql`${conversations.participants} @> ${JSON.stringify([user.id])}::jsonb`,
16
+ ),
17
+ )
18
+ .limit(1);
19
+
20
+ if (!rows.length) {
21
+ throw createError({ statusCode: 404, statusMessage: 'Conversation not found' });
22
+ }
23
+
24
+ const participantIds = (rows[0]!.participants ?? []) as string[];
25
+
26
+ // Resolve participant IDs to display names
27
+ const participantUsers = participantIds.length > 0
28
+ ? await db
29
+ .select({ id: users.id, username: users.username, displayName: users.displayName, avatarUrl: users.avatarUrl })
30
+ .from(users)
31
+ .where(inArray(users.id, participantIds))
32
+ : [];
33
+
34
+ return {
35
+ id: rows[0]!.id,
36
+ participants: participantUsers.map((u) => ({
37
+ id: u.id,
38
+ username: u.username,
39
+ displayName: u.displayName ?? u.username,
40
+ avatarUrl: u.avatarUrl,
41
+ })),
42
+ };
43
+ });
@@ -0,0 +1,73 @@
1
+ import { getConversationMessages } from '@commonpub/server';
2
+
3
+ export default defineEventHandler(async (event) => {
4
+ const user = requireAuth(event);
5
+ const userId = user.id;
6
+ const { conversationId } = parseParams(event, { conversationId: 'uuid' });
7
+
8
+ const db = useDB();
9
+
10
+ setResponseHeader(event, 'Content-Type', 'text/event-stream');
11
+ setResponseHeader(event, 'Cache-Control', 'no-cache');
12
+ setResponseHeader(event, 'Connection', 'keep-alive');
13
+
14
+ const encoder = new TextEncoder();
15
+ let lastMessageCount = 0;
16
+
17
+ const stream = new ReadableStream({
18
+ async start(controller) {
19
+ // Send initial messages
20
+ try {
21
+ const msgs = await getConversationMessages(db, conversationId, userId);
22
+ lastMessageCount = msgs.length;
23
+ controller.enqueue(
24
+ encoder.encode(`data: ${JSON.stringify({ type: 'init', messages: msgs })}\n\n`),
25
+ );
26
+ } catch {
27
+ controller.close();
28
+ return;
29
+ }
30
+
31
+ // Poll for new messages every 3 seconds
32
+ const interval = setInterval(async () => {
33
+ try {
34
+ const msgs = await getConversationMessages(db, conversationId, userId);
35
+ if (msgs.length > lastMessageCount) {
36
+ const newMsgs = msgs.slice(lastMessageCount);
37
+ lastMessageCount = msgs.length;
38
+ controller.enqueue(
39
+ encoder.encode(`data: ${JSON.stringify({ type: 'new', messages: newMsgs })}\n\n`),
40
+ );
41
+ }
42
+ } catch {
43
+ clearInterval(interval);
44
+ controller.close();
45
+ }
46
+ }, 3000);
47
+
48
+ // Keepalive every 25 seconds
49
+ const keepalive = setInterval(() => {
50
+ try {
51
+ controller.enqueue(encoder.encode(': keepalive\n\n'));
52
+ } catch {
53
+ clearInterval(keepalive);
54
+ clearInterval(interval);
55
+ }
56
+ }, 25000);
57
+
58
+ event.node.req.on('close', () => {
59
+ clearInterval(interval);
60
+ clearInterval(keepalive);
61
+ try { controller.close(); } catch { /* already closed */ }
62
+ });
63
+ },
64
+ });
65
+
66
+ return new Response(stream, {
67
+ headers: {
68
+ 'Content-Type': 'text/event-stream',
69
+ 'Cache-Control': 'no-cache',
70
+ 'Connection': 'keep-alive',
71
+ },
72
+ });
73
+ });
@@ -0,0 +1,13 @@
1
+ import { getConversationMessages, markMessagesRead } from '@commonpub/server';
2
+ import type { MessageItem } from '@commonpub/server';
3
+
4
+ export default defineEventHandler(async (event): Promise<MessageItem[]> => {
5
+ const db = useDB();
6
+ const user = requireAuth(event);
7
+ const { conversationId } = parseParams(event, { conversationId: 'uuid' });
8
+
9
+ const messages = await getConversationMessages(db, conversationId, user.id);
10
+ await markMessagesRead(db, conversationId, user.id);
11
+
12
+ return messages;
13
+ });
@@ -0,0 +1,12 @@
1
+ import { sendMessage } from '@commonpub/server';
2
+ import type { MessageItem } from '@commonpub/server';
3
+ import { sendMessageSchema } from '@commonpub/schema';
4
+
5
+ export default defineEventHandler(async (event): Promise<MessageItem> => {
6
+ const db = useDB();
7
+ const user = requireAuth(event);
8
+ const { conversationId } = parseParams(event, { conversationId: 'uuid' });
9
+ const input = await parseBody(event, sendMessageSchema);
10
+
11
+ return sendMessage(db, conversationId, user.id, input.body);
12
+ });
@@ -0,0 +1,39 @@
1
+ import { listConversations } from '@commonpub/server';
2
+ import { users } from '@commonpub/schema';
3
+ import { inArray } from 'drizzle-orm';
4
+
5
+ export default defineEventHandler(async (event) => {
6
+ const db = useDB();
7
+ const user = requireAuth(event);
8
+
9
+ const conversations = await listConversations(db, user.id);
10
+
11
+ // Collect all unique participant IDs
12
+ const allIds = new Set<string>();
13
+ for (const conv of conversations) {
14
+ for (const id of (conv.participants ?? [])) {
15
+ allIds.add(id);
16
+ }
17
+ }
18
+
19
+ // Resolve IDs to user profiles
20
+ const userMap = new Map<string, { username: string; displayName: string | null; avatarUrl: string | null }>();
21
+ if (allIds.size > 0) {
22
+ const resolved = await db
23
+ .select({ id: users.id, username: users.username, displayName: users.displayName, avatarUrl: users.avatarUrl })
24
+ .from(users)
25
+ .where(inArray(users.id, [...allIds]));
26
+ for (const u of resolved) {
27
+ userMap.set(u.id, { username: u.username, displayName: u.displayName, avatarUrl: u.avatarUrl });
28
+ }
29
+ }
30
+
31
+ // Replace participant IDs with resolved user objects
32
+ return conversations.map((conv) => ({
33
+ ...conv,
34
+ participants: (conv.participants ?? []).map((id: string) => {
35
+ const u = userMap.get(id);
36
+ return u ? { username: u.username, displayName: u.displayName, avatarUrl: u.avatarUrl } : { username: id, displayName: null, avatarUrl: null };
37
+ }),
38
+ }));
39
+ });