@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,186 @@
1
+ import type { Metadata } from 'next'
2
+
3
+ import { PruneForm, SelectedPruneForm } from '@/components/admin/user-forms'
4
+ import { PANEL_CARD, PANEL_NOTE } from '@/components/shell/panel-list'
5
+ import { PanelPage } from '@/components/shell/panel-page'
6
+ import { adminPageContext } from '@/server/admin'
7
+ import { readAdminUndo } from '@/server/admin-undo'
8
+ import { getTranslator, tr } from '@/server/i18n'
9
+ import { parsePruneCriteria, userBulkRepository } from '@/server/user-admin'
10
+ import { userPruneCopy } from '@/view/admin-user-copy'
11
+
12
+ export async function generateMetadata(): Promise<Metadata> {
13
+ return { title: await tr('page.prune-members') }
14
+ }
15
+
16
+ const INPUT =
17
+ 'w-full rounded-md border border-border bg-background px-3 py-2 text-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring'
18
+
19
+ export default async function AdminPrunePage({
20
+ searchParams,
21
+ }: {
22
+ searchParams: Promise<Record<string, string | string[] | undefined>>
23
+ }) {
24
+ const context = await adminPageContext()
25
+ if (context === null) return null
26
+
27
+ const repository = userBulkRepository()
28
+ if (repository === null) {
29
+ return (
30
+ <PanelPage title={await tr('page.prune-members')}>
31
+ <p className="mt-2 text-sm text-muted-foreground">
32
+ {await tr('page.this-board-running-in-memory-sample-2')}
33
+ </p>
34
+ </PanelPage>
35
+ )
36
+ }
37
+
38
+ const params = await searchParams
39
+ const selection = Array.isArray(params.selection) ? params.selection[0] : params.selection
40
+ const selectedOperation =
41
+ selection === undefined
42
+ ? null
43
+ : await readAdminUndo({ token: selection, actorUserId: context.session.userId })
44
+ const selectedIds =
45
+ selectedOperation?.operation === 'user.prune_selection' &&
46
+ Array.isArray(selectedOperation.snapshot.userIds)
47
+ ? selectedOperation.snapshot.userIds.map(Number).filter(Number.isSafeInteger)
48
+ : []
49
+ const selectedPreview =
50
+ selectedIds.length === 0 ? null : await repository.selectedPrunePreview(selectedIds)
51
+ const criteria = selection === undefined ? parsePruneCriteria(params) : null
52
+ const preview = criteria === null ? null : await repository.prunePreview(criteria)
53
+ const translator = await getTranslator()
54
+
55
+ const value = (key: string): string => {
56
+ const raw = params[key]
57
+ const text = Array.isArray(raw) ? raw[0] : raw
58
+ return text ?? ''
59
+ }
60
+
61
+ return (
62
+ <PanelPage
63
+ back={{ href: '/admin/users', label: translator.t('adminUsers.allMembers') }}
64
+ title={await tr('page.prune-members')}
65
+ lede={translator.t('adminUsers.pruneLede')}
66
+ >
67
+ {selection !== undefined && selectedPreview !== null ? (
68
+ <section className={PANEL_CARD}>
69
+ <h2 className="font-heading text-lg font-semibold">
70
+ {translator.t('adminUsers.accountsWouldClose', { count: selectedPreview.total })}
71
+ </h2>
72
+ <ul className="flex flex-col gap-1 text-sm text-muted-foreground">
73
+ {selectedPreview.sample.map((row) => (
74
+ <li key={row.id}>
75
+ <a
76
+ href={`/admin/users/${row.id}`}
77
+ className="font-medium text-foreground underline decoration-border underline-offset-2 hover:decoration-foreground"
78
+ >
79
+ {row.username}
80
+ </a>{' '}
81
+ {translator.t('adminUsers.pruneMemberMeta', {
82
+ email: row.email,
83
+ date: row.createdAt.toISOString().slice(0, 10),
84
+ })}
85
+ </li>
86
+ ))}
87
+ </ul>
88
+ <SelectedPruneForm
89
+ selection={selection}
90
+ copy={userPruneCopy(selectedPreview.total, translator)}
91
+ />
92
+ </section>
93
+ ) : selection !== undefined ? (
94
+ <p className={PANEL_NOTE}>{translator.t('admin.undoExpired')}</p>
95
+ ) : (
96
+ <>
97
+ <form method="get" className={PANEL_CARD}>
98
+ <label className="flex flex-col gap-1 text-sm">
99
+ <span className="font-medium">{await tr('page.registered-before')}</span>
100
+ <input type="date" name="before" defaultValue={value('before')} className={INPUT} />
101
+ <span className="text-xs text-muted-foreground">
102
+ {await tr('page.required-without-it-prune-matches')}
103
+ </span>
104
+ </label>
105
+
106
+ <label className="flex flex-col gap-1 text-sm">
107
+ <span className="font-medium">{await tr('page.not-seen-since')}</span>
108
+ <input
109
+ type="date"
110
+ name="inactive"
111
+ defaultValue={value('inactive')}
112
+ className={INPUT}
113
+ />
114
+ <span className="text-xs text-muted-foreground">
115
+ {await tr('page.members-who-have-never-been')}
116
+ </span>
117
+ </label>
118
+
119
+ <label className="flex items-center gap-2 text-sm">
120
+ <input
121
+ type="checkbox"
122
+ name="awaiting"
123
+ value="1"
124
+ defaultChecked={value('awaiting') !== ''}
125
+ className="size-4"
126
+ />
127
+ <span>{await tr('page.only-accounts-still-awaiting-activation')}</span>
128
+ </label>
129
+
130
+ <div>
131
+ <button
132
+ type="submit"
133
+ className="inline-flex h-10 items-center justify-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground hover:opacity-90"
134
+ >
135
+ {await tr('page.show-me')}
136
+ </button>
137
+ </div>
138
+ </form>
139
+
140
+ {criteria === null ? (
141
+ <p className={PANEL_NOTE}>{await tr('page.choose-registration-date-see-what')}</p>
142
+ ) : preview !== null && preview.total === 0 ? (
143
+ <p className={PANEL_NOTE}>{translator.t('adminUsers.pruneNoMatches')}</p>
144
+ ) : preview !== null ? (
145
+ <section className={PANEL_CARD}>
146
+ <h2 className="font-heading text-lg font-semibold">
147
+ {translator.t('adminUsers.accountsWouldClose', { count: preview.total })}
148
+ </h2>
149
+
150
+ <ul className="flex flex-col gap-1 text-sm text-muted-foreground">
151
+ {preview.sample.map((row) => (
152
+ <li key={row.id}>
153
+ <a
154
+ href={`/admin/users/${row.id}`}
155
+ className="font-medium text-foreground underline decoration-border underline-offset-2 hover:decoration-foreground"
156
+ >
157
+ {row.username}
158
+ </a>{' '}
159
+ {translator.t('adminUsers.pruneMemberMeta', {
160
+ email: row.email,
161
+ date: row.createdAt.toISOString().slice(0, 10),
162
+ })}
163
+ </li>
164
+ ))}
165
+ {preview.total > preview.sample.length && (
166
+ <li>
167
+ {translator.t('adminUsers.andMore', {
168
+ count: preview.total - preview.sample.length,
169
+ })}
170
+ </li>
171
+ )}
172
+ </ul>
173
+
174
+ <PruneForm
175
+ before={value('before')}
176
+ inactive={value('inactive')}
177
+ awaiting={value('awaiting') !== ''}
178
+ copy={userPruneCopy(preview.total, translator)}
179
+ />
180
+ </section>
181
+ ) : null}
182
+ </>
183
+ )}
184
+ </PanelPage>
185
+ )
186
+ }
@@ -0,0 +1,11 @@
1
+ import { NextResponse } from 'next/server'
2
+
3
+ export const dynamic = 'force-dynamic'
4
+ export const revalidate = 0
5
+
6
+ export function GET(): NextResponse {
7
+ return NextResponse.json(
8
+ { ok: true, at: new Date().toISOString() },
9
+ { headers: { 'cache-control': 'no-store' } },
10
+ )
11
+ }
@@ -0,0 +1,66 @@
1
+ import { NextResponse } from 'next/server'
2
+
3
+ import { env, logger, metrics } from '@meith/core'
4
+
5
+ import { bearerSecret, secretMatches } from '@/server/secret-auth'
6
+ import { systemHealthRepository } from '@/server/system-admin'
7
+
8
+ export const dynamic = 'force-dynamic'
9
+ export const revalidate = 0
10
+
11
+ const TOKEN_HEADER = 'x-metrics-token'
12
+
13
+ const CONTENT_TYPE = 'text/plain; version=0.0.4; charset=utf-8'
14
+
15
+ const queueJobs = metrics.gauge(
16
+ 'meith_queue_jobs',
17
+ 'Jobs currently queued or dead-lettered, labelled by status.',
18
+ )
19
+ const dbConnectionsActive = metrics.gauge(
20
+ 'meith_db_connections_active',
21
+ 'Active server-side connections to this database.',
22
+ )
23
+
24
+ function authorised(request: Request): boolean {
25
+ const expected = env.METRICS_TOKEN
26
+
27
+ if (!expected) {
28
+ logger().warn(
29
+ 'METRICS_TOKEN is not set; the metrics endpoint is unauthenticated. This is ' +
30
+ 'permitted in development only.',
31
+ )
32
+ return true
33
+ }
34
+
35
+ return secretMatches(bearerSecret(request, TOKEN_HEADER), expected)
36
+ }
37
+
38
+ async function refreshDatabaseGauges(): Promise<void> {
39
+ const repository = systemHealthRepository()
40
+ if (repository === null) return
41
+
42
+ const [volumes, activeConnections] = await Promise.all([
43
+ repository.volumes(),
44
+ repository.activeConnections(),
45
+ ])
46
+
47
+ queueJobs.set(volumes.queuedJobs, { status: 'queued' })
48
+ queueJobs.set(volumes.deadLetteredJobs, { status: 'dead' })
49
+ dbConnectionsActive.set(activeConnections)
50
+ }
51
+
52
+ export async function GET(request: Request): Promise<NextResponse | Response> {
53
+ if (!env.METRICS_ENABLED || !authorised(request)) {
54
+ return NextResponse.json({ error: 'Not found' }, { status: 404 })
55
+ }
56
+
57
+ try {
58
+ await refreshDatabaseGauges()
59
+ } catch (err) {
60
+ logger({ module: 'metrics' }).error({ err }, 'could not refresh the database-derived gauges')
61
+ }
62
+
63
+ return new Response(metrics.render(), {
64
+ headers: { 'content-type': CONTENT_TYPE, 'cache-control': 'no-store' },
65
+ })
66
+ }
@@ -0,0 +1,27 @@
1
+ import type { NextRequest } from 'next/server'
2
+
3
+ import { dispatchPluginRoute } from '@/server/plugin-routes'
4
+
5
+ export const dynamic = 'force-dynamic'
6
+
7
+ async function handle(
8
+ request: NextRequest,
9
+ params: Promise<{ key: string; path: string[] }>,
10
+ ): Promise<Response> {
11
+ const { key, path } = await params
12
+ return dispatchPluginRoute(request, key, path)
13
+ }
14
+
15
+ export async function GET(
16
+ request: NextRequest,
17
+ context: { params: Promise<{ key: string; path: string[] }> },
18
+ ): Promise<Response> {
19
+ return handle(request, context.params)
20
+ }
21
+
22
+ export async function POST(
23
+ request: NextRequest,
24
+ context: { params: Promise<{ key: string; path: string[] }> },
25
+ ): Promise<Response> {
26
+ return handle(request, context.params)
27
+ }
@@ -0,0 +1,15 @@
1
+ import { getContainer } from '@/server/container'
2
+ import { getActor } from '@/server/context'
3
+ import { crossOriginRefusal, isSameOrigin } from '@/server/same-origin'
4
+ import { seeOther } from '@/server/see-other'
5
+
6
+ export async function POST(request: Request) {
7
+ if (!isSameOrigin(request)) return crossOriginRefusal()
8
+
9
+ const actor = await getActor()
10
+ const { authorizer, readState } = getContainer()
11
+ if (actor.userId === null || readState === null) return seeOther('/')
12
+
13
+ await readState.markForumsRead(actor.userId, await authorizer.visibleForumIds(actor), new Date())
14
+ return seeOther('/')
15
+ }
@@ -0,0 +1,29 @@
1
+ import { canHoldThreads } from '@meith/forums'
2
+
3
+ import { getContainer } from '@/server/container'
4
+ import { getActor } from '@/server/context'
5
+ import { crossOriginRefusal, isSameOrigin } from '@/server/same-origin'
6
+ import { seeOther } from '@/server/see-other'
7
+
8
+ function idFrom(value: string): number | null {
9
+ if (!/^[1-9]\d*$/.test(value)) return null
10
+ const id = Number(value)
11
+ return Number.isSafeInteger(id) ? id : null
12
+ }
13
+
14
+ export async function POST(request: Request, { params }: { params: Promise<{ id: string }> }) {
15
+ if (!isSameOrigin(request)) return crossOriginRefusal()
16
+
17
+ const id = idFrom((await params).id)
18
+ const actor = await getActor()
19
+ const { forums, authorizer, readState } = getContainer()
20
+ if (id === null || actor.userId === null || readState === null) return seeOther('/')
21
+
22
+ const forum = await forums.findById(id)
23
+ if (!forum || !canHoldThreads(forum.type)) return seeOther('/')
24
+ const matrix = await authorizer.forumMatrix(actor, id)
25
+ if (!authorizer.can(actor, 'forum.view', { forumId: id, forum: matrix })) return seeOther('/')
26
+
27
+ await readState.markForumsRead(actor.userId, [id], new Date())
28
+ return seeOther(`/${id}-${forum.slug}`)
29
+ }
@@ -0,0 +1,40 @@
1
+ import { ALL_THREAD_AUTHORS, PUBLIC_CONTENT } from '@meith/core'
2
+ import { canHoldThreads } from '@meith/forums'
3
+
4
+ import { getContainer } from '@/server/container'
5
+ import { getActor } from '@/server/context'
6
+ import { crossOriginRefusal, isSameOrigin } from '@/server/same-origin'
7
+ import { seeOther } from '@/server/see-other'
8
+
9
+ function idFrom(value: string | null): number | null {
10
+ if (value === null || !/^[1-9]\d*$/.test(value)) return null
11
+ const id = Number(value)
12
+ return Number.isSafeInteger(id) ? id : null
13
+ }
14
+
15
+ export async function POST(request: Request, { params }: { params: Promise<{ id: string }> }) {
16
+ if (!isSameOrigin(request)) return crossOriginRefusal()
17
+
18
+ const url = new URL(request.url)
19
+ const threadId = idFrom((await params).id)
20
+ const postId = idFrom(url.searchParams.get('post'))
21
+ const actor = await getActor()
22
+ const { forums, posts, threads, authorizer, readState } = getContainer()
23
+ if (threadId === null || postId === null || actor.userId === null || readState === null)
24
+ return seeOther('/')
25
+
26
+ const thread = await threads.findById(threadId, PUBLIC_CONTENT, ALL_THREAD_AUTHORS)
27
+ if (!thread) return seeOther('/')
28
+ const forum = await forums.findById(thread.forumId)
29
+ if (!forum || !canHoldThreads(forum.type)) return seeOther('/')
30
+ const matrix = await authorizer.forumMatrix(actor, forum.id)
31
+ const target = {
32
+ ...(await authorizer.moderatorTargetIn(actor, forum.id, matrix)),
33
+ threadAuthorId: thread.authorUserId,
34
+ }
35
+ if (!authorizer.can(actor, 'thread.view', target)) return seeOther('/')
36
+ if ((await posts.findVisibleById(threadId, postId)) === null) return seeOther('/')
37
+
38
+ await readState.markThreadRead(actor.userId, threadId, postId, new Date())
39
+ return seeOther(`/thread/${threadId}-${thread.slug}`)
40
+ }
@@ -0,0 +1,63 @@
1
+ import { NextResponse } from 'next/server'
2
+
3
+ import { logger } from '@meith/core'
4
+ import { assessScheduler } from '@meith/tasks'
5
+
6
+ import { systemHealthRepository } from '@/server/system-admin'
7
+
8
+ export const dynamic = 'force-dynamic'
9
+ export const revalidate = 0
10
+
11
+ interface ReadyBody {
12
+ readonly ok: boolean
13
+ readonly at: string
14
+ readonly database: { readonly ok: boolean; readonly error?: string }
15
+ readonly scheduler: {
16
+ readonly ok: boolean
17
+ readonly schedulerStopped: boolean
18
+ readonly stale: number
19
+ readonly failing: number
20
+ } | null
21
+ }
22
+
23
+ function respond(body: ReadyBody): NextResponse {
24
+ return NextResponse.json(body, {
25
+ status: body.ok ? 200 : 503,
26
+ headers: { 'cache-control': 'no-store' },
27
+ })
28
+ }
29
+
30
+ export async function GET(): Promise<NextResponse> {
31
+ const repository = systemHealthRepository()
32
+ const now = new Date()
33
+
34
+ if (repository === null) {
35
+ return respond({ ok: true, at: now.toISOString(), database: { ok: true }, scheduler: null })
36
+ }
37
+
38
+ try {
39
+ const tasks = await repository.taskHealth()
40
+ const health = assessScheduler(tasks, now)
41
+
42
+ return respond({
43
+ ok: !health.schedulerStopped,
44
+ at: now.toISOString(),
45
+ database: { ok: true },
46
+ scheduler: {
47
+ ok: !health.schedulerStopped,
48
+ schedulerStopped: health.schedulerStopped,
49
+ stale: health.stale,
50
+ failing: health.failing,
51
+ },
52
+ })
53
+ } catch (err) {
54
+ logger({ module: 'ready' }).error({ err }, 'readiness check could not reach the database')
55
+
56
+ return respond({
57
+ ok: false,
58
+ at: now.toISOString(),
59
+ database: { ok: false, error: 'database unreachable' },
60
+ scheduler: null,
61
+ })
62
+ }
63
+ }
@@ -0,0 +1,77 @@
1
+ import { NextResponse } from 'next/server'
2
+
3
+ import { env, logger, withRequestContext } from '@meith/core'
4
+ import { tick } from '@meith/tasks'
5
+
6
+ import { getContainer } from '@/server/container'
7
+ import { bearerSecret, secretMatches } from '@/server/secret-auth'
8
+
9
+ export const dynamic = 'force-dynamic'
10
+ export const revalidate = 0
11
+
12
+ const SECRET_HEADER = 'x-tick-secret'
13
+
14
+ function authorised(request: Request): boolean {
15
+ const expected = env.TICK_SECRET
16
+
17
+ if (!expected) {
18
+ logger().warn(
19
+ 'TICK_SECRET is not set; the tick endpoint is unauthenticated. This is ' +
20
+ 'permitted in development only.',
21
+ )
22
+ return true
23
+ }
24
+
25
+ if (new URL(request.url).searchParams.has('secret')) {
26
+ logger().warn(
27
+ `The tick was called with the secret in the query string, which is no longer read. Present it as "Authorization: Bearer <TICK_SECRET>" or as the "${SECRET_HEADER}" header instead.`,
28
+ )
29
+ }
30
+
31
+ return secretMatches(bearerSecret(request, SECRET_HEADER), expected)
32
+ }
33
+
34
+ async function runTick(request: Request): Promise<NextResponse> {
35
+ return withRequestContext({}, async () => {
36
+ if (!authorised(request)) {
37
+ return NextResponse.json({ error: 'Not found' }, { status: 404 })
38
+ }
39
+
40
+ const { scheduler } = getContainer()
41
+
42
+ if (!scheduler) {
43
+ return NextResponse.json(
44
+ {
45
+ ok: false,
46
+ ran: [],
47
+ reason:
48
+ 'No scheduler: DATA_SOURCE is "fixture". The tick needs durable, ' +
49
+ 'cross-instance state to guarantee a task is not run twice.',
50
+ },
51
+ { status: 503 },
52
+ )
53
+ }
54
+
55
+ const outcomes = await tick({
56
+ repository: scheduler.repository,
57
+ tasks: scheduler.tasks,
58
+ onError: scheduler.onTaskFailure,
59
+ })
60
+
61
+ const failed = outcomes.filter((o) => o.status === 'failed')
62
+
63
+ return NextResponse.json({
64
+ ok: failed.length === 0,
65
+ ran: outcomes,
66
+ registered: scheduler.tasks.length,
67
+ })
68
+ })
69
+ }
70
+
71
+ export async function GET(request: Request): Promise<NextResponse> {
72
+ return runTick(request)
73
+ }
74
+
75
+ export async function POST(request: Request): Promise<NextResponse> {
76
+ return runTick(request)
77
+ }