@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,34 @@
1
+ import { processInboxActivity } from '@commonpub/protocol';
2
+ import { createInboxHandlers } from '@commonpub/server';
3
+ import { verifyInboxRequest, extractDomain } from '../utils/inbox';
4
+
5
+ export default defineEventHandler(async (event) => {
6
+ const config = useConfig();
7
+ if (!config.features.federation) {
8
+ throw createError({ statusCode: 404, statusMessage: 'Not Found' });
9
+ }
10
+
11
+ if (getMethod(event) !== 'POST') {
12
+ throw createError({ statusCode: 405, statusMessage: 'Method Not Allowed' });
13
+ }
14
+
15
+ // Verify signature, domain, date freshness, body size
16
+ const { body } = await verifyInboxRequest(event, 'shared-inbox');
17
+
18
+ const db = useDB();
19
+ const runtimeConfig = useRuntimeConfig();
20
+ const domain = extractDomain((runtimeConfig.public?.siteUrl as string) || `https://${config.instance.domain}`);
21
+ const callbacks = createInboxHandlers({ db, domain });
22
+
23
+ try {
24
+ const result = await processInboxActivity(body, callbacks);
25
+ if (!result.success) {
26
+ throw createError({ statusCode: 400, statusMessage: result.error ?? 'Invalid activity' });
27
+ }
28
+ return { status: 'accepted' };
29
+ } catch (err: unknown) {
30
+ if ((err as { statusCode?: number }).statusCode) throw err;
31
+ console.error('[shared-inbox]', err);
32
+ throw createError({ statusCode: 400, statusMessage: 'Invalid activity' });
33
+ }
34
+ });
@@ -0,0 +1,27 @@
1
+ // NodeInfo 2.1 response
2
+ import { buildNodeInfoResponse } from '@commonpub/protocol';
3
+ import { getPlatformStats } from '@commonpub/server';
4
+
5
+ export default defineEventHandler(async () => {
6
+ const config = useConfig();
7
+ const db = useDB();
8
+
9
+ let userCount = 0;
10
+ let localPostCount = 0;
11
+
12
+ try {
13
+ const stats = await getPlatformStats(db);
14
+ userCount = stats.users.total ?? 0;
15
+ localPostCount = stats.content.total ?? 0;
16
+ } catch {
17
+ // DB may not be available, return zeros
18
+ }
19
+
20
+ return buildNodeInfoResponse({
21
+ config,
22
+ version: '0.0.1',
23
+ userCount,
24
+ activeMonthCount: userCount,
25
+ localPostCount,
26
+ });
27
+ });
@@ -0,0 +1,19 @@
1
+ export default defineEventHandler((event) => {
2
+ const config = useRuntimeConfig();
3
+ const siteUrl = config.public.siteUrl as string;
4
+
5
+ const content = `User-agent: *
6
+ Allow: /
7
+ Disallow: /api/
8
+ Disallow: /admin/
9
+ Disallow: /settings/
10
+ Disallow: /messages/
11
+ Disallow: /create/
12
+
13
+ Sitemap: ${siteUrl}/sitemap.xml
14
+ `;
15
+
16
+ setResponseHeader(event, 'Content-Type', 'text/plain; charset=utf-8');
17
+ setResponseHeader(event, 'Cache-Control', 'public, max-age=86400');
18
+ return content;
19
+ });
@@ -0,0 +1,105 @@
1
+ import { eq } from 'drizzle-orm';
2
+ import { contentItems, users } from '@commonpub/schema';
3
+ import { listHubs, listPaths } from '@commonpub/server';
4
+
5
+ function escapeXml(str: string): string {
6
+ return str
7
+ .replace(/&/g, '&')
8
+ .replace(/</g, '&lt;')
9
+ .replace(/>/g, '&gt;')
10
+ .replace(/"/g, '&quot;')
11
+ .replace(/'/g, '&apos;');
12
+ }
13
+
14
+ export default defineEventHandler(async (event) => {
15
+ const db = useDB();
16
+ const config = useRuntimeConfig();
17
+ const siteUrl = config.public.siteUrl as string;
18
+
19
+ // Published content
20
+ const publishedContent = await db
21
+ .select({
22
+ type: contentItems.type,
23
+ slug: contentItems.slug,
24
+ updatedAt: contentItems.updatedAt,
25
+ })
26
+ .from(contentItems)
27
+ .where(eq(contentItems.status, 'published'));
28
+
29
+ // Users with public profiles
30
+ const publicUsers = await db
31
+ .select({
32
+ username: users.username,
33
+ updatedAt: users.updatedAt,
34
+ })
35
+ .from(users)
36
+ .where(eq(users.status, 'active'));
37
+
38
+ // Hubs
39
+ const { items: hubs } = await listHubs(db, { limit: 100 });
40
+
41
+ // Learning paths
42
+ const { items: paths } = await listPaths(db, { status: 'published', limit: 100 });
43
+
44
+ const urls: Array<{ loc: string; lastmod: string; priority: string; changefreq: string }> = [];
45
+
46
+ // Static pages
47
+ urls.push({ loc: siteUrl, lastmod: new Date().toISOString(), priority: '1.0', changefreq: 'daily' });
48
+ urls.push({ loc: `${siteUrl}/search`, lastmod: new Date().toISOString(), priority: '0.5', changefreq: 'weekly' });
49
+
50
+ // Content pages
51
+ for (const item of publishedContent) {
52
+ urls.push({
53
+ loc: `${siteUrl}/${item.type}/${item.slug}`,
54
+ lastmod: new Date(item.updatedAt).toISOString(),
55
+ priority: '0.8',
56
+ changefreq: 'weekly',
57
+ });
58
+ }
59
+
60
+ // User profiles
61
+ for (const user of publicUsers) {
62
+ urls.push({
63
+ loc: `${siteUrl}/u/${user.username}`,
64
+ lastmod: new Date(user.updatedAt).toISOString(),
65
+ priority: '0.6',
66
+ changefreq: 'weekly',
67
+ });
68
+ }
69
+
70
+ // Hub pages (skip federated hubs — they have canonical URLs on the origin)
71
+ for (const hub of hubs) {
72
+ if ('source' in hub && hub.source === 'federated') continue;
73
+ const localHub = hub as { slug: string; createdAt: Date; updatedAt?: Date };
74
+ urls.push({
75
+ loc: `${siteUrl}/hubs/${localHub.slug}`,
76
+ lastmod: new Date(localHub.updatedAt ?? localHub.createdAt ?? new Date()).toISOString(),
77
+ priority: '0.7',
78
+ changefreq: 'weekly',
79
+ });
80
+ }
81
+
82
+ // Learning paths
83
+ for (const path of paths) {
84
+ urls.push({
85
+ loc: `${siteUrl}/learn/${path.slug}`,
86
+ lastmod: new Date((path as any).updatedAt ?? (path as any).createdAt ?? new Date()).toISOString(),
87
+ priority: '0.7',
88
+ changefreq: 'monthly',
89
+ });
90
+ }
91
+
92
+ const xml = `<?xml version="1.0" encoding="UTF-8"?>
93
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
94
+ ${urls.map((u) => ` <url>
95
+ <loc>${escapeXml(u.loc)}</loc>
96
+ <lastmod>${u.lastmod}</lastmod>
97
+ <changefreq>${u.changefreq}</changefreq>
98
+ <priority>${u.priority}</priority>
99
+ </url>`).join('\n')}
100
+ </urlset>`;
101
+
102
+ setResponseHeader(event, 'Content-Type', 'application/xml; charset=utf-8');
103
+ setResponseHeader(event, 'Cache-Control', 'public, max-age=3600, stale-while-revalidate=1800');
104
+ return xml;
105
+ });
@@ -0,0 +1,33 @@
1
+ import { getUserByUsername, getFollowers } from '@commonpub/server';
2
+
3
+ export default defineEventHandler(async (event) => {
4
+ const username = getRouterParam(event, 'username')!;
5
+ const db = useDB();
6
+ const config = useConfig();
7
+
8
+ const profile = await getUserByUsername(db, username);
9
+ if (!profile) {
10
+ throw createError({ statusCode: 404, statusMessage: 'Actor not found' });
11
+ }
12
+
13
+ const domain = config.instance.domain;
14
+ const actorUri = `https://${domain}/users/${username}`;
15
+
16
+ let followers: string[] = [];
17
+ try {
18
+ const result = await getFollowers(db, actorUri);
19
+ followers = result.map((f) => f.followerActorUri);
20
+ } catch {
21
+ // May not have federation tables
22
+ }
23
+
24
+ setResponseHeader(event, 'content-type', 'application/activity+json');
25
+
26
+ return {
27
+ '@context': 'https://www.w3.org/ns/activitystreams',
28
+ id: `${actorUri}/followers`,
29
+ type: 'OrderedCollection',
30
+ totalItems: followers.length,
31
+ orderedItems: followers,
32
+ };
33
+ });
@@ -0,0 +1,33 @@
1
+ import { getUserByUsername, getFollowing } from '@commonpub/server';
2
+
3
+ export default defineEventHandler(async (event) => {
4
+ const username = getRouterParam(event, 'username')!;
5
+ const db = useDB();
6
+ const config = useConfig();
7
+
8
+ const profile = await getUserByUsername(db, username);
9
+ if (!profile) {
10
+ throw createError({ statusCode: 404, statusMessage: 'Actor not found' });
11
+ }
12
+
13
+ const domain = config.instance.domain;
14
+ const actorUri = `https://${domain}/users/${username}`;
15
+
16
+ let following: string[] = [];
17
+ try {
18
+ const result = await getFollowing(db, actorUri);
19
+ following = result.map((f) => f.followingActorUri);
20
+ } catch {
21
+ // May not have federation tables
22
+ }
23
+
24
+ setResponseHeader(event, 'content-type', 'application/activity+json');
25
+
26
+ return {
27
+ '@context': 'https://www.w3.org/ns/activitystreams',
28
+ id: `${actorUri}/following`,
29
+ type: 'OrderedCollection',
30
+ totalItems: following.length,
31
+ orderedItems: following,
32
+ };
33
+ });
@@ -0,0 +1,34 @@
1
+ import { processInboxActivity } from '@commonpub/protocol';
2
+ import { createInboxHandlers } from '@commonpub/server';
3
+ import { verifyInboxRequest, extractDomain } from '../../../utils/inbox';
4
+
5
+ export default defineEventHandler(async (event) => {
6
+ const config = useConfig();
7
+ if (!config.features.federation) {
8
+ throw createError({ statusCode: 404, statusMessage: 'Not Found' });
9
+ }
10
+
11
+ if (getMethod(event) !== 'POST') {
12
+ throw createError({ statusCode: 405, statusMessage: 'Method Not Allowed' });
13
+ }
14
+
15
+ // Verify signature, domain, date freshness, body size
16
+ const { body } = await verifyInboxRequest(event, 'user-inbox');
17
+
18
+ const db = useDB();
19
+ const runtimeConfig = useRuntimeConfig();
20
+ const domain = extractDomain((runtimeConfig.public?.siteUrl as string) || `https://${config.instance.domain}`);
21
+ const callbacks = createInboxHandlers({ db, domain });
22
+
23
+ try {
24
+ const result = await processInboxActivity(body, callbacks);
25
+ if (!result.success) {
26
+ throw createError({ statusCode: 400, statusMessage: result.error ?? 'Invalid activity' });
27
+ }
28
+ return { status: 'accepted' };
29
+ } catch (err: unknown) {
30
+ if ((err as { statusCode?: number }).statusCode) throw err;
31
+ console.error('[user-inbox]', err);
32
+ throw createError({ statusCode: 400, statusMessage: 'Invalid activity' });
33
+ }
34
+ });
@@ -0,0 +1,35 @@
1
+ import { generateOutboxCollection, generateOutboxPage } from '@commonpub/protocol';
2
+ import { getUserByUsername, countOutboxItems, getOutboxPage } from '@commonpub/server';
3
+
4
+ const PAGE_SIZE = 20;
5
+
6
+ /**
7
+ * User actor outbox — serves paginated outbound activities for a specific user.
8
+ * Returns Create/Update/Delete activities that were successfully delivered.
9
+ */
10
+ export default defineEventHandler(async (event) => {
11
+ const username = getRouterParam(event, 'username')!;
12
+ const db = useDB();
13
+ const config = useConfig();
14
+
15
+ const profile = await getUserByUsername(db, username);
16
+ if (!profile) {
17
+ throw createError({ statusCode: 404, statusMessage: 'Actor not found' });
18
+ }
19
+
20
+ const domain = config.instance.domain;
21
+ const actorUri = `https://${domain}/users/${username}`;
22
+ const query = getQuery(event);
23
+ const page = query.page ? parseInt(String(query.page), 10) : 0;
24
+
25
+ setResponseHeader(event, 'content-type', 'application/activity+json');
26
+
27
+ const totalItems = await countOutboxItems(db, actorUri);
28
+
29
+ if (!page || isNaN(page)) {
30
+ return generateOutboxCollection(totalItems, domain, username);
31
+ }
32
+
33
+ const items = await getOutboxPage(db, actorUri, page, PAGE_SIZE);
34
+ return generateOutboxPage(items as never[], domain, username, page, PAGE_SIZE, totalItems);
35
+ });
@@ -0,0 +1,62 @@
1
+ import { getUserByUsername, getOrCreateActorKeypair } from '@commonpub/server';
2
+
3
+ export default defineEventHandler(async (event) => {
4
+ const username = getRouterParam(event, 'username')!;
5
+ const accept = getRequestHeader(event, 'accept') || '';
6
+
7
+ // Only serve AP actor for ActivityPub clients
8
+ if (!accept.includes('application/activity+json') && !accept.includes('application/ld+json')) {
9
+ // Redirect browsers to the profile page
10
+ return sendRedirect(event, `/u/${username}`);
11
+ }
12
+
13
+ const db = useDB();
14
+ const config = useConfig();
15
+ const profile = await getUserByUsername(db, username);
16
+
17
+ if (!profile) {
18
+ throw createError({ statusCode: 404, statusMessage: 'Actor not found' });
19
+ }
20
+
21
+ const domain = config.instance.domain;
22
+ const actorUri = `https://${domain}/users/${username}`;
23
+
24
+ let publicKeyPem = '';
25
+ try {
26
+ const keypair = await getOrCreateActorKeypair(db, profile.id);
27
+ publicKeyPem = keypair.publicKeyPem;
28
+ } catch {
29
+ // Key generation may fail if crypto is unavailable
30
+ }
31
+
32
+ setResponseHeader(event, 'content-type', 'application/activity+json');
33
+
34
+ return {
35
+ '@context': [
36
+ 'https://www.w3.org/ns/activitystreams',
37
+ 'https://w3id.org/security/v1',
38
+ ],
39
+ id: actorUri,
40
+ type: 'Person',
41
+ preferredUsername: username,
42
+ name: profile.displayName || username,
43
+ summary: profile.bio || '',
44
+ inbox: `${actorUri}/inbox`,
45
+ outbox: `${actorUri}/outbox`,
46
+ followers: `${actorUri}/followers`,
47
+ following: `${actorUri}/following`,
48
+ url: `https://${domain}/u/${username}`,
49
+ endpoints: {
50
+ sharedInbox: `https://${domain}/inbox`,
51
+ },
52
+ ...(publicKeyPem
53
+ ? {
54
+ publicKey: {
55
+ id: `${actorUri}#main-key`,
56
+ owner: actorUri,
57
+ publicKeyPem,
58
+ },
59
+ }
60
+ : {}),
61
+ };
62
+ });
@@ -0,0 +1,36 @@
1
+ // Auth helper — extracts authenticated user from event context
2
+ import type { H3Event } from 'h3';
3
+
4
+ export interface AuthUser {
5
+ id: string;
6
+ username: string;
7
+ role: string;
8
+ }
9
+
10
+ export function requireAuth(event: H3Event): AuthUser {
11
+ const auth = event.context.auth;
12
+ if (!auth?.user) {
13
+ const cookie = getRequestHeader(event, 'cookie') || '';
14
+ const hasSessionCookie = cookie.includes('better-auth.session_token');
15
+ throw createError({
16
+ statusCode: 401,
17
+ statusMessage: hasSessionCookie
18
+ ? 'Session expired or invalid. Please log in again.'
19
+ : 'Not logged in. Please log in to continue.',
20
+ });
21
+ }
22
+ return auth.user as AuthUser;
23
+ }
24
+
25
+ export function requireAdmin(event: H3Event): AuthUser {
26
+ const user = requireAuth(event);
27
+ if (user.role !== 'admin') {
28
+ throw createError({ statusCode: 403, statusMessage: 'Admin access required' });
29
+ }
30
+ return user;
31
+ }
32
+
33
+ export function getOptionalUser(event: H3Event): AuthUser | null {
34
+ const auth = event.context.auth;
35
+ return (auth?.user as AuthUser) ?? null;
36
+ }
@@ -0,0 +1,34 @@
1
+ // Singleton Drizzle DB instance for Nitro server
2
+ import { drizzle } from 'drizzle-orm/node-postgres';
3
+ // @ts-expect-error no types for pg
4
+ import pg from 'pg';
5
+ import * as schema from '@commonpub/schema';
6
+ import type { DB } from '@commonpub/server';
7
+
8
+ let db: DB | null = null;
9
+
10
+ export function useDB(): DB {
11
+ if (db) return db;
12
+
13
+ const config = useRuntimeConfig();
14
+ const databaseUrl = config.databaseUrl as string;
15
+
16
+ if (!databaseUrl) {
17
+ throw new Error('DATABASE_URL is not configured. Set NUXT_DATABASE_URL environment variable.');
18
+ }
19
+
20
+ // Guard against default auth secret in production
21
+ if (process.env.NODE_ENV === 'production' && config.authSecret === 'dev-secret-change-me') {
22
+ throw new Error('NUXT_AUTH_SECRET must be set in production. Do not use the default dev secret.');
23
+ }
24
+
25
+ const pool = new pg.Pool({
26
+ connectionString: databaseUrl,
27
+ max: 20,
28
+ idleTimeoutMillis: 30_000,
29
+ connectionTimeoutMillis: 5_000,
30
+ });
31
+ db = drizzle(pool, { schema });
32
+
33
+ return db;
34
+ }
@@ -0,0 +1,24 @@
1
+ // Consistent error helpers for Nitro API routes
2
+
3
+ export function validationError(errors: Record<string, string[]>): never {
4
+ throw createError({
5
+ statusCode: 400,
6
+ statusMessage: 'Validation failed',
7
+ data: { errors },
8
+ });
9
+ }
10
+
11
+ export function notFound(entity: string): never {
12
+ throw createError({
13
+ statusCode: 404,
14
+ statusMessage: `${entity} not found`,
15
+ });
16
+ }
17
+
18
+ export function forbidden(message = 'Permission denied'): never {
19
+ throw createError({ statusCode: 403, statusMessage: message });
20
+ }
21
+
22
+ export function badRequest(message: string): never {
23
+ throw createError({ statusCode: 400, statusMessage: message });
24
+ }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Shared inbox verification utilities.
3
+ * Handles HTTP Signature verification, actor domain validation,
4
+ * body size limits, and Date header freshness checks.
5
+ */
6
+ import { verifyHttpSignature, resolveActor } from '@commonpub/protocol';
7
+ import type { H3Event } from 'h3';
8
+
9
+ /** Maximum allowed body size for inbox POSTs (1 MB) */
10
+ const MAX_BODY_SIZE = 1_048_576;
11
+
12
+ /** Maximum allowed clock skew for Date header (5 minutes) */
13
+ const MAX_DATE_SKEW_MS = 5 * 60 * 1000;
14
+
15
+ function extractKeyId(signatureHeader: string): string | null {
16
+ const match = signatureHeader.match(/keyId="([^"]+)"/);
17
+ return match ? match[1] : null;
18
+ }
19
+
20
+ /** Extract clean domain from a URL string */
21
+ export function extractDomain(url: string): string {
22
+ try {
23
+ const parsed = new URL(url);
24
+ if (parsed.hostname) return parsed.hostname;
25
+ } catch { /* fall through */ }
26
+ return url.replace(/^https?:\/\//, '').replace(/[:/].*$/, '');
27
+ }
28
+
29
+ interface VerifiedInbox {
30
+ actorUri: string;
31
+ body: Record<string, unknown>;
32
+ }
33
+
34
+ /**
35
+ * Verify an inbound AP activity request.
36
+ * Checks: body size, signature presence, actor resolution, domain match,
37
+ * Date freshness, and HTTP Signature cryptographic verification.
38
+ *
39
+ * Throws H3Error on any failure.
40
+ */
41
+ export async function verifyInboxRequest(event: H3Event, label: string): Promise<VerifiedInbox> {
42
+ // 1. Body size check
43
+ const contentLength = getHeader(event, 'content-length');
44
+ if (contentLength && parseInt(contentLength, 10) > MAX_BODY_SIZE) {
45
+ throw createError({ statusCode: 413, statusMessage: 'Payload too large' });
46
+ }
47
+
48
+ // 2. Require Signature header
49
+ const signatureHeader = getHeader(event, 'signature');
50
+ if (!signatureHeader) {
51
+ throw createError({ statusCode: 401, statusMessage: 'Missing HTTP Signature' });
52
+ }
53
+
54
+ // 3. Extract and validate keyId
55
+ const keyId = extractKeyId(signatureHeader);
56
+ if (!keyId) {
57
+ throw createError({ statusCode: 401, statusMessage: 'Invalid Signature header: missing keyId' });
58
+ }
59
+
60
+ const actorUri = keyId.replace(/#.*$/, '');
61
+
62
+ // 4. Resolve actor and public key
63
+ const actor = await resolveActor(actorUri, fetch);
64
+ if (!actor?.publicKey?.publicKeyPem) {
65
+ throw createError({ statusCode: 401, statusMessage: 'Could not resolve actor public key' });
66
+ }
67
+
68
+ // 5. Actor domain validation — keyId domain must match resolved actor id domain
69
+ try {
70
+ const keyIdDomain = new URL(actorUri).hostname;
71
+ const actorIdDomain = new URL(actor.id ?? actorUri).hostname;
72
+ if (keyIdDomain !== actorIdDomain) {
73
+ console.warn(`[${label}] Domain mismatch: keyId=${keyIdDomain}, actor.id=${actorIdDomain}`);
74
+ throw createError({ statusCode: 401, statusMessage: 'Actor domain does not match keyId domain' });
75
+ }
76
+ } catch (err) {
77
+ if ((err as { statusCode?: number }).statusCode) throw err;
78
+ throw createError({ statusCode: 401, statusMessage: 'Invalid actor URI' });
79
+ }
80
+
81
+ // 6. Date header freshness check
82
+ const dateHeader = getHeader(event, 'date');
83
+ if (dateHeader) {
84
+ const requestDate = new Date(dateHeader).getTime();
85
+ if (!isNaN(requestDate)) {
86
+ const skew = Math.abs(Date.now() - requestDate);
87
+ if (skew > MAX_DATE_SKEW_MS) {
88
+ console.warn(`[${label}] Date header too old/new: skew=${Math.round(skew / 1000)}s from ${actorUri}`);
89
+ throw createError({ statusCode: 401, statusMessage: 'Request date too far from server time' });
90
+ }
91
+ }
92
+ }
93
+
94
+ // 7. Read body and reconstruct Request for signature verification
95
+ const body = await readBody(event);
96
+ const bodyStr = typeof body === 'string' ? body : JSON.stringify(body);
97
+
98
+ // Body size check on actual content (in case Content-Length was missing/wrong)
99
+ if (bodyStr.length > MAX_BODY_SIZE) {
100
+ throw createError({ statusCode: 413, statusMessage: 'Payload too large' });
101
+ }
102
+
103
+ const url = getRequestURL(event);
104
+ const headers = new Headers();
105
+ for (const [key, value] of Object.entries(getHeaders(event))) {
106
+ if (value) headers.set(key, Array.isArray(value) ? value[0]! : value);
107
+ }
108
+ const verifyRequest = new Request(url.toString(), {
109
+ method: 'POST',
110
+ headers,
111
+ body: bodyStr,
112
+ });
113
+
114
+ // 8. Verify HTTP Signature cryptographically
115
+ const signatureValid = await verifyHttpSignature(verifyRequest, actor.publicKey.publicKeyPem);
116
+ if (!signatureValid) {
117
+ console.warn(`[${label}] HTTP Signature verification failed for ${actorUri}`);
118
+ throw createError({ statusCode: 401, statusMessage: 'Invalid HTTP Signature' });
119
+ }
120
+
121
+ return { actorUri, body };
122
+ }