@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,84 @@
1
+ 'use server'
2
+
3
+ import { redirect } from 'next/navigation'
4
+
5
+ import { ForbiddenError } from '@meith/core'
6
+ import { msg } from '@meith/i18n'
7
+ import type { NotificationService } from '@meith/notifications'
8
+
9
+ import type { FormState } from './auth-form-state'
10
+ import { getActor } from './context'
11
+ import { formStateReporter } from './form-state-reporter'
12
+ import { positiveInt } from './form-values'
13
+ import { tr } from './i18n'
14
+ import { audiencesForActor } from './notification-audience'
15
+ import { notificationService } from './notifications'
16
+ import { pushAvailability } from './push'
17
+
18
+ const toFormState = formStateReporter('notification-actions', 'unexpected error in notifications')
19
+
20
+ async function requireOwnCentre(): Promise<{
21
+ service: NotificationService
22
+ userId: number
23
+ }> {
24
+ const actor = await getActor()
25
+ if (actor.userId === null) throw new ForbiddenError(msg('error.app.must-logged'))
26
+
27
+ const service = notificationService()
28
+ if (service === null) {
29
+ throw new ForbiddenError(msg('error.app.board-running-in-memory-sample-data-16'))
30
+ }
31
+ return { service, userId: actor.userId }
32
+ }
33
+
34
+ export async function markNotificationReadAction(
35
+ _prev: FormState,
36
+ form: FormData,
37
+ ): Promise<FormState> {
38
+ const notificationId = positiveInt(form, 'notificationId')
39
+ if (notificationId === null) return { error: await tr('notice.app.notification-exist') }
40
+
41
+ try {
42
+ const { service, userId } = await requireOwnCentre()
43
+ await service.markRead(userId, notificationId)
44
+ } catch (err) {
45
+ return toFormState(err)
46
+ }
47
+
48
+ redirect('/notifications?read=one')
49
+ }
50
+
51
+ export async function markAllNotificationsReadAction(
52
+ _prev: FormState,
53
+ _form: FormData,
54
+ ): Promise<FormState> {
55
+ try {
56
+ const { service, userId } = await requireOwnCentre()
57
+ await service.markAllRead(userId)
58
+ } catch (err) {
59
+ return toFormState(err)
60
+ }
61
+
62
+ redirect('/notifications?read=all')
63
+ }
64
+
65
+ export async function saveNotificationPreferencesAction(
66
+ _prev: FormState,
67
+ form: FormData,
68
+ ): Promise<FormState> {
69
+ try {
70
+ const { service, userId } = await requireOwnCentre()
71
+ const email = form.getAll('email').filter((value): value is string => typeof value === 'string')
72
+ const push = form.getAll('push').filter((value): value is string => typeof value === 'string')
73
+ const offersPush = (await pushAvailability()).enabled
74
+
75
+ for (const audience of await audiencesForActor()) {
76
+ await service.savePreferences(userId, audience, email, 'email')
77
+ if (offersPush) await service.savePreferences(userId, audience, push, 'push')
78
+ }
79
+ } catch (err) {
80
+ return toFormState(err)
81
+ }
82
+
83
+ redirect('/notifications/preferences?saved=1')
84
+ }
@@ -0,0 +1,10 @@
1
+ import 'server-only'
2
+
3
+ import { getActor } from './context'
4
+
5
+ export type PreferenceAudience = 'member' | 'staff'
6
+
7
+ export async function audiencesForActor(): Promise<readonly PreferenceAudience[]> {
8
+ const actor = await getActor()
9
+ return actor.global.canAccessAdminCp === true ? ['member', 'staff'] : ['member']
10
+ }
@@ -0,0 +1,82 @@
1
+ 'use server'
2
+
3
+ import { revalidatePath } from 'next/cache'
4
+
5
+ import { ForbiddenError } from '@meith/core'
6
+ import { msg } from '@meith/i18n'
7
+
8
+ import type { FormState } from './auth-form-state'
9
+ import { EMPTY_STATE } from './auth-form-state'
10
+ import { getActor } from './context'
11
+ import { formStateReporter } from './form-state-reporter'
12
+ import { positiveInt } from './form-values'
13
+ import { requireMessaging } from './messages'
14
+ import { notificationService } from './notifications'
15
+
16
+ const toFormState = formStateReporter(
17
+ 'notification-menu-actions',
18
+ 'unexpected error in the notifications menu',
19
+ )
20
+
21
+ function refreshShell(): void {
22
+ revalidatePath('/', 'layout')
23
+ }
24
+
25
+ export async function markNotificationSeenAction(
26
+ _prev: FormState,
27
+ form: FormData,
28
+ ): Promise<FormState> {
29
+ const notificationId = positiveInt(form, 'notificationId')
30
+ if (notificationId === null) return EMPTY_STATE
31
+
32
+ try {
33
+ const actor = await getActor()
34
+ if (actor.userId === null) throw new ForbiddenError(msg('error.app.must-logged'))
35
+
36
+ const service = notificationService()
37
+ if (service !== null) await service.markRead(actor.userId, notificationId)
38
+ } catch (err) {
39
+ return toFormState(err)
40
+ }
41
+
42
+ refreshShell()
43
+ return EMPTY_STATE
44
+ }
45
+
46
+ export async function markAllNotificationsSeenAction(
47
+ _prev: FormState,
48
+ _form: FormData,
49
+ ): Promise<FormState> {
50
+ try {
51
+ const actor = await getActor()
52
+ if (actor.userId === null) throw new ForbiddenError(msg('error.app.must-logged'))
53
+
54
+ const service = notificationService()
55
+ if (service !== null) await service.markAllRead(actor.userId)
56
+ } catch (err) {
57
+ return toFormState(err)
58
+ }
59
+
60
+ refreshShell()
61
+ return EMPTY_STATE
62
+ }
63
+
64
+ export async function markMessagesSeenAction(_prev: FormState, form: FormData): Promise<FormState> {
65
+ const copyIds = form
66
+ .getAll('copyId')
67
+ .filter((value): value is string => typeof value === 'string')
68
+ .map((value) => Number(value))
69
+ .filter((value) => Number.isInteger(value) && value > 0)
70
+
71
+ if (copyIds.length === 0) return EMPTY_STATE
72
+
73
+ try {
74
+ const { service, userId } = await requireMessaging(await getActor())
75
+ await service.markRead(userId, copyIds)
76
+ } catch (err) {
77
+ return toFormState(err)
78
+ }
79
+
80
+ refreshShell()
81
+ return EMPTY_STATE
82
+ }
@@ -0,0 +1,96 @@
1
+ import 'server-only'
2
+
3
+ import type { Actor } from '@meith/authorization'
4
+ import type { MessageListRow } from '@meith/messages'
5
+ import { ModerationQueue, type QueueItem, type ReportRow, ReportService } from '@meith/moderation'
6
+ import type { NotificationView } from '@meith/notifications'
7
+
8
+ import {
9
+ buildNotificationMenuView,
10
+ type ModerationSummary,
11
+ type NotificationMenuModel,
12
+ } from '../view/notification-menu'
13
+ import { getContainer } from './container'
14
+ import { getTranslator } from './i18n'
15
+ import { messageService, unreadMessageCount } from './messages'
16
+ import { modCpCounts, resolveModCpAccess } from './modcp'
17
+ import { notificationService, unreadNotificationCount } from './notifications'
18
+ import { hasReportScope, resolveReportScope } from './report-scope'
19
+
20
+ async function moderationSummary(): Promise<ModerationSummary | null> {
21
+ const access = await resolveModCpAccess()
22
+ if (access === null) return null
23
+
24
+ const { moderationQueue, reports } = getContainer()
25
+
26
+ const counts = await modCpCounts()
27
+
28
+ const queue: readonly QueueItem[] =
29
+ moderationQueue === null
30
+ ? []
31
+ : await new ModerationQueue({ queue: moderationQueue })
32
+ .list(access.forumIds, { offset: 0 })
33
+ .then((page) => page.items)
34
+ .catch(() => [])
35
+
36
+ const scope = await resolveReportScope()
37
+ const reportRows: readonly ReportRow[] =
38
+ reports === null || !hasReportScope(scope)
39
+ ? []
40
+ : await new ReportService({ reports })
41
+ .listOpen(scope, { offset: 0 })
42
+ .then((page) => page.rows)
43
+ .catch(() => [])
44
+
45
+ return {
46
+ queue,
47
+ reports: reportRows,
48
+ pending: counts.pending,
49
+ openReports: counts.openReports,
50
+ }
51
+ }
52
+
53
+ export async function buildNotificationMenuModel(
54
+ actor: Actor,
55
+ ): Promise<NotificationMenuModel | null> {
56
+ if (actor.userId === null) return null
57
+
58
+ const { authorizer } = getContainer()
59
+ const translator = await getTranslator()
60
+ const now = new Date()
61
+
62
+ const notifications = notificationService()
63
+ const notificationRows: readonly NotificationView[] =
64
+ notifications === null
65
+ ? []
66
+ : await notifications
67
+ .list(actor.userId, {}, translator)
68
+ .then((page) => page.rows)
69
+ .catch(() => [])
70
+
71
+ const canPm = authorizer.can(actor, 'pm.use')
72
+ const messages = messageService()
73
+ const messageRows: readonly MessageListRow[] =
74
+ !canPm || messages === null
75
+ ? []
76
+ : await messages
77
+ .list({ userId: actor.userId, folder: 'inbox', offset: 0 })
78
+ .then((page) => page.rows)
79
+ .catch(() => [])
80
+
81
+ const [notificationsUnread, messagesUnread, moderation] = await Promise.all([
82
+ unreadNotificationCount(actor.userId),
83
+ canPm ? unreadMessageCount(actor.userId) : Promise.resolve(0),
84
+ moderationSummary(),
85
+ ])
86
+
87
+ return buildNotificationMenuView({
88
+ notifications: notificationRows,
89
+ notificationsUnread,
90
+ messages: messageRows,
91
+ messagesUnread,
92
+ moderation,
93
+ now,
94
+ t: translator,
95
+ })
96
+ }
@@ -0,0 +1,93 @@
1
+ import 'server-only'
2
+
3
+ import forumConfig from '@board/config'
4
+ import { cache } from 'react'
5
+
6
+ import { currentRequestId } from '@meith/core/logger'
7
+ import type { ReportNotifierPort, WarningNotifierPort } from '@meith/moderation'
8
+ import { type NotificationAudit, NotificationService } from '@meith/notifications'
9
+ import {
10
+ type PluginDefinition,
11
+ type PluginNotificationKindSpec,
12
+ pluginNotificationKindSpecs,
13
+ } from '@meith/plugin-kit'
14
+
15
+ import { getContainer } from './container'
16
+ import { emitEvent, filterView } from './plugin-view'
17
+
18
+ const PLUGIN_KINDS: readonly PluginNotificationKindSpec[] = (forumConfig.plugins ?? []).flatMap(
19
+ (entry) => {
20
+ if (entry.enabled === false) return []
21
+ const definition = entry.plugin as PluginDefinition | undefined
22
+ if (definition === undefined) return []
23
+ return pluginNotificationKindSpecs(definition.key, definition.notifications ?? [])
24
+ },
25
+ )
26
+
27
+ const PLUGIN_AUDIT: NotificationAudit = {
28
+ before: (input) =>
29
+ filterView('notification.create.before', input, { requestId: currentRequestId() ?? null }),
30
+
31
+ created: (input) =>
32
+ emitEvent('notification.created', input, { requestId: currentRequestId() ?? null }),
33
+ }
34
+
35
+ export function notificationService(): NotificationService | null {
36
+ const { notifications } = getContainer()
37
+ return notifications === null
38
+ ? null
39
+ : new NotificationService({ notifications, extraKinds: PLUGIN_KINDS, audit: PLUGIN_AUDIT })
40
+ }
41
+
42
+ export function warningNotifier(): WarningNotifierPort | null {
43
+ const service = notificationService()
44
+ if (service === null) return null
45
+
46
+ return {
47
+ async warned(input) {
48
+ await service.raise({
49
+ userId: input.userId,
50
+ kind: 'warning.received',
51
+ data: {
52
+ title: input.title,
53
+ points: input.points,
54
+ totalPoints: input.totalPoints,
55
+ reason: input.reason,
56
+ restriction: input.restriction,
57
+ },
58
+ href: null,
59
+ })
60
+ },
61
+ }
62
+ }
63
+
64
+ export function reportNotifier(): ReportNotifierPort | null {
65
+ const service = notificationService()
66
+ if (service === null) return null
67
+
68
+ return {
69
+ async reportClosed(input) {
70
+ await service.raise({
71
+ userId: input.reporterUserId,
72
+ kind: 'report.actioned',
73
+ data: { outcome: input.outcome, targetLabel: input.targetLabel },
74
+ dedupeKey: `report.actioned:${input.reportId}`,
75
+ href: null,
76
+ })
77
+ },
78
+ }
79
+ }
80
+
81
+ export const unreadNotificationCount = cache(async function unreadNotificationCount(
82
+ userId: number | null,
83
+ ): Promise<number> {
84
+ if (userId === null) return 0
85
+ const service = notificationService()
86
+ if (service === null) return 0
87
+
88
+ try {
89
+ return await service.unreadCount(userId)
90
+ } catch {
91
+ return 0
92
+ }
93
+ })
@@ -0,0 +1,27 @@
1
+ 'use server'
2
+
3
+ import { cookies } from 'next/headers'
4
+
5
+ import {
6
+ isOnboardingPanel,
7
+ ONBOARDING_COOKIE_MAX_AGE,
8
+ onboardingCookieName,
9
+ } from '@/view/onboarding'
10
+
11
+ import { redirectToCurrentPath } from './redirect-back'
12
+
13
+ const COOKIE_OPTIONS = {
14
+ path: '/',
15
+ maxAge: ONBOARDING_COOKIE_MAX_AGE,
16
+ sameSite: 'lax',
17
+ httpOnly: false,
18
+ } as const
19
+
20
+ export async function dismissOnboardingAction(form: FormData): Promise<void> {
21
+ const panel = form.get('panel')
22
+ if (typeof panel === 'string' && isOnboardingPanel(panel)) {
23
+ ;(await cookies()).set(onboardingCookieName(panel), '1', COOKIE_OPTIONS)
24
+ }
25
+
26
+ await redirectToCurrentPath()
27
+ }
@@ -0,0 +1,89 @@
1
+ import 'server-only'
2
+
3
+ import { cookies } from 'next/headers'
4
+
5
+ import type { Actor } from '@meith/authorization'
6
+
7
+ import {
8
+ hasCompleteProfile,
9
+ isNewMember,
10
+ type OnboardingPanel,
11
+ onboardingCookieName,
12
+ } from '@/view/onboarding'
13
+
14
+ import { avatarsFor } from './avatars'
15
+ import { getContainer } from './container'
16
+ import { getTranslator } from './i18n'
17
+ import { legalDocument } from './legal'
18
+
19
+ export interface OnboardingNoticeModel {
20
+ readonly panel: OnboardingPanel
21
+ readonly message: string
22
+ readonly ctaHref: string | null
23
+ readonly ctaLabel: string | null
24
+ readonly dismissLabel: string
25
+ }
26
+
27
+ async function dismissed(panel: OnboardingPanel): Promise<boolean> {
28
+ return (await cookies()).get(onboardingCookieName(panel))?.value === '1'
29
+ }
30
+
31
+ /**
32
+ * At most one of the welcome banner or the profile-completion nudge, never
33
+ * both — stacking unrelated notices in the same board-wide slot is the
34
+ * "heavy multi-step tour" the feature is meant to avoid.
35
+ */
36
+ export async function onboardingBanner(actor: Actor): Promise<OnboardingNoticeModel | null> {
37
+ if (actor.userId === null) return null
38
+
39
+ const profile = await getContainer()
40
+ .memberProfiles.findPublicById(actor.userId)
41
+ .catch(() => null)
42
+ if (profile === null || !isNewMember(profile.postCount)) return null
43
+
44
+ const t = await getTranslator()
45
+ const dismissLabel = t.t('onboarding.dismiss')
46
+
47
+ if (!(await dismissed('welcome'))) {
48
+ return {
49
+ panel: 'welcome',
50
+ message: t.t('onboarding.welcome.message'),
51
+ ctaHref: null,
52
+ ctaLabel: null,
53
+ dismissLabel,
54
+ }
55
+ }
56
+
57
+ const avatarUrl = (await avatarsFor([actor.userId])).get(actor.userId) ?? null
58
+ if (!hasCompleteProfile({ avatarUrl, bio: profile.bio }) && !(await dismissed('profile'))) {
59
+ return {
60
+ panel: 'profile',
61
+ message: t.t('onboarding.profile.message'),
62
+ ctaHref: '/usercp/profile',
63
+ ctaLabel: t.t('onboarding.profile.cta'),
64
+ dismissLabel,
65
+ }
66
+ }
67
+
68
+ return null
69
+ }
70
+
71
+ export async function firstPostGuidance(actor: Actor): Promise<OnboardingNoticeModel | null> {
72
+ if (actor.userId === null || (await dismissed('first-post'))) return null
73
+
74
+ const profile = await getContainer()
75
+ .memberProfiles.findPublicById(actor.userId)
76
+ .catch(() => null)
77
+ if (profile === null || !isNewMember(profile.postCount)) return null
78
+
79
+ const rules = await legalDocument('rules')
80
+ const t = await getTranslator()
81
+
82
+ return {
83
+ panel: 'first-post',
84
+ message: t.t('onboarding.firstPost.message'),
85
+ ctaHref: rules?.href ?? null,
86
+ ctaLabel: rules === null ? null : t.t('onboarding.firstPost.cta'),
87
+ dismissLabel: t.t('onboarding.dismiss'),
88
+ }
89
+ }
@@ -0,0 +1,14 @@
1
+ import 'server-only'
2
+
3
+ import { filterView } from './plugin-view'
4
+
5
+ export interface PageMetadata {
6
+ readonly title: string
7
+ readonly description: string | null
8
+ readonly canonical: string
9
+ readonly imageUrl: string | null
10
+ }
11
+
12
+ export async function pageMetadata(route: string, page: PageMetadata): Promise<PageMetadata> {
13
+ return filterView('metadata.page', page, { route })
14
+ }
@@ -0,0 +1,42 @@
1
+ import 'server-only'
2
+
3
+ export type PasskeyPurpose = 'register' | 'authenticate' | 'second-factor' | 'credential-proof'
4
+
5
+ export interface PasskeyChallenge {
6
+ readonly challenge: string
7
+ readonly userId?: number
8
+ readonly sessionId?: number
9
+ readonly provedAt?: number
10
+ }
11
+
12
+ export function packChallenge(
13
+ purpose: PasskeyPurpose,
14
+ challenge: string,
15
+ binding: Omit<PasskeyChallenge, 'challenge'> = {},
16
+ ): string {
17
+ return `${purpose}:${encodeURIComponent(JSON.stringify({ challenge, ...binding }))}`
18
+ }
19
+
20
+ export function unpackChallenge(
21
+ raw: string | undefined,
22
+ purpose: PasskeyPurpose,
23
+ ): PasskeyChallenge | null {
24
+ if (raw === undefined) return null
25
+
26
+ const separator = raw.indexOf(':')
27
+ if (separator < 0 || raw.slice(0, separator) !== purpose) return null
28
+
29
+ try {
30
+ const value = JSON.parse(decodeURIComponent(raw.slice(separator + 1))) as Record<
31
+ string,
32
+ unknown
33
+ >
34
+ if (typeof value.challenge !== 'string' || value.challenge === '') return null
35
+ if (value.userId !== undefined && !Number.isInteger(value.userId)) return null
36
+ if (value.sessionId !== undefined && !Number.isInteger(value.sessionId)) return null
37
+ if (value.provedAt !== undefined && !Number.isInteger(value.provedAt)) return null
38
+ return value as unknown as PasskeyChallenge
39
+ } catch {
40
+ return null
41
+ }
42
+ }