@meith/web 0.16.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 (511) hide show
  1. package/LICENSE.md +165 -0
  2. package/app/(auth)/layout.tsx +6 -0
  3. package/app/(auth)/login/page.tsx +106 -0
  4. package/app/(auth)/login/verify/page.tsx +55 -0
  5. package/app/(auth)/register/page.tsx +83 -0
  6. package/app/(auth)/reset/confirm/page.tsx +45 -0
  7. package/app/(auth)/reset/page.tsx +22 -0
  8. package/app/(auth)/verify/page.tsx +44 -0
  9. package/app/(auth)/verify/resend/page.tsx +51 -0
  10. package/app/(board)/[slug]/feed.xml/route.ts +48 -0
  11. package/app/(board)/[slug]/new/page.tsx +112 -0
  12. package/app/(board)/[slug]/page.tsx +403 -0
  13. package/app/(board)/discover/[view]/page.tsx +115 -0
  14. package/app/(board)/discover/page.tsx +5 -0
  15. package/app/(board)/layout.tsx +22 -0
  16. package/app/(board)/member/[id]/page.tsx +187 -0
  17. package/app/(board)/member/[id]/reputation/page.tsx +195 -0
  18. package/app/(board)/member/by-name/[name]/page.tsx +32 -0
  19. package/app/(board)/messages/[id]/page.tsx +103 -0
  20. package/app/(board)/messages/compose/page.tsx +60 -0
  21. package/app/(board)/messages/layout.tsx +5 -0
  22. package/app/(board)/messages/page.tsx +179 -0
  23. package/app/(board)/modcp/forums/page.tsx +59 -0
  24. package/app/(board)/modcp/ip/page.tsx +158 -0
  25. package/app/(board)/modcp/layout.tsx +5 -0
  26. package/app/(board)/modcp/log/page.tsx +98 -0
  27. package/app/(board)/modcp/page.tsx +112 -0
  28. package/app/(board)/moderation/layout.tsx +5 -0
  29. package/app/(board)/moderation/page.tsx +111 -0
  30. package/app/(board)/moderation/reports/page.tsx +193 -0
  31. package/app/(board)/moderation/warn/page.tsx +158 -0
  32. package/app/(board)/notifications/layout.tsx +5 -0
  33. package/app/(board)/notifications/page.tsx +144 -0
  34. package/app/(board)/notifications/preferences/page.tsx +65 -0
  35. package/app/(board)/online/page.tsx +119 -0
  36. package/app/(board)/page.tsx +195 -0
  37. package/app/(board)/plugins/[key]/[[...path]]/page.tsx +72 -0
  38. package/app/(board)/privacy/page.tsx +12 -0
  39. package/app/(board)/report/page.tsx +71 -0
  40. package/app/(board)/rules/page.tsx +12 -0
  41. package/app/(board)/search/[token]/page.tsx +130 -0
  42. package/app/(board)/search/page.tsx +289 -0
  43. package/app/(board)/stats/page.tsx +202 -0
  44. package/app/(board)/subscriptions/layout.tsx +5 -0
  45. package/app/(board)/subscriptions/page.tsx +142 -0
  46. package/app/(board)/terms/page.tsx +12 -0
  47. package/app/(board)/thread/[slug]/edit/page.tsx +107 -0
  48. package/app/(board)/thread/[slug]/feed.xml/route.ts +42 -0
  49. package/app/(board)/thread/[slug]/page.tsx +707 -0
  50. package/app/(board)/thread/[slug]/poll/page.tsx +41 -0
  51. package/app/(board)/thread/[slug]/post/[postId]/history/page.tsx +160 -0
  52. package/app/(board)/thread/[slug]/reply/page.tsx +136 -0
  53. package/app/(board)/unsubscribe/page.tsx +73 -0
  54. package/app/(board)/usercp/avatar/page.tsx +56 -0
  55. package/app/(board)/usercp/contacts/page.tsx +141 -0
  56. package/app/(board)/usercp/email/confirm/page.tsx +52 -0
  57. package/app/(board)/usercp/layout.tsx +23 -0
  58. package/app/(board)/usercp/options/page.tsx +56 -0
  59. package/app/(board)/usercp/page.tsx +71 -0
  60. package/app/(board)/usercp/profile/page.tsx +81 -0
  61. package/app/(board)/usercp/security/page.tsx +238 -0
  62. package/app/(board)/usercp/security/verify/page.tsx +92 -0
  63. package/app/(board)/usercp/signature/page.tsx +67 -0
  64. package/app/admin/antispam/page.tsx +155 -0
  65. package/app/admin/api/marketplace/screenshot/route.ts +68 -0
  66. package/app/admin/api/plugins/[key]/[...path]/route.ts +27 -0
  67. package/app/admin/api-tokens/page.tsx +129 -0
  68. package/app/admin/content/announcements/page.tsx +110 -0
  69. package/app/admin/content/attachments/page.tsx +199 -0
  70. package/app/admin/content/navigation/page.tsx +123 -0
  71. package/app/admin/content/page.tsx +170 -0
  72. package/app/admin/forums/[id]/page.tsx +96 -0
  73. package/app/admin/forums/[id]/permissions/page.tsx +108 -0
  74. package/app/admin/forums/page.tsx +45 -0
  75. package/app/admin/groups/[id]/page.tsx +133 -0
  76. package/app/admin/groups/memberships/page.tsx +56 -0
  77. package/app/admin/groups/page.tsx +104 -0
  78. package/app/admin/groups/promotions/page.tsx +143 -0
  79. package/app/admin/layout.tsx +86 -0
  80. package/app/admin/log/page.tsx +116 -0
  81. package/app/admin/page.tsx +206 -0
  82. package/app/admin/plugins/[key]/[[...path]]/page.tsx +276 -0
  83. package/app/admin/plugins/browse/page.tsx +70 -0
  84. package/app/admin/plugins/page.tsx +165 -0
  85. package/app/admin/security/page.tsx +124 -0
  86. package/app/admin/settings/page.tsx +190 -0
  87. package/app/admin/system/page.tsx +303 -0
  88. package/app/admin/themes/[key]/page.tsx +110 -0
  89. package/app/admin/themes/browse/page.tsx +70 -0
  90. package/app/admin/themes/page.tsx +163 -0
  91. package/app/admin/users/[id]/merge/page.tsx +136 -0
  92. package/app/admin/users/[id]/page.tsx +208 -0
  93. package/app/admin/users/mail/page.tsx +68 -0
  94. package/app/admin/users/page.tsx +260 -0
  95. package/app/admin/users/prune/page.tsx +186 -0
  96. package/app/api/health/route.ts +11 -0
  97. package/app/api/metrics/route.ts +66 -0
  98. package/app/api/plugins/[key]/[...path]/route.ts +27 -0
  99. package/app/api/read/all/route.ts +15 -0
  100. package/app/api/read/forum/[id]/route.ts +29 -0
  101. package/app/api/read/thread/[id]/route.ts +40 -0
  102. package/app/api/ready/route.ts +63 -0
  103. package/app/api/system/tick/route.ts +77 -0
  104. package/app/api/v1/[...path]/route.ts +208 -0
  105. package/app/api/v1/openapi.json/route.ts +12 -0
  106. package/app/apple-icon.ts +11 -0
  107. package/app/atom.xml/route.ts +7 -0
  108. package/app/attachment/[id]/route.ts +10 -0
  109. package/app/attachment/[id]/thumb/route.ts +10 -0
  110. package/app/auth/passkey/options/route.ts +145 -0
  111. package/app/auth/passkey/verify/route.ts +241 -0
  112. package/app/auth/resume/route.ts +55 -0
  113. package/app/auth/sso/[provider]/callback/route.ts +173 -0
  114. package/app/auth/sso/[provider]/go/route.ts +57 -0
  115. package/app/auth/sso/[provider]/route.ts +105 -0
  116. package/app/avatar/[id]/route.ts +31 -0
  117. package/app/brand/icon-192.png/route.ts +7 -0
  118. package/app/brand/icon-512.png/route.ts +7 -0
  119. package/app/brand/icon-maskable.png/route.ts +7 -0
  120. package/app/demo/stripe/[[...path]]/route.ts +27 -0
  121. package/app/error.tsx +15 -0
  122. package/app/feed.xml/route.ts +7 -0
  123. package/app/forumdisplay.php/page.tsx +11 -0
  124. package/app/group/[id]/badge/[scheme]/route.ts +26 -0
  125. package/app/icon.ts +13 -0
  126. package/app/install/page.tsx +221 -0
  127. package/app/jump/page.tsx +28 -0
  128. package/app/layout.tsx +124 -0
  129. package/app/logo/[scheme]/route.ts +24 -0
  130. package/app/manifest.ts +43 -0
  131. package/app/member.php/page.tsx +11 -0
  132. package/app/memberlist.php/page.tsx +11 -0
  133. package/app/not-found.tsx +14 -0
  134. package/app/opengraph-image.ts +11 -0
  135. package/app/redirect/page.tsx +44 -0
  136. package/app/robots.txt/route.ts +35 -0
  137. package/app/showthread.php/page.tsx +11 -0
  138. package/app/sitemap/[page]/route.ts +64 -0
  139. package/app/sitemap.xml/route.ts +31 -0
  140. package/app/twitter-image.ts +11 -0
  141. package/app/viewforum.php/page.tsx +11 -0
  142. package/app/viewtopic.php/page.tsx +11 -0
  143. package/bin/forum-web.mjs +241 -0
  144. package/components.json +21 -0
  145. package/instrumentation.ts +18 -0
  146. package/next.config.mjs +182 -0
  147. package/package.json +99 -0
  148. package/postcss.config.mjs +7 -0
  149. package/proxy.ts +160 -0
  150. package/src/components/account/active-sessions.tsx +87 -0
  151. package/src/components/account/avatar-form.tsx +119 -0
  152. package/src/components/account/credential-proof-form.tsx +76 -0
  153. package/src/components/account/logout-form.tsx +14 -0
  154. package/src/components/account/notification-forms.tsx +157 -0
  155. package/src/components/account/passkey-enrol.tsx +81 -0
  156. package/src/components/account/passkey-proof-button.tsx +47 -0
  157. package/src/components/account/push-device-form.tsx +193 -0
  158. package/src/components/account/relation-forms.tsx +69 -0
  159. package/src/components/account/reputation-forms.tsx +124 -0
  160. package/src/components/account/security-activity.tsx +46 -0
  161. package/src/components/account/sign-in-methods.tsx +166 -0
  162. package/src/components/account/subscription-forms.tsx +151 -0
  163. package/src/components/account/two-factor-forms.tsx +226 -0
  164. package/src/components/account/usercp-forms.tsx +387 -0
  165. package/src/components/account/usercp-nav.tsx +14 -0
  166. package/src/components/account/usercp-shell.tsx +34 -0
  167. package/src/components/admin/admin-forms.tsx +92 -0
  168. package/src/components/admin/admin-nav.tsx +52 -0
  169. package/src/components/admin/admin-undo.tsx +32 -0
  170. package/src/components/admin/api-token-forms.tsx +89 -0
  171. package/src/components/admin/badge-forms.tsx +97 -0
  172. package/src/components/admin/branding-forms.tsx +102 -0
  173. package/src/components/admin/content-forms.tsx +712 -0
  174. package/src/components/admin/form-bits.tsx +11 -0
  175. package/src/components/admin/forum-forms.tsx +524 -0
  176. package/src/components/admin/forum-tree.tsx +454 -0
  177. package/src/components/admin/group-forms.tsx +646 -0
  178. package/src/components/admin/mail-test-card.tsx +90 -0
  179. package/src/components/admin/marketplace-forms.tsx +30 -0
  180. package/src/components/admin/marketplace-listing.tsx +123 -0
  181. package/src/components/admin/navigation-forms.tsx +229 -0
  182. package/src/components/admin/navigation-tree.tsx +442 -0
  183. package/src/components/admin/oklch-picker.tsx +180 -0
  184. package/src/components/admin/plugin-forms.tsx +225 -0
  185. package/src/components/admin/settings-form.tsx +148 -0
  186. package/src/components/admin/system-forms.tsx +169 -0
  187. package/src/components/admin/theme-changes.tsx +354 -0
  188. package/src/components/admin/theme-forms.tsx +636 -0
  189. package/src/components/admin/theme-palette.tsx +119 -0
  190. package/src/components/admin/theme-preview.tsx +461 -0
  191. package/src/components/admin/user-bulk-toolbar.tsx +99 -0
  192. package/src/components/admin/user-forms.tsx +488 -0
  193. package/src/components/auth/auth-page.tsx +45 -0
  194. package/src/components/auth/form-controls.tsx +117 -0
  195. package/src/components/auth/login-form.tsx +47 -0
  196. package/src/components/auth/passkey-client.ts +171 -0
  197. package/src/components/auth/passkey-second-factor.tsx +69 -0
  198. package/src/components/auth/passkey-sign-in.tsx +61 -0
  199. package/src/components/auth/register-form.tsx +136 -0
  200. package/src/components/auth/resend-verification-form.tsx +34 -0
  201. package/src/components/auth/reset-confirm-form.tsx +43 -0
  202. package/src/components/auth/reset-request-form.tsx +35 -0
  203. package/src/components/auth/second-factor-form.tsx +51 -0
  204. package/src/components/auth/sso-buttons.tsx +47 -0
  205. package/src/components/board/live-region.tsx +74 -0
  206. package/src/components/board/search-off-notice.tsx +18 -0
  207. package/src/components/board/section-page.tsx +114 -0
  208. package/src/components/content/attachment-field.tsx +51 -0
  209. package/src/components/content/composer-field.ts +31 -0
  210. package/src/components/content/composer-ids.ts +6 -0
  211. package/src/components/content/composer-intents.tsx +27 -0
  212. package/src/components/content/composer-recovery.tsx +215 -0
  213. package/src/components/content/edit-post-form.tsx +121 -0
  214. package/src/components/content/markdown-editor.tsx +590 -0
  215. package/src/components/content/markdown-syntax.ts +53 -0
  216. package/src/components/content/mention-token.ts +23 -0
  217. package/src/components/content/multiquote-button.tsx +32 -0
  218. package/src/components/content/multiquote-selection.tsx +105 -0
  219. package/src/components/content/multiquote.ts +125 -0
  220. package/src/components/content/new-thread-form.tsx +169 -0
  221. package/src/components/content/poll-edit-form.tsx +96 -0
  222. package/src/components/content/poll.tsx +89 -0
  223. package/src/components/content/quote-in-place.tsx +51 -0
  224. package/src/components/content/reply-form.tsx +113 -0
  225. package/src/components/content/thanks-button.tsx +65 -0
  226. package/src/components/content/thread-rating.tsx +143 -0
  227. package/src/components/install/install-form.tsx +497 -0
  228. package/src/components/legal/legal-document.tsx +22 -0
  229. package/src/components/messages/message-forms.tsx +152 -0
  230. package/src/components/moderation/inline-moderation-form.tsx +158 -0
  231. package/src/components/moderation/modcp-nav.tsx +15 -0
  232. package/src/components/moderation/modcp-shell.tsx +41 -0
  233. package/src/components/moderation/queue-form.tsx +96 -0
  234. package/src/components/moderation/report-forms.tsx +120 -0
  235. package/src/components/moderation/signature-lock-form.tsx +83 -0
  236. package/src/components/moderation/thread-surgery-form.tsx +96 -0
  237. package/src/components/moderation/thread-tools-form.tsx +137 -0
  238. package/src/components/moderation/warning-forms.tsx +125 -0
  239. package/src/components/profile/custom-field.tsx +79 -0
  240. package/src/components/shell/appearance-icons.tsx +35 -0
  241. package/src/components/shell/board-notice.tsx +21 -0
  242. package/src/components/shell/copy-record.ts +20 -0
  243. package/src/components/shell/copy.tsx +17 -0
  244. package/src/components/shell/crash-notice.tsx +86 -0
  245. package/src/components/shell/demo-banner.tsx +49 -0
  246. package/src/components/shell/group-name-style.tsx +6 -0
  247. package/src/components/shell/measure.ts +1 -0
  248. package/src/components/shell/notification-menu.tsx +361 -0
  249. package/src/components/shell/offline-notice.tsx +29 -0
  250. package/src/components/shell/onboarding-banner.tsx +39 -0
  251. package/src/components/shell/page-shell.tsx +167 -0
  252. package/src/components/shell/panel-list.tsx +95 -0
  253. package/src/components/shell/panel-nav.tsx +40 -0
  254. package/src/components/shell/panel-overview.tsx +81 -0
  255. package/src/components/shell/panel-page.tsx +96 -0
  256. package/src/components/shell/panel-pagination.tsx +28 -0
  257. package/src/components/shell/panel-shell.tsx +31 -0
  258. package/src/components/shell/theme-runtime-style.tsx +6 -0
  259. package/src/components/shell/theme-switcher.tsx +97 -0
  260. package/src/components/shell/timezone-probe.tsx +45 -0
  261. package/src/components/shell/view-tabs.tsx +60 -0
  262. package/src/config.ts +13 -0
  263. package/src/server/absent-services.ts +29 -0
  264. package/src/server/admin-actions.ts +171 -0
  265. package/src/server/admin-settings-actions.ts +83 -0
  266. package/src/server/admin-undo-actions.ts +70 -0
  267. package/src/server/admin-undo.ts +75 -0
  268. package/src/server/admin.ts +112 -0
  269. package/src/server/announcements.ts +82 -0
  270. package/src/server/antispam.ts +310 -0
  271. package/src/server/api/content.ts +486 -0
  272. package/src/server/api/http.ts +119 -0
  273. package/src/server/api/members.ts +109 -0
  274. package/src/server/api/messages.ts +113 -0
  275. package/src/server/api/registry.ts +30 -0
  276. package/src/server/api/search.ts +98 -0
  277. package/src/server/api/subscriptions.ts +106 -0
  278. package/src/server/api-auth.ts +66 -0
  279. package/src/server/api-token-actions.ts +85 -0
  280. package/src/server/api-tokens-admin.ts +95 -0
  281. package/src/server/appearance-actions.ts +39 -0
  282. package/src/server/attachment-download.ts +43 -0
  283. package/src/server/attachment-embed.ts +63 -0
  284. package/src/server/attachment-upload-actions.ts +92 -0
  285. package/src/server/attachments.ts +257 -0
  286. package/src/server/auth-actions.ts +479 -0
  287. package/src/server/auth-config.ts +61 -0
  288. package/src/server/auth-events.ts +92 -0
  289. package/src/server/auth-form-state.ts +14 -0
  290. package/src/server/auth-mail.ts +104 -0
  291. package/src/server/avatars.ts +83 -0
  292. package/src/server/board-latest-actions.ts +7 -0
  293. package/src/server/board-latest.tsx +103 -0
  294. package/src/server/board-offline.ts +23 -0
  295. package/src/server/board-url.ts +14 -0
  296. package/src/server/brand-assets.ts +151 -0
  297. package/src/server/brand-mark.tsx +110 -0
  298. package/src/server/branding-actions.ts +53 -0
  299. package/src/server/branding.ts +101 -0
  300. package/src/server/cache-targets.ts +13 -0
  301. package/src/server/code-highlighting.ts +33 -0
  302. package/src/server/container.ts +402 -0
  303. package/src/server/content-actions.ts +459 -0
  304. package/src/server/content-admin-actions.ts +492 -0
  305. package/src/server/content-admin.ts +95 -0
  306. package/src/server/content-security-policy.ts +35 -0
  307. package/src/server/context.ts +32 -0
  308. package/src/server/cookies.ts +139 -0
  309. package/src/server/credential-proof-actions.ts +42 -0
  310. package/src/server/credential-proof.ts +35 -0
  311. package/src/server/current-location.ts +19 -0
  312. package/src/server/demo-stripe.ts +104 -0
  313. package/src/server/demo-uploads.ts +28 -0
  314. package/src/server/demo.ts +80 -0
  315. package/src/server/discovery.ts +104 -0
  316. package/src/server/embeds.ts +113 -0
  317. package/src/server/error-notice.tsx +41 -0
  318. package/src/server/federation-actions.ts +113 -0
  319. package/src/server/federation.ts +138 -0
  320. package/src/server/feed-builder.ts +72 -0
  321. package/src/server/feed-routes.ts +122 -0
  322. package/src/server/fixture-actor-source.ts +62 -0
  323. package/src/server/fixture-forum-repo.ts +59 -0
  324. package/src/server/fixture-member-profile-repo.ts +30 -0
  325. package/src/server/fixture-post-repo.ts +108 -0
  326. package/src/server/fixture-thread-repo.ts +98 -0
  327. package/src/server/form-state-reporter.ts +34 -0
  328. package/src/server/form-values.ts +23 -0
  329. package/src/server/forum-admin-actions.ts +353 -0
  330. package/src/server/forum-admin.ts +98 -0
  331. package/src/server/group-admin-actions.ts +341 -0
  332. package/src/server/group-admin.ts +98 -0
  333. package/src/server/group-badge-actions.ts +61 -0
  334. package/src/server/group-badge.ts +62 -0
  335. package/src/server/group-identity.ts +104 -0
  336. package/src/server/i18n-catalogs.ts +42 -0
  337. package/src/server/i18n.ts +69 -0
  338. package/src/server/image-upload.ts +109 -0
  339. package/src/server/inline-moderation-actions.ts +129 -0
  340. package/src/server/install-actions.ts +124 -0
  341. package/src/server/install.ts +281 -0
  342. package/src/server/legacy-redirect.ts +83 -0
  343. package/src/server/legal.ts +43 -0
  344. package/src/server/location-header.ts +37 -0
  345. package/src/server/mail-brand.ts +43 -0
  346. package/src/server/mail-health.ts +41 -0
  347. package/src/server/mail-send.ts +44 -0
  348. package/src/server/mail-test-actions.ts +67 -0
  349. package/src/server/mail-test.ts +56 -0
  350. package/src/server/markdown-pipeline.ts +31 -0
  351. package/src/server/marketplace-actions.ts +47 -0
  352. package/src/server/marketplace-admin.ts +260 -0
  353. package/src/server/mention-actions.ts +21 -0
  354. package/src/server/mention-search.ts +35 -0
  355. package/src/server/message-actions.ts +110 -0
  356. package/src/server/messages.ts +141 -0
  357. package/src/server/modcp.ts +125 -0
  358. package/src/server/moderation-actions.ts +165 -0
  359. package/src/server/navigation-admin-actions.ts +217 -0
  360. package/src/server/navigation.ts +112 -0
  361. package/src/server/nonce.ts +13 -0
  362. package/src/server/notification-actions.ts +84 -0
  363. package/src/server/notification-audience.ts +10 -0
  364. package/src/server/notification-menu-actions.ts +82 -0
  365. package/src/server/notification-menu.ts +96 -0
  366. package/src/server/notifications.ts +93 -0
  367. package/src/server/onboarding-actions.ts +27 -0
  368. package/src/server/onboarding.ts +89 -0
  369. package/src/server/page-metadata.ts +14 -0
  370. package/src/server/passkey-challenge.ts +42 -0
  371. package/src/server/plugin-admin-actions.ts +232 -0
  372. package/src/server/plugin-admin.ts +276 -0
  373. package/src/server/plugin-host.ts +143 -0
  374. package/src/server/plugin-pages.ts +192 -0
  375. package/src/server/plugin-panel.ts +49 -0
  376. package/src/server/plugin-routes.ts +298 -0
  377. package/src/server/plugin-view.tsx +58 -0
  378. package/src/server/poll-actions.ts +104 -0
  379. package/src/server/poll-scope.ts +52 -0
  380. package/src/server/post-notifications.ts +71 -0
  381. package/src/server/post-scope.ts +86 -0
  382. package/src/server/presence.ts +177 -0
  383. package/src/server/profile-fields.ts +112 -0
  384. package/src/server/push-actions.ts +113 -0
  385. package/src/server/push.ts +40 -0
  386. package/src/server/redirect-back.ts +15 -0
  387. package/src/server/registration.ts +7 -0
  388. package/src/server/relation-actions.ts +89 -0
  389. package/src/server/relations.ts +45 -0
  390. package/src/server/reply-core.ts +173 -0
  391. package/src/server/report-actions.ts +165 -0
  392. package/src/server/report-scope.ts +21 -0
  393. package/src/server/reputation-actions.ts +146 -0
  394. package/src/server/reputation-target.ts +49 -0
  395. package/src/server/reputation.ts +70 -0
  396. package/src/server/request-fingerprint.ts +65 -0
  397. package/src/server/safe-path.ts +16 -0
  398. package/src/server/same-origin.ts +45 -0
  399. package/src/server/search-page.ts +313 -0
  400. package/src/server/search.ts +45 -0
  401. package/src/server/secret-auth.ts +20 -0
  402. package/src/server/see-other.ts +11 -0
  403. package/src/server/seed-board.ts +417 -0
  404. package/src/server/session-actions.ts +78 -0
  405. package/src/server/session-cookies.ts +124 -0
  406. package/src/server/session-key.ts +17 -0
  407. package/src/server/settings.ts +47 -0
  408. package/src/server/signatures.ts +62 -0
  409. package/src/server/sso-handshake.ts +79 -0
  410. package/src/server/stats.ts +63 -0
  411. package/src/server/subscription-actions.ts +161 -0
  412. package/src/server/surgery-actions.ts +180 -0
  413. package/src/server/syndication.ts +77 -0
  414. package/src/server/system-admin-actions.ts +134 -0
  415. package/src/server/system-admin.ts +90 -0
  416. package/src/server/test-container.ts +114 -0
  417. package/src/server/theme-admin-actions.ts +242 -0
  418. package/src/server/theme-admin.ts +203 -0
  419. package/src/server/theme-runtime.ts +89 -0
  420. package/src/server/theme-style.ts +279 -0
  421. package/src/server/theme.ts +52 -0
  422. package/src/server/thread-core.ts +207 -0
  423. package/src/server/thread-rating-actions.ts +55 -0
  424. package/src/server/thread-tool-actions.ts +154 -0
  425. package/src/server/two-factor-actions.ts +178 -0
  426. package/src/server/two-factor.ts +86 -0
  427. package/src/server/upgrade-notice.ts +41 -0
  428. package/src/server/user-admin-actions.ts +572 -0
  429. package/src/server/user-admin.ts +165 -0
  430. package/src/server/usercp-actions.ts +263 -0
  431. package/src/server/usercp-mail.ts +120 -0
  432. package/src/server/viewer-preferences.ts +64 -0
  433. package/src/server/warning-actions.ts +122 -0
  434. package/src/styles/globals.css +969 -0
  435. package/src/theme/contract.fixture.ts +793 -0
  436. package/src/view/account-copy.ts +351 -0
  437. package/src/view/admin-content-copy.ts +66 -0
  438. package/src/view/admin-copy.ts +106 -0
  439. package/src/view/admin-forum-tree-copy.ts +40 -0
  440. package/src/view/admin-group-copy.ts +80 -0
  441. package/src/view/admin-log.ts +58 -0
  442. package/src/view/admin-nav.ts +164 -0
  443. package/src/view/admin-navigation-copy.ts +58 -0
  444. package/src/view/admin-panel-copy.ts +194 -0
  445. package/src/view/admin-settings.ts +173 -0
  446. package/src/view/admin-theme-copy.ts +229 -0
  447. package/src/view/admin-user-copy.ts +96 -0
  448. package/src/view/attachments.ts +51 -0
  449. package/src/view/auth-copy.ts +147 -0
  450. package/src/view/board-index.ts +167 -0
  451. package/src/view/board-latest.ts +101 -0
  452. package/src/view/breadcrumb.ts +50 -0
  453. package/src/view/contacts.ts +85 -0
  454. package/src/view/content-copy.ts +118 -0
  455. package/src/view/contrast.ts +286 -0
  456. package/src/view/copy.ts +101 -0
  457. package/src/view/count.ts +8 -0
  458. package/src/view/discovery-view.ts +74 -0
  459. package/src/view/editor-toolbar.ts +84 -0
  460. package/src/view/error-notice.ts +52 -0
  461. package/src/view/feed.ts +130 -0
  462. package/src/view/forum-cursor.ts +54 -0
  463. package/src/view/forum-display.ts +152 -0
  464. package/src/view/forum-jump.ts +60 -0
  465. package/src/view/forum-notice.ts +24 -0
  466. package/src/view/inline-moderation.ts +87 -0
  467. package/src/view/install-copy.ts +95 -0
  468. package/src/view/legal.ts +58 -0
  469. package/src/view/locale.ts +40 -0
  470. package/src/view/marketplace-panel.ts +51 -0
  471. package/src/view/member-identity.ts +23 -0
  472. package/src/view/member-profile.ts +65 -0
  473. package/src/view/messages.ts +222 -0
  474. package/src/view/metadata.ts +75 -0
  475. package/src/view/modcp-nav.ts +76 -0
  476. package/src/view/moderation-copy.ts +84 -0
  477. package/src/view/moderation-queue.ts +67 -0
  478. package/src/view/navigation-arrange.ts +188 -0
  479. package/src/view/navigation.ts +173 -0
  480. package/src/view/notification-menu.ts +202 -0
  481. package/src/view/notifications.ts +84 -0
  482. package/src/view/onboarding.ts +26 -0
  483. package/src/view/pager.ts +191 -0
  484. package/src/view/panel-nav.ts +203 -0
  485. package/src/view/plugin-panel.ts +42 -0
  486. package/src/view/post-form.ts +61 -0
  487. package/src/view/post-link.ts +29 -0
  488. package/src/view/presence.ts +99 -0
  489. package/src/view/promotion-rules.ts +52 -0
  490. package/src/view/redirect-notice.ts +21 -0
  491. package/src/view/reputation.ts +107 -0
  492. package/src/view/search-controls.ts +103 -0
  493. package/src/view/search-results.ts +291 -0
  494. package/src/view/security-activity.ts +73 -0
  495. package/src/view/setting-groups.ts +43 -0
  496. package/src/view/shell.ts +164 -0
  497. package/src/view/slug-id.ts +12 -0
  498. package/src/view/sso-hand-off.ts +44 -0
  499. package/src/view/sso-notices.ts +48 -0
  500. package/src/view/subscriptions.ts +76 -0
  501. package/src/view/theme-draft.ts +167 -0
  502. package/src/view/theme-preference.ts +30 -0
  503. package/src/view/theme-tokens.ts +333 -0
  504. package/src/view/thread-view.ts +280 -0
  505. package/src/view/time.ts +31 -0
  506. package/src/view/timezone.ts +35 -0
  507. package/src/view/two-factor.ts +6 -0
  508. package/src/view/usercp-nav.ts +103 -0
  509. package/src/view/usercp.ts +196 -0
  510. package/src/view/warnings.ts +128 -0
  511. package/src/view/word-filter.ts +5 -0
@@ -0,0 +1,70 @@
1
+ import 'server-only'
2
+
3
+ import type { Actor } from '@meith/authorization'
4
+ import type { RaterLimits, ReputationSettings } from '@meith/reputation'
5
+ import { ReputationService } from '@meith/reputation'
6
+
7
+ import { getContainer } from './container'
8
+ import { getActor } from './context'
9
+ import { getSettings } from './settings'
10
+
11
+ export function reputationService(): ReputationService | null {
12
+ const { reputation } = getContainer()
13
+ return reputation === null ? null : new ReputationService({ reputation })
14
+ }
15
+
16
+ export async function reputationSettings(): Promise<ReputationSettings> {
17
+ const settings = await getSettings()
18
+ return {
19
+ enabled: settings.get('reputation.enabled'),
20
+ allowNegative: settings.get('reputation.allow_negative'),
21
+ commentRequired: settings.get('reputation.comment_required'),
22
+ minPostsToGive: settings.get('reputation.min_posts_to_give'),
23
+ }
24
+ }
25
+
26
+ export async function viewerRaterLimits(viewer?: Actor): Promise<RaterLimits> {
27
+ const actor = viewer ?? (await getActor())
28
+ const { authorizer, memberProfiles } = getContainer()
29
+
30
+ if (actor.userId === null) return { canGive: false, maxPerDay: 0, postCount: 0 }
31
+
32
+ const profile = await memberProfiles.findPublicById(actor.userId).catch(() => null)
33
+
34
+ return {
35
+ canGive: authorizer.can(actor, 'reputation.give'),
36
+ maxPerDay: authorizer.globalLimit(actor, 'maxReputationPerDay'),
37
+ postCount: profile?.postCount ?? 0,
38
+ }
39
+ }
40
+
41
+ export interface PostThanks {
42
+ readonly thanked: boolean
43
+ readonly count: number
44
+ }
45
+
46
+ export async function thanksForPosts(
47
+ postIds: readonly number[],
48
+ ): Promise<ReadonlyMap<number, PostThanks>> {
49
+ const service = reputationService()
50
+ if (service === null || postIds.length === 0) return new Map()
51
+
52
+ const actor = await getActor()
53
+
54
+ const [counts, mine] = await Promise.all([
55
+ service.thanksForPosts(postIds).catch(() => new Map<number, number>()),
56
+ actor.userId === null
57
+ ? Promise.resolve(new Map())
58
+ : service.existingForPosts({ givenByUserId: actor.userId, postIds }).catch(() => new Map()),
59
+ ])
60
+
61
+ return new Map(
62
+ postIds.map((postId) => [
63
+ postId,
64
+ {
65
+ thanked: (mine.get(postId)?.points ?? 0) > 0,
66
+ count: counts.get(postId) ?? 0,
67
+ },
68
+ ]),
69
+ )
70
+ }
@@ -0,0 +1,65 @@
1
+ import 'server-only'
2
+
3
+ import { headers } from 'next/headers'
4
+
5
+ import { env, logger, resolveClientAddress, truncateIp } from '@meith/core'
6
+ import { demoAddressToken, demoDiscardsAddresses } from '@meith/demo'
7
+
8
+ export interface RequestFingerprint {
9
+ readonly ipPrefix: string | null
10
+ readonly userAgent: string | null
11
+ }
12
+
13
+ let announcedUntrustedForwarding = false
14
+
15
+ export async function remoteAddress(): Promise<string | null> {
16
+ const jar = await headers()
17
+ const forwarded = jar.get('x-forwarded-for')
18
+
19
+ if (env.TRUSTED_PROXY_HOPS === 0 && forwarded !== null && !announcedUntrustedForwarding) {
20
+ announcedUntrustedForwarding = true
21
+ logger({ module: 'request' }).warn(
22
+ 'TRUSTED_PROXY_HOPS is 0 and a request arrived carrying X-Forwarded-For; ' +
23
+ 'the header is being ignored. Set it to the number of proxies in front ' +
24
+ 'of the board, or addresses will not resolve.',
25
+ )
26
+ }
27
+
28
+ return resolveClientAddress(
29
+ { forwardedFor: forwarded, realIp: jar.get('x-real-ip') },
30
+ env.TRUSTED_PROXY_HOPS,
31
+ )
32
+ }
33
+
34
+ export async function retainedIpPrefix(): Promise<string | null> {
35
+ if (demoDiscardsAddresses()) return null
36
+ return truncateIp(await remoteAddress()) ?? null
37
+ }
38
+
39
+ export async function countingPrefix(): Promise<string | null> {
40
+ const address = await remoteAddress()
41
+ if (address === null || address === '') return null
42
+
43
+ const prefix = truncateIp(address)
44
+ if (prefix === undefined) return null
45
+
46
+ return demoDiscardsAddresses() ? demoAddressToken(prefix) : prefix
47
+ }
48
+
49
+ export async function countingAddress(): Promise<string | null> {
50
+ const address = await remoteAddress()
51
+ if (address === null || address === '') return null
52
+
53
+ return demoDiscardsAddresses() ? demoAddressToken(address) : address
54
+ }
55
+
56
+ /** Where a request came from and what made it, as much as is kept about either. */
57
+ export async function requestFingerprint(): Promise<RequestFingerprint> {
58
+ const incoming = await headers()
59
+ const agent = incoming.get('user-agent') ?? ''
60
+
61
+ return {
62
+ ipPrefix: await retainedIpPrefix(),
63
+ userAgent: agent.trim() === '' ? null : agent.slice(0, 300),
64
+ }
65
+ }
@@ -0,0 +1,16 @@
1
+ import 'server-only'
2
+
3
+ export function isSafeLocalPath(path: string): boolean {
4
+ if (!path.startsWith('/') || path.startsWith('//')) return false
5
+
6
+ for (const character of path) {
7
+ const code = character.charCodeAt(0)
8
+ if (code <= 0x20 || code === 0x7f || character === '\\') return false
9
+ }
10
+
11
+ try {
12
+ return new URL(path, 'http://localhost').origin === 'http://localhost'
13
+ } catch {
14
+ return false
15
+ }
16
+ }
@@ -0,0 +1,45 @@
1
+ import 'server-only'
2
+
3
+ function boardHost(request: Request): string | null {
4
+ const host = request.headers.get('x-forwarded-host') ?? request.headers.get('host')
5
+ if (host === null) return null
6
+
7
+ const trimmed = host.trim().toLowerCase()
8
+ return trimmed === '' ? null : trimmed
9
+ }
10
+
11
+ export function isSameOrigin(request: Request): boolean {
12
+ const origin = request.headers.get('origin')?.trim()
13
+
14
+ if (origin !== undefined && origin !== '' && origin !== 'null') {
15
+ const host = boardHost(request)
16
+ if (host === null) return false
17
+
18
+ try {
19
+ return new URL(origin).host.toLowerCase() === host
20
+ } catch {
21
+ return false
22
+ }
23
+ }
24
+
25
+ const site = request.headers.get('sec-fetch-site')
26
+ return site === 'same-origin' || site === 'none'
27
+ }
28
+
29
+ export function isTopLevelNavigation(request: Request): boolean {
30
+ const mode = request.headers.get('sec-fetch-mode')
31
+ const dest = request.headers.get('sec-fetch-dest')
32
+
33
+ if (mode !== null || dest !== null) {
34
+ return (mode === null || mode === 'navigate') && (dest === null || dest === 'document')
35
+ }
36
+
37
+ return (request.headers.get('accept') ?? '').includes('text/html')
38
+ }
39
+
40
+ export function crossOriginRefusal(): Response {
41
+ return new Response('This request did not come from the board.', {
42
+ status: 403,
43
+ headers: { 'content-type': 'text/plain; charset=utf-8', 'cache-control': 'no-store' },
44
+ })
45
+ }
@@ -0,0 +1,313 @@
1
+ import { msg } from '@meith/i18n'
2
+ import 'server-only'
3
+
4
+ import { randomBytes } from 'node:crypto'
5
+
6
+ import { foldIdentifier } from '@meith/accounts'
7
+ import type { Actor } from '@meith/authorization'
8
+ import { ForbiddenError, NotFoundError } from '@meith/core'
9
+ import { getDb, ownsSearch, PostgresSearchStore, SEARCH_WINDOW, type StoredSearch } from '@meith/db'
10
+ import {
11
+ isRefined,
12
+ isRunnable,
13
+ narrowFilters,
14
+ parseSearchInput,
15
+ readGrouping,
16
+ readMatch,
17
+ readPeriod,
18
+ readSort,
19
+ type SearchCursor,
20
+ type SearchFilterSet,
21
+ type SearchRefinement,
22
+ type SearchResults,
23
+ type SearchSummary,
24
+ searchQueryFrom,
25
+ trimRefinement,
26
+ } from '@meith/search'
27
+
28
+ import { REFINE_FIELDS } from '@/view/search-controls'
29
+
30
+ import { limitMessage, spendLimit } from './antispam'
31
+ import { getContainer } from './container'
32
+ import { filterView, viewerRef } from './plugin-view'
33
+ import { requireSearch, requireSearchEnabled, searchMinWordLength, searchScopeFor } from './search'
34
+ import { getSettings } from './settings'
35
+
36
+ export const SEARCH_PAGE = 20
37
+
38
+ export const MAX_AUTHOR_NAMES = 5
39
+
40
+ export const SEARCH_COUNT_CAP = SEARCH_WINDOW
41
+
42
+ export function searchStore(): PostgresSearchStore | null {
43
+ return getContainer().dataSource === 'postgres' ? new PostgresSearchStore(getDb()) : null
44
+ }
45
+
46
+ function newToken(): string {
47
+ return randomBytes(18).toString('base64url')
48
+ }
49
+
50
+ export interface RunSearchInput {
51
+ readonly actor: Actor
52
+ readonly sessionKey: string | null
53
+ readonly terms: string
54
+ readonly filters: Omit<SearchFilterSet, 'authorUserIds' | 'authorNames'>
55
+ readonly authors: string
56
+ }
57
+
58
+ export type RunSearchOutcome =
59
+ | { readonly kind: 'ok'; readonly token: string }
60
+ | {
61
+ readonly kind: 'refused'
62
+ readonly reason: 'empty' | 'too-short' | 'too-long'
63
+ readonly minWordLength: number
64
+ }
65
+ | { readonly kind: 'flooded'; readonly seconds: number }
66
+ | { readonly kind: 'limited'; readonly message: string }
67
+ | { readonly kind: 'unknown-author'; readonly name: string }
68
+
69
+ export async function runSearch(input: RunSearchInput): Promise<RunSearchOutcome> {
70
+ await requireSearchEnabled()
71
+
72
+ const minWordLength = await searchMinWordLength()
73
+
74
+ const parsed = parseSearchInput(input.terms, minWordLength)
75
+ if (!isRunnable(parsed)) {
76
+ return { kind: 'refused', reason: parsed.refusal ?? 'empty', minWordLength }
77
+ }
78
+
79
+ const store = searchStore()
80
+ if (store === null) throw new ForbiddenError(msg('error.app.board-search-index'))
81
+
82
+ const authors = await resolveAuthors(parseAuthorNames(input.authors))
83
+ if (authors.kind === 'unknown') return { kind: 'unknown-author', name: authors.name }
84
+
85
+ const settings = await getSettings()
86
+ const { authorizer } = getContainer()
87
+
88
+ const floodSeconds = authorizer.can(input.actor, 'flood.bypass')
89
+ ? 0
90
+ : Number(settings.get('search.flood_seconds') ?? 0)
91
+
92
+ const limited = await spendLimit({ scope: 'search', actor: input.actor, settings })
93
+ if (limited !== null && !limited.allowed) {
94
+ return { kind: 'limited', message: limitMessage(limited) }
95
+ }
96
+
97
+ const filters: SearchFilterSet = {
98
+ ...input.filters,
99
+ ...(authors.ids.length === 0 ? {} : { authorUserIds: authors.ids, authorNames: authors.names }),
100
+ }
101
+
102
+ const terms = await filterView('search.query.before', parsed.terms, viewerRef(input.actor))
103
+
104
+ const stored = await store.create({
105
+ token: newToken(),
106
+ userId: input.actor.userId,
107
+ sessionKey: input.sessionKey,
108
+ terms,
109
+ filters: { ...filters },
110
+ floodSeconds,
111
+ })
112
+
113
+ if (stored === null) return { kind: 'flooded', seconds: floodSeconds }
114
+
115
+ return { kind: 'ok', token: stored.token }
116
+ }
117
+
118
+ export interface SearchPageView {
119
+ readonly search: StoredSearch
120
+ readonly results: SearchResults
121
+ readonly summary: SearchSummary
122
+ readonly filters: SearchFilterSet
123
+ readonly effective: SearchFilterSet
124
+ readonly refine: SearchRefinement
125
+ }
126
+
127
+ export async function openSearch(input: {
128
+ readonly actor: Actor
129
+ readonly sessionKey: string | null
130
+ readonly token: string
131
+ readonly after: SearchCursor | null
132
+ readonly offset?: number
133
+ readonly refine: SearchRefinement
134
+ readonly now: Date
135
+ }): Promise<SearchPageView> {
136
+ await requireSearchEnabled()
137
+
138
+ const store = searchStore()
139
+ if (store === null) throw new NotFoundError(msg('error.app.such-search'))
140
+
141
+ const search = await store.findByToken(input.token)
142
+ if (search === null) throw new NotFoundError(msg('error.app.such-search'))
143
+
144
+ if (!ownsSearch(search, { userId: input.actor.userId, sessionKey: input.sessionKey })) {
145
+ throw new NotFoundError(msg('error.app.such-search'))
146
+ }
147
+
148
+ const filters = readFilters(search.filters)
149
+ const refine = trimRefinement(input.refine, filters)
150
+ const effective = narrowFilters(filters, refine)
151
+ const scope = await searchScopeFor(input.actor)
152
+ const provider = requireSearch()
153
+
154
+ const [results, summary] = await Promise.all([
155
+ provider.search(
156
+ searchQueryFrom(search.terms, effective, {
157
+ limit: SEARCH_PAGE,
158
+ after: input.after,
159
+ ...(input.offset === undefined ? {} : { offset: input.offset }),
160
+ now: input.now,
161
+ }),
162
+ scope,
163
+ ),
164
+ provider.summarize(
165
+ searchQueryFrom(search.terms, facetFilters(filters, refine), {
166
+ limit: SEARCH_PAGE,
167
+ after: null,
168
+ now: input.now,
169
+ }),
170
+ scope,
171
+ ),
172
+ ])
173
+
174
+ if (!isRefined(refine) && search.hitCount !== summary.total) {
175
+ await store.recordHitCount(search.id, summary.total)
176
+ }
177
+
178
+ return {
179
+ search,
180
+ results: await filterHits(results, search.terms, input.actor),
181
+ summary,
182
+ filters,
183
+ effective,
184
+ refine,
185
+ }
186
+ }
187
+
188
+ /**
189
+ * A plugin may reorder or drop; a row it invents has no hit behind it and is
190
+ * dropped, because the hits arrived permission-filtered in SQL.
191
+ */
192
+ async function filterHits(
193
+ results: SearchResults,
194
+ terms: string,
195
+ actor: Actor,
196
+ ): Promise<SearchResults> {
197
+ const ordered = await filterView(
198
+ 'search.results',
199
+ results.hits.map((hit) => ({ postId: hit.postId, threadId: hit.threadId, rank: hit.rank })),
200
+ { ...viewerRef(actor), terms },
201
+ )
202
+
203
+ const byPostId = new Map(results.hits.map((hit) => [hit.postId, hit]))
204
+ const hits = ordered
205
+ .map((row) => byPostId.get(row.postId))
206
+ .filter((hit): hit is (typeof results.hits)[number] => hit !== undefined)
207
+
208
+ return { ...results, hits }
209
+ }
210
+
211
+ function facetFilters(filters: SearchFilterSet, refine: SearchRefinement): SearchFilterSet {
212
+ return narrowFilters(filters, {
213
+ ...refine,
214
+ forumIds: undefined,
215
+ authorUserIds: undefined,
216
+ })
217
+ }
218
+
219
+ export function readFilters(raw: Readonly<Record<string, unknown>>): SearchFilterSet {
220
+ const forumIds = ids(raw.forumIds)
221
+ const authorUserIds = ids(raw.authorUserIds)
222
+ const authorNames = names(raw.authorNames)
223
+
224
+ return {
225
+ sort: readSort(raw.sort),
226
+ match: readMatch(raw.match),
227
+ grouping: readGrouping(raw.grouping),
228
+ period: readPeriod(raw.period),
229
+ ...(forumIds === undefined ? {} : { forumIds }),
230
+ ...(authorUserIds === undefined ? {} : { authorUserIds }),
231
+ ...(authorNames === undefined ? {} : { authorNames }),
232
+ }
233
+ }
234
+
235
+ export function readRefinement(
236
+ params: Readonly<Record<string, string | string[] | undefined>>,
237
+ ): SearchRefinement {
238
+ const many = (key: string): readonly string[] => {
239
+ const raw = params[key]
240
+ if (raw === undefined) return []
241
+ return (Array.isArray(raw) ? raw : [raw]).filter((value) => value !== '')
242
+ }
243
+ const one = (key: string): string | undefined => many(key)[0]
244
+
245
+ const chosen = (key: string): readonly number[] | undefined => {
246
+ const values = many(key)
247
+ .map((value) => Number(value))
248
+ .filter((value) => Number.isSafeInteger(value) && value > 0)
249
+
250
+ return values.length === 0 ? undefined : values
251
+ }
252
+
253
+ const forumIds = chosen(REFINE_FIELDS.forum)
254
+ const authorUserIds = chosen(REFINE_FIELDS.author)
255
+
256
+ const sort = one(REFINE_FIELDS.sort)
257
+ const grouping = one(REFINE_FIELDS.grouping)
258
+ const match = one(REFINE_FIELDS.match)
259
+ const period = one(REFINE_FIELDS.period)
260
+
261
+ return {
262
+ ...(sort === undefined ? {} : { sort: readSort(sort) }),
263
+ ...(grouping === undefined ? {} : { grouping: readGrouping(grouping) }),
264
+ ...(match === undefined || readMatch(match) === 'everything'
265
+ ? {}
266
+ : { match: readMatch(match) }),
267
+ ...(period === undefined || readPeriod(period) === 'any' ? {} : { period: readPeriod(period) }),
268
+ ...(forumIds === undefined ? {} : { forumIds }),
269
+ ...(authorUserIds === undefined ? {} : { authorUserIds }),
270
+ }
271
+ }
272
+
273
+ export function parseAuthorNames(raw: string): readonly string[] {
274
+ return raw
275
+ .split(',')
276
+ .map((name) => name.trim())
277
+ .filter((name) => name !== '')
278
+ .slice(0, MAX_AUTHOR_NAMES)
279
+ }
280
+
281
+ async function resolveAuthors(
282
+ wanted: readonly string[],
283
+ ): Promise<
284
+ | { readonly kind: 'ok'; readonly ids: readonly number[]; readonly names: readonly string[] }
285
+ | { readonly kind: 'unknown'; readonly name: string }
286
+ > {
287
+ if (wanted.length === 0) return { kind: 'ok', ids: [], names: [] }
288
+
289
+ const { accountStore } = getContainer()
290
+ const ids: number[] = []
291
+ const found: string[] = []
292
+
293
+ for (const name of wanted) {
294
+ const account = await accountStore.accounts.findByUsernameLower(foldIdentifier(name))
295
+ if (account === null) return { kind: 'unknown', name }
296
+ ids.push(account.id)
297
+ found.push(account.username)
298
+ }
299
+
300
+ return { kind: 'ok', ids, names: found }
301
+ }
302
+
303
+ function ids(value: unknown): readonly number[] | undefined {
304
+ return Array.isArray(value) && value.every((entry) => Number.isSafeInteger(entry))
305
+ ? (value as number[])
306
+ : undefined
307
+ }
308
+
309
+ function names(value: unknown): readonly string[] | undefined {
310
+ return Array.isArray(value) && value.every((entry) => typeof entry === 'string')
311
+ ? (value as string[])
312
+ : undefined
313
+ }
@@ -0,0 +1,45 @@
1
+ import { msg } from '@meith/i18n'
2
+ import 'server-only'
3
+
4
+ import type { Actor } from '@meith/authorization'
5
+ import { contentScopeFrom, ForbiddenError } from '@meith/core'
6
+ import { getDb, PostgresSearchRepository } from '@meith/db'
7
+ import type { SearchScope } from '@meith/search'
8
+
9
+ import { getContainer } from './container'
10
+ import { getSettings } from './settings'
11
+
12
+ export async function searchEnabled(): Promise<boolean> {
13
+ return (await getSettings()).get('search.enabled') === true
14
+ }
15
+
16
+ export async function requireSearchEnabled(): Promise<void> {
17
+ if (!(await searchEnabled())) throw new ForbiddenError(msg('board.search.disabled'))
18
+ }
19
+
20
+ export async function searchMinWordLength(): Promise<number> {
21
+ return (await getSettings()).get('search.min_word_length')
22
+ }
23
+
24
+ export function searchProvider(): PostgresSearchRepository | null {
25
+ return getContainer().dataSource === 'postgres' ? new PostgresSearchRepository(getDb()) : null
26
+ }
27
+
28
+ export function requireSearch(): PostgresSearchRepository {
29
+ const provider = searchProvider()
30
+ if (provider === null) {
31
+ throw new ForbiddenError(msg('error.app.board-running-in-memory-sample-data-11'))
32
+ }
33
+ return provider
34
+ }
35
+
36
+ export async function searchScopeFor(actor: Actor): Promise<SearchScope> {
37
+ const { authorizer } = getContainer()
38
+
39
+ const staff = actor.global.isAdministrator === true || actor.global.isSuperModerator === true
40
+
41
+ return {
42
+ ...(await authorizer.threadAudience(actor)),
43
+ content: contentScopeFrom({ seesUnapproved: staff, seesDeleted: staff }),
44
+ }
45
+ }
@@ -0,0 +1,20 @@
1
+ import { timingSafeEqual } from 'node:crypto'
2
+
3
+ export function secretMatches(presented: string, expected: string): boolean {
4
+ const a = Buffer.from(presented, 'utf8')
5
+ const b = Buffer.from(expected, 'utf8')
6
+ if (a.length !== b.length) {
7
+ timingSafeEqual(b, b)
8
+ return false
9
+ }
10
+ return timingSafeEqual(a, b)
11
+ }
12
+
13
+ export function bearerSecret(request: Request, headerName: string): string {
14
+ const authorization = request.headers.get('authorization')
15
+ if (authorization !== null && /^Bearer\s+/i.test(authorization)) {
16
+ return authorization.replace(/^Bearer\s+/i, '')
17
+ }
18
+
19
+ return request.headers.get(headerName) ?? ''
20
+ }
@@ -0,0 +1,11 @@
1
+ import 'server-only'
2
+
3
+ import { isSafeLocalPath } from './safe-path'
4
+
5
+ export function seeOther(path: string): Response {
6
+ if (!isSafeLocalPath(path)) {
7
+ throw new Error(`seeOther expects a path on this board, got: ${path}`)
8
+ }
9
+
10
+ return new Response(null, { status: 303, headers: { location: path } })
11
+ }