@jant/core 0.6.16 → 0.7.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 (508) hide show
  1. package/README.md +11 -11
  2. package/bin/commands/assets/upload.js +192 -15
  3. package/bin/commands/import-site.js +60 -4
  4. package/bin/commands/reset-password.js +21 -8
  5. package/bin/commands/site/snapshot/export.js +1 -1
  6. package/bin/commands/site/snapshot/import.js +1 -1
  7. package/bin/lib/d1-query.js +5 -16
  8. package/bin/lib/node-env.js +40 -4
  9. package/bin/lib/site-selection.js +85 -29
  10. package/bin/lib/wrangler-cli.js +28 -2
  11. package/bin/lib/wrangler-json.js +35 -0
  12. package/dist/{app-C7_4oab9.js → app-K_Aa1MMn.js} +52476 -13330
  13. package/dist/client/.vite/manifest.json +255 -90
  14. package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
  15. package/dist/client/_assets/chunks/create-editor-CD3FhrOB.js +257 -0
  16. package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
  17. package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
  18. package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
  19. package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
  20. package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
  21. package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
  22. package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
  23. package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
  24. package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
  25. package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
  26. package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
  27. package/dist/client/_assets/chunks/sortable-list-CgaL2jCs.js +2 -0
  28. package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
  29. package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
  30. package/dist/client/_assets/chunks/unsafe-svg-0QCkP0vZ.js +2 -0
  31. package/dist/client/_assets/client-GiYENVw8.css +2 -0
  32. package/dist/client/_assets/client-VnFxJN7G.js +295 -0
  33. package/dist/client/_assets/client-auth-k8gJ6QJj.js +828 -0
  34. package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
  35. package/dist/client/_assets/client-compose-B_kDtWkW.js +2004 -0
  36. package/dist/client/_assets/client-manage-M90aSTOk.js +2170 -0
  37. package/dist/client/_assets/client-settings-DfYs9n1F.js +1112 -0
  38. package/dist/{export-Dl5KCiEs.js → github-sync-BPAvT999.js} +2254 -107
  39. package/dist/index.js +2 -5
  40. package/dist/node.js +5 -7
  41. package/package.json +3 -3
  42. package/src/__tests__/auth-session-freshness.test.ts +57 -0
  43. package/src/__tests__/basecoat-variants.test.ts +121 -0
  44. package/src/__tests__/bin/assets-upload.test.ts +68 -0
  45. package/src/__tests__/export-hugo-build.test.ts +260 -0
  46. package/src/__tests__/export-service.test.ts +231 -0
  47. package/src/__tests__/helpers/component-css.ts +91 -0
  48. package/src/__tests__/helpers/db.ts +33 -0
  49. package/src/__tests__/helpers/export-fixtures.ts +3 -0
  50. package/src/__tests__/stylesheet-audience.test.ts +159 -0
  51. package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
  52. package/src/app.tsx +71 -16
  53. package/src/auth.ts +18 -1
  54. package/src/client/__tests__/collection-page-actions.test.ts +51 -2
  55. package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
  56. package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
  57. package/src/client/__tests__/compose-bridge.test.ts +272 -0
  58. package/src/client/__tests__/compose-launch.test.ts +62 -1
  59. package/src/client/__tests__/compose-shortcuts.test.ts +38 -9
  60. package/src/client/__tests__/compose-triggers.test.ts +90 -0
  61. package/src/client/__tests__/copy-field.test.ts +162 -0
  62. package/src/client/__tests__/custom-url-menu.test.ts +127 -6
  63. package/src/client/__tests__/image-processor.test.ts +324 -2
  64. package/src/client/__tests__/lazy-entries.test.ts +92 -0
  65. package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
  66. package/src/client/__tests__/runtime-paths.test.ts +84 -0
  67. package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
  68. package/src/client/__tests__/toast.test.ts +23 -0
  69. package/src/client/__tests__/video-processor-probe.test.ts +175 -0
  70. package/src/client/__tests__/video-processor.test.ts +181 -0
  71. package/src/client/audio-processor.ts +24 -15
  72. package/src/client/audio-waveform.ts +49 -0
  73. package/src/client/collection-dialog-host.ts +72 -0
  74. package/src/client/collection-navigation.ts +51 -16
  75. package/src/client/collection-page-actions.ts +26 -0
  76. package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
  77. package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
  78. package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
  79. package/src/client/components/__tests__/jant-command-palette.test.ts +28 -0
  80. package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
  81. package/src/client/components/__tests__/jant-compose-dialog.test.ts +1250 -53
  82. package/src/client/components/__tests__/jant-compose-editor.test.ts +544 -40
  83. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +0 -1
  84. package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
  85. package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
  86. package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
  87. package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
  88. package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
  89. package/src/client/components/__tests__/jant-settings-general.test.ts +541 -172
  90. package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
  91. package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
  92. package/src/client/components/collection-dialog-types.ts +49 -0
  93. package/src/client/components/collection-manager-types.ts +32 -1
  94. package/src/client/components/collection-types.ts +1 -15
  95. package/src/client/components/compose-types.ts +75 -1
  96. package/src/client/components/jant-collection-dialog.ts +722 -0
  97. package/src/client/components/jant-collection-directory.ts +567 -182
  98. package/src/client/components/jant-collection-form.ts +53 -232
  99. package/src/client/components/jant-command-palette.ts +100 -49
  100. package/src/client/components/jant-compose-dialog.ts +1486 -707
  101. package/src/client/components/jant-compose-editor.ts +673 -336
  102. package/src/client/components/jant-compose-fullscreen.ts +51 -43
  103. package/src/client/components/jant-config-editor.ts +65 -53
  104. package/src/client/components/jant-confirm-dialog.ts +13 -11
  105. package/src/client/components/jant-locale-picker.ts +418 -0
  106. package/src/client/components/jant-media-lightbox.ts +146 -136
  107. package/src/client/components/jant-nav-manager.ts +690 -366
  108. package/src/client/components/jant-post-menu.ts +1086 -282
  109. package/src/client/components/jant-post-picker.ts +324 -0
  110. package/src/client/components/jant-repo-picker.ts +138 -106
  111. package/src/client/components/jant-settings-avatar.ts +31 -27
  112. package/src/client/components/jant-settings-general.ts +617 -543
  113. package/src/client/components/jant-settings-language.ts +1087 -0
  114. package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
  115. package/src/client/components/jant-text-preview.ts +94 -86
  116. package/src/client/components/nav-manager-types.ts +62 -1
  117. package/src/client/components/settings-types.ts +14 -29
  118. package/src/client/components/smart-collection-conditions.ts +285 -0
  119. package/src/client/components/smart-collection-dialog-types.ts +68 -0
  120. package/src/client/compose-bridge.ts +214 -141
  121. package/src/client/compose-launch.ts +76 -2
  122. package/src/client/compose-shortcuts.ts +6 -8
  123. package/src/client/compose-triggers.ts +50 -0
  124. package/src/client/copy-field.ts +108 -0
  125. package/src/client/custom-url-menu.ts +60 -4
  126. package/src/client/icons.ts +14 -0
  127. package/src/client/image-plan.ts +165 -0
  128. package/src/client/image-processor.ts +195 -117
  129. package/src/client/image-worker-client.ts +45 -0
  130. package/src/client/image-worker.ts +135 -0
  131. package/src/client/lazy-entries.ts +95 -0
  132. package/src/client/lazy-slugify.ts +2 -2
  133. package/src/client/media-scroll-hint.ts +43 -2
  134. package/src/client/mediabunny.ts +29 -0
  135. package/src/client/post-picker.ts +41 -0
  136. package/src/client/runtime-paths.ts +74 -1
  137. package/src/client/settings-bridge.ts +4 -10
  138. package/src/client/site-header-nav.js +18 -6
  139. package/src/client/smart-collection-dialog-host.ts +116 -0
  140. package/src/client/smart-collection-page-actions.ts +208 -0
  141. package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
  142. package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
  143. package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
  144. package/src/client/tiptap/bubble-menu.ts +47 -16
  145. package/src/client/tiptap/floating-position.ts +216 -2
  146. package/src/client/tiptap/inline-image-upload.ts +7 -3
  147. package/src/client/tiptap/link-toolbar.ts +22 -9
  148. package/src/client/toast.ts +13 -0
  149. package/src/client/types/vite-worker.d.ts +12 -0
  150. package/src/client/upload-with-metadata.ts +3 -13
  151. package/src/client/video-processor.ts +351 -107
  152. package/src/client-auth.ts +14 -31
  153. package/src/client-compose.ts +19 -0
  154. package/src/client-manage.ts +13 -0
  155. package/src/client-settings.ts +14 -0
  156. package/src/client-site.ts +10 -3
  157. package/src/client.ts +4 -0
  158. package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
  159. package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
  160. package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
  161. package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
  162. package/src/db/migrations/0032_married_tomas.sql +3 -0
  163. package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
  164. package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
  165. package/src/db/migrations/meta/0032_snapshot.json +2582 -0
  166. package/src/db/migrations/meta/0033_snapshot.json +2816 -0
  167. package/src/db/migrations/meta/0034_snapshot.json +2816 -0
  168. package/src/db/migrations/meta/_journal.json +21 -0
  169. package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
  170. package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
  171. package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
  172. package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
  173. package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
  174. package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
  175. package/src/db/migrations/pg/meta/_journal.json +21 -0
  176. package/src/db/pg/schema.ts +195 -27
  177. package/src/db/post-visibility.ts +117 -0
  178. package/src/db/schema.ts +222 -27
  179. package/src/i18n/__tests__/detect.test.ts +26 -81
  180. package/src/i18n/__tests__/supported-locales.test.ts +83 -0
  181. package/src/i18n/context.tsx +12 -2
  182. package/src/i18n/detect.ts +7 -66
  183. package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
  184. package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
  185. package/src/i18n/locales/public/en.po +490 -158
  186. package/src/i18n/locales/public/en.ts +1 -1
  187. package/src/i18n/locales/public/zh-Hans.po +487 -155
  188. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  189. package/src/i18n/locales/public/zh-Hant.po +487 -155
  190. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  191. package/src/i18n/locales/settings/en.po +551 -69
  192. package/src/i18n/locales/settings/en.ts +1 -1
  193. package/src/i18n/locales/settings/zh-Hans.po +597 -115
  194. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  195. package/src/i18n/locales/settings/zh-Hant.po +610 -128
  196. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  197. package/src/i18n/locales.ts +94 -0
  198. package/src/i18n/middleware.ts +12 -2
  199. package/src/i18n/supported-locales.ts +80 -0
  200. package/src/lib/__tests__/api-settings.test.ts +11 -2
  201. package/src/lib/__tests__/asset-path.test.ts +92 -0
  202. package/src/lib/__tests__/deferred.test.ts +90 -0
  203. package/src/lib/__tests__/discover-ping.test.ts +89 -0
  204. package/src/lib/__tests__/discover.test.ts +410 -0
  205. package/src/lib/__tests__/feed-policy.test.ts +26 -0
  206. package/src/lib/__tests__/feed.test.ts +1526 -15
  207. package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
  208. package/src/lib/__tests__/hosted-signin.test.ts +1 -1
  209. package/src/lib/__tests__/http-cache.test.ts +162 -0
  210. package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
  211. package/src/lib/__tests__/jant-docs.test.ts +23 -0
  212. package/src/lib/__tests__/lang-detect.test.ts +231 -0
  213. package/src/lib/__tests__/media-helpers.test.ts +81 -0
  214. package/src/lib/__tests__/navigation.test.ts +135 -1
  215. package/src/lib/__tests__/resolve-config.test.ts +64 -3
  216. package/src/lib/__tests__/schemas.test.ts +42 -4
  217. package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
  218. package/src/lib/__tests__/slug.test.ts +70 -0
  219. package/src/lib/__tests__/slugify.test.ts +85 -0
  220. package/src/lib/__tests__/storage.test.ts +32 -0
  221. package/src/lib/__tests__/thread-fold.test.ts +113 -0
  222. package/src/lib/__tests__/upload-sideload.test.ts +14 -0
  223. package/src/lib/__tests__/url-fetch.test.ts +1 -2
  224. package/src/lib/__tests__/url.test.ts +78 -72
  225. package/src/lib/__tests__/view-language.test.ts +413 -0
  226. package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
  227. package/src/lib/address.ts +57 -0
  228. package/src/lib/api-posts.ts +12 -0
  229. package/src/lib/asset-path.ts +98 -1
  230. package/src/lib/build-env.ts +16 -0
  231. package/src/lib/collection-groups.ts +5 -1
  232. package/src/lib/collection-paths.ts +3 -10
  233. package/src/lib/constants.ts +28 -3
  234. package/src/lib/copy-field.ts +36 -0
  235. package/src/lib/crypto.ts +40 -0
  236. package/src/lib/csp-builder.ts +2 -1
  237. package/src/lib/deferred.ts +79 -0
  238. package/src/lib/discover-ping.ts +141 -0
  239. package/src/lib/discover.ts +437 -0
  240. package/src/lib/env.ts +134 -0
  241. package/src/lib/errors.ts +33 -0
  242. package/src/lib/feed-policy.ts +236 -2
  243. package/src/lib/feed.ts +926 -73
  244. package/src/lib/filter-dimensions.ts +1349 -0
  245. package/src/lib/github-sync-site-config.ts +3 -0
  246. package/src/lib/hosted-signin.ts +1 -1
  247. package/src/lib/http-cache.ts +148 -0
  248. package/src/lib/hugo-markdown.ts +16 -0
  249. package/src/lib/icons.ts +9 -2
  250. package/src/lib/ids.ts +3 -0
  251. package/src/lib/jant-docs.ts +42 -0
  252. package/src/lib/lang-detect.ts +308 -0
  253. package/src/lib/media-helpers.ts +13 -1
  254. package/src/lib/navigation.ts +78 -11
  255. package/src/lib/pagination.ts +28 -24
  256. package/src/lib/per-language-surfaces.ts +67 -0
  257. package/src/lib/post-meta.ts +19 -0
  258. package/src/lib/render.tsx +55 -7
  259. package/src/lib/resolve-config.ts +53 -5
  260. package/src/lib/schemas.ts +201 -10
  261. package/src/lib/site-header-fragment.tsx +2 -1
  262. package/src/lib/site-unavailable-page.ts +47 -0
  263. package/src/lib/slug.ts +51 -7
  264. package/src/lib/slugify.ts +113 -0
  265. package/src/lib/startup-config.ts +4 -6
  266. package/src/lib/storage.ts +7 -0
  267. package/src/lib/summary.ts +50 -0
  268. package/src/lib/thread-fold.ts +161 -0
  269. package/src/lib/time.ts +32 -0
  270. package/src/lib/timeline.ts +42 -1
  271. package/src/lib/tiptap-render.ts +1 -2
  272. package/src/lib/upload.ts +16 -13
  273. package/src/lib/url.ts +139 -86
  274. package/src/lib/version.ts +10 -8
  275. package/src/lib/view-language.ts +436 -0
  276. package/src/lib/view.ts +51 -23
  277. package/src/lib/viewer-context.ts +62 -0
  278. package/src/middleware/__tests__/auth.test.ts +70 -2
  279. package/src/middleware/__tests__/cache-control.test.ts +71 -3
  280. package/src/middleware/__tests__/onboarding.test.ts +76 -5
  281. package/src/middleware/__tests__/secure-headers.test.ts +1 -1
  282. package/src/middleware/__tests__/session.test.ts +181 -33
  283. package/src/middleware/auth.ts +50 -31
  284. package/src/middleware/cache-control.ts +75 -10
  285. package/src/middleware/config.ts +6 -6
  286. package/src/middleware/error-handler.ts +12 -0
  287. package/src/middleware/onboarding.ts +32 -10
  288. package/src/middleware/secure-headers.ts +1 -1
  289. package/src/middleware/session.ts +96 -8
  290. package/src/node/__tests__/cli-migrate.test.ts +21 -19
  291. package/src/node/__tests__/cli-reset-password.test.ts +81 -8
  292. package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
  293. package/src/node/__tests__/cli-site-selection.test.ts +128 -38
  294. package/src/node/__tests__/runtime.test.ts +37 -0
  295. package/src/node/index.ts +1 -0
  296. package/src/preset.css +76 -22
  297. package/src/routes/__tests__/feed-validator.test.ts +156 -0
  298. package/src/routes/api/__tests__/collections.test.ts +114 -0
  299. package/src/routes/api/__tests__/discover.test.ts +222 -0
  300. package/src/routes/api/__tests__/nav-items.test.ts +212 -2
  301. package/src/routes/api/__tests__/post-translations.test.ts +829 -0
  302. package/src/routes/api/__tests__/settings.test.ts +24 -3
  303. package/src/routes/api/__tests__/telegram.test.ts +6 -5
  304. package/src/routes/api/collections.ts +49 -6
  305. package/src/routes/api/custom-urls.ts +0 -1
  306. package/src/routes/api/discover.ts +95 -0
  307. package/src/routes/api/export.ts +3 -0
  308. package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
  309. package/src/routes/api/nav-items.ts +55 -8
  310. package/src/routes/api/posts.ts +159 -0
  311. package/src/routes/api/public/__tests__/archive.test.ts +111 -0
  312. package/src/routes/api/public/archive.ts +77 -81
  313. package/src/routes/api/public/posts.ts +23 -6
  314. package/src/routes/api/smart-collections.ts +132 -0
  315. package/src/routes/api/telegram.ts +2 -40
  316. package/src/routes/api/uploads.ts +1 -2
  317. package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
  318. package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
  319. package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
  320. package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
  321. package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
  322. package/src/routes/auth/__tests__/setup.test.ts +278 -26
  323. package/src/routes/auth/__tests__/signin.test.ts +135 -0
  324. package/src/routes/auth/setup.tsx +934 -186
  325. package/src/routes/auth/signin.tsx +10 -8
  326. package/src/routes/compose.tsx +5 -0
  327. package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
  328. package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
  329. package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
  330. package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
  331. package/src/routes/dash/custom-urls.tsx +1 -37
  332. package/src/routes/dash/settings.tsx +502 -41
  333. package/src/routes/discover-announce.ts +96 -0
  334. package/src/routes/feed/__tests__/feed.test.ts +165 -2
  335. package/src/routes/feed/feed.ts +69 -40
  336. package/src/routes/feed/sitemap.ts +95 -11
  337. package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
  338. package/src/routes/pages/__tests__/archive-params.test.ts +370 -6
  339. package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
  340. package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
  341. package/src/routes/pages/__tests__/collections.test.ts +9 -3
  342. package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
  343. package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
  344. package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
  345. package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
  346. package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
  347. package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
  348. package/src/routes/pages/archive.tsx +471 -369
  349. package/src/routes/pages/collection.tsx +94 -96
  350. package/src/routes/pages/collections.tsx +38 -50
  351. package/src/routes/pages/featured.tsx +32 -13
  352. package/src/routes/pages/home.tsx +25 -6
  353. package/src/routes/pages/language.tsx +113 -0
  354. package/src/routes/pages/latest.tsx +26 -16
  355. package/src/routes/pages/new.tsx +3 -0
  356. package/src/routes/pages/page.tsx +178 -17
  357. package/src/routes/pages/partials.tsx +41 -0
  358. package/src/routes/pages/search.tsx +18 -3
  359. package/src/routes/pages/smart-collection.tsx +340 -0
  360. package/src/routes/pages/subscribe.tsx +182 -0
  361. package/src/runtime/__tests__/index.test.ts +19 -0
  362. package/src/runtime/__tests__/node.test.ts +129 -4
  363. package/src/runtime/cloudflare.ts +21 -2
  364. package/src/runtime/index.ts +27 -1
  365. package/src/runtime/node.ts +18 -1
  366. package/src/runtime/site.ts +95 -24
  367. package/src/services/__tests__/auth.test.ts +116 -7
  368. package/src/services/__tests__/collection.test.ts +108 -4
  369. package/src/services/__tests__/language.test.ts +495 -0
  370. package/src/services/__tests__/navigation.test.ts +213 -10
  371. package/src/services/__tests__/path.test.ts +180 -0
  372. package/src/services/__tests__/post-language.test.ts +830 -0
  373. package/src/services/__tests__/post.test.ts +46 -9
  374. package/src/services/__tests__/settings.test.ts +228 -17
  375. package/src/services/__tests__/site-admin.test.ts +5 -5
  376. package/src/services/__tests__/smart-collection.test.ts +651 -0
  377. package/src/services/about-page.ts +22 -1
  378. package/src/services/auth.ts +51 -49
  379. package/src/services/bootstrap.ts +188 -38
  380. package/src/services/collection-directory-position.ts +76 -0
  381. package/src/services/collection.ts +331 -46
  382. package/src/services/custom-url.ts +27 -22
  383. package/src/services/export-theme/assets/client-site.css +1 -1
  384. package/src/services/export-theme/assets/client-site.js +123 -123
  385. package/src/services/export-theme/layouts/_default/rss.xml +77 -1
  386. package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
  387. package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
  388. package/src/services/export-theme/layouts/partials/pagination.html +14 -13
  389. package/src/services/export-theme/layouts/partials/post-card.html +8 -4
  390. package/src/services/export-theme/layouts/partials/reply.html +2 -1
  391. package/src/services/export-theme/layouts/partials/thread-preview.html +40 -30
  392. package/src/services/export-theme/styles/main.css +13 -1
  393. package/src/services/export.ts +278 -19
  394. package/src/services/index.ts +34 -3
  395. package/src/services/language.ts +412 -0
  396. package/src/services/mcp.ts +3 -1
  397. package/src/services/media.ts +14 -6
  398. package/src/services/navigation.ts +429 -67
  399. package/src/services/path.ts +221 -6
  400. package/src/services/post.ts +1021 -152
  401. package/src/services/search.ts +39 -2
  402. package/src/services/settings.ts +238 -43
  403. package/src/services/site-admin.ts +24 -7
  404. package/src/services/site.ts +77 -1
  405. package/src/services/smart-collection.ts +636 -0
  406. package/src/services/telegram.ts +20 -22
  407. package/src/services/upload-session.ts +1 -3
  408. package/src/style-author.css +27 -0
  409. package/src/styles/components-author.css +1622 -0
  410. package/src/styles/components.css +24 -1571
  411. package/src/styles/site-media.css +16 -2
  412. package/src/styles/tokens.css +82 -12
  413. package/src/styles/ui-author.css +7602 -0
  414. package/src/styles/ui.css +1087 -7219
  415. package/src/types/app-context.ts +7 -0
  416. package/src/types/bindings.ts +3 -0
  417. package/src/types/config.ts +94 -15
  418. package/src/types/constants.ts +188 -16
  419. package/src/types/entities.ts +87 -1
  420. package/src/types/operations.ts +64 -2
  421. package/src/types/props.ts +157 -31
  422. package/src/types/views.ts +55 -1
  423. package/src/ui/__tests__/font-themes.test.ts +52 -12
  424. package/src/ui/compose/ComposeDialog.tsx +201 -7
  425. package/src/ui/compose/ComposePrompt.tsx +1 -5
  426. package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
  427. package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
  428. package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
  429. package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
  430. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
  431. package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
  432. package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
  433. package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
  434. package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
  435. package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
  436. package/src/ui/dash/settings/GeneralContent.tsx +275 -113
  437. package/src/ui/dash/settings/LanguageContent.tsx +409 -0
  438. package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
  439. package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
  440. package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
  441. package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
  442. package/src/ui/dash/settings/discover-copy.ts +116 -0
  443. package/src/ui/feed/LinkCard.tsx +55 -45
  444. package/src/ui/feed/NoteCard.tsx +9 -4
  445. package/src/ui/feed/PostStatusBadges.tsx +90 -43
  446. package/src/ui/feed/QuoteCard.tsx +55 -30
  447. package/src/ui/feed/ThreadPreview.tsx +6 -2
  448. package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
  449. package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
  450. package/src/ui/feed/thread-preview-state.ts +20 -6
  451. package/src/ui/font-themes.ts +39 -29
  452. package/src/ui/layouts/BaseLayout.tsx +155 -10
  453. package/src/ui/layouts/SiteLayout.tsx +192 -34
  454. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
  455. package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
  456. package/src/ui/pages/ArchivePage.tsx +282 -289
  457. package/src/ui/pages/CollectionPage.tsx +86 -76
  458. package/src/ui/pages/CollectionsPage.tsx +5 -0
  459. package/src/ui/pages/ComposePage.tsx +8 -0
  460. package/src/ui/pages/PostPage.tsx +47 -0
  461. package/src/ui/pages/SearchPage.tsx +3 -3
  462. package/src/ui/pages/SmartCollectionPage.tsx +434 -0
  463. package/src/ui/pages/SubscribePage.tsx +236 -0
  464. package/src/ui/pages/__tests__/ArchivePage.test.tsx +65 -9
  465. package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
  466. package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
  467. package/src/ui/shared/CollectionDirectory.tsx +178 -3
  468. package/src/ui/shared/CollectionsManager.tsx +70 -17
  469. package/src/ui/shared/CopyField.tsx +127 -0
  470. package/src/ui/shared/EmptyPostContent.tsx +23 -0
  471. package/src/ui/shared/MediaGallery.tsx +125 -46
  472. package/src/ui/shared/Pagination.tsx +20 -5
  473. package/src/ui/shared/PostFooter.tsx +20 -11
  474. package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
  475. package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
  476. package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
  477. package/src/ui/shared/__tests__/pagination.test.ts +38 -24
  478. package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
  479. package/src/ui/shared/archive-labels.ts +98 -0
  480. package/src/ui/shared/collection-management-labels.ts +94 -16
  481. package/src/ui/shared/navigation-labels.ts +45 -71
  482. package/src/ui/shared/post-article-attributes.ts +2 -0
  483. package/src/ui/shared/smart-collection-labels.ts +343 -0
  484. package/dist/app-C75Zc8dD.js +0 -6
  485. package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
  486. package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
  487. package/dist/client/_assets/chunks/url-CU9pEbk5.js +0 -1
  488. package/dist/client/_assets/client-Bq0vre8Y.js +0 -296
  489. package/dist/client/_assets/client-D3VZyG4L.css +0 -2
  490. package/dist/client/_assets/client-auth-F2rb9WfF.js +0 -5571
  491. package/dist/env-BPYViDJJ.js +0 -281
  492. package/dist/github-api-BNF35Lkx.js +0 -176
  493. package/dist/github-app-DdiD-bC4.js +0 -275
  494. package/dist/github-sync-Bqg62IvV.js +0 -4
  495. package/dist/github-sync-HdK2EgvJ.js +0 -431
  496. package/dist/url-CbLAtlZe.js +0 -900
  497. package/src/client/__tests__/collection-created-notice.test.ts +0 -36
  498. package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
  499. package/src/client/__tests__/post-form-bridge.test.ts +0 -82
  500. package/src/client/collection-created-notice.ts +0 -66
  501. package/src/client/collection-form-bridge.ts +0 -191
  502. package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
  503. package/src/client/components/jant-post-form.ts +0 -422
  504. package/src/client/components/post-form-template.ts +0 -352
  505. package/src/client/components/post-form-types.ts +0 -96
  506. package/src/client/media-metadata.ts +0 -247
  507. package/src/client/post-form-bridge.ts +0 -193
  508. package/src/ui/pages/CollectionEditorPage.tsx +0 -175
@@ -5,7 +5,7 @@
5
5
  * or Bearer API tokens.
6
6
  */
7
7
 
8
- import type { MiddlewareHandler } from "hono";
8
+ import type { Context, MiddlewareHandler } from "hono";
9
9
  import type { Bindings } from "../types.js";
10
10
  import type { AppVariables } from "../types/app-context.js";
11
11
  import { getDevApiToken, getInternalAdminToken } from "../lib/env.js";
@@ -110,6 +110,42 @@ function getPostSigninRedirect(requestUrl: string): string | null {
110
110
  return isSafeInternalRedirect(candidate) ? candidate : null;
111
111
  }
112
112
 
113
+ /**
114
+ * Whether the request's session belongs to a member of the current site.
115
+ *
116
+ * This is the single definition of "signed in *here*", and every entrance must
117
+ * ask it rather than `c.var.isAuthenticated`. That flag only answers "carries a
118
+ * session", which is a weaker claim: better-auth's cookie cache keeps answering
119
+ * from the browser's own copy for minutes after the rows behind it are gone, so
120
+ * a session routinely outlives the site it was issued for — a factory reset and
121
+ * a fresh setup being the obvious way. When a guard requires membership and an
122
+ * entrance settles for a session, the two disagree and bounce the visitor
123
+ * between them until the browser gives up.
124
+ *
125
+ * @param c - The request context, after `attachSession` has run
126
+ * @returns `true` when the session's user is a member of `c.var.currentSite`
127
+ *
128
+ * @example
129
+ * ```ts
130
+ * // /signin only sends someone away if they are signed in to *this* site.
131
+ * if (await isCurrentSiteMember(c)) return c.redirect("/");
132
+ * ```
133
+ */
134
+ export async function isCurrentSiteMember(c: Context<Env>): Promise<boolean> {
135
+ const session = c.var.session;
136
+ if (!session?.user) return false;
137
+
138
+ // `siteMembers.get` resolves to `null` for "not a member", so a throw is
139
+ // always infrastructure and is left to propagate. Rewriting a failing
140
+ // database read into "not a member" would read to the user as being randomly
141
+ // signed out.
142
+ const membership = await c.var.services.siteMembers.get(
143
+ c.var.currentSite.id,
144
+ session.user.id,
145
+ );
146
+ return !!membership;
147
+ }
148
+
113
149
  /**
114
150
  * Middleware that requires authentication.
115
151
  * Redirects to signin page if not authenticated.
@@ -136,25 +172,16 @@ export function requireAuth(redirectTo = "/signin"): MiddlewareHandler<Env> {
136
172
  return `${publicHref}${separator}redirect=${encodeURIComponent(postSignin)}`;
137
173
  };
138
174
 
139
- // Session was already fetched by `attachSession` middleware.
140
- const session = c.var.session;
141
- if (!session?.user) {
175
+ // Session was already fetched by `attachSession` middleware. The membership
176
+ // read inside `isCurrentSiteMember` and `next()` both stay outside a catch:
177
+ // a failing database read or a route handler blowing up used to be
178
+ // rewritten into a redirect to sign-in, which reads to the user as being
179
+ // randomly signed out and loses whatever they were doing.
180
+ if (!(await isCurrentSiteMember(c))) {
142
181
  return c.redirect(buildRedirectTarget());
143
182
  }
144
183
 
145
- try {
146
- const membership = await c.var.services.siteMembers.get(
147
- c.var.currentSite.id,
148
- session.user.id,
149
- );
150
- if (!membership) {
151
- return c.redirect(buildRedirectTarget());
152
- }
153
-
154
- await next();
155
- } catch {
156
- return c.redirect(buildRedirectTarget());
157
- }
184
+ await next();
158
185
  };
159
186
  }
160
187
 
@@ -166,20 +193,12 @@ export function requireAuth(redirectTo = "/signin"): MiddlewareHandler<Env> {
166
193
  export function requireAuthApi(): MiddlewareHandler<Env> {
167
194
  return async (c, next) => {
168
195
  // 1. Try session auth (session is pre-fetched by `attachSession` middleware).
169
- const session = c.var.session;
170
- if (session?.user) {
171
- try {
172
- const membership = await c.var.services.siteMembers.get(
173
- c.var.currentSite.id,
174
- session.user.id,
175
- );
176
- if (membership) {
177
- await next();
178
- return;
179
- }
180
- } catch {
181
- // Membership check failed — fall through to Bearer token
182
- }
196
+ // Only the membership lookup falls through to Bearer on failure. `next()`
197
+ // must stay outside that catch, or a route handler throwing turns into a
198
+ // 401 and the client treats a server error as an expired login.
199
+ if (await isCurrentSiteMember(c).catch(() => false)) {
200
+ await next();
201
+ return;
183
202
  }
184
203
 
185
204
  // 2. Try Bearer token auth
@@ -20,6 +20,26 @@
20
20
  * sitemaps, favicons, manifests, static assets) set their own `Cache-Control`
21
21
  * explicitly; this middleware leaves those untouched and only fills in the
22
22
  * default for the un-annotated dynamic responses.
23
+ *
24
+ * The default differs by audience, because `no-store` costs more than it
25
+ * looks: it also disables the browser's back/forward cache, so going back
26
+ * re-requests the page, re-renders it and re-runs its scripts instead of
27
+ * restoring it instantly. For an anonymous visitor there is nothing to
28
+ * protect — the page is the public one anybody gets — so they get `no-cache`,
29
+ * which keeps shared caches out exactly as before (that is `private`'s job)
30
+ * while letting the browser hold its own copy and revalidate.
31
+ *
32
+ * A signed-in author keeps `no-store`. The back/forward cache restores a whole
33
+ * page snapshot without asking the server, so a page rendered while signed in
34
+ * would still be there after signing out — on a shared machine that shows the
35
+ * previous session's view. Trading the author's back-button latency for that
36
+ * is not worth it; readers are the traffic that matters here anyway.
37
+ *
38
+ * Credential surfaces are the exception to "anonymous has nothing to protect".
39
+ * A sign-in form, a setup form, a reset page whose URL carries a one-time
40
+ * token, an SSO handoff: all of them are served to someone with no session
41
+ * yet, so the anonymous default would let the browser write them to disk.
42
+ * `noStore()` marks those paths, and `app.tsx` mounts it on every one of them.
23
43
  */
24
44
 
25
45
  import type { MiddlewareHandler } from "hono";
@@ -29,32 +49,77 @@ import type { AppVariables } from "../types/app-context.js";
29
49
  type Env = { Bindings: Bindings; Variables: AppVariables };
30
50
 
31
51
  /**
32
- * Default cache directive for dynamic, potentially auth-variant responses.
33
- * `private` forbids shared/CDN caches from storing the response; `no-store`
34
- * prevents any cache (including the browser) from keeping a copy.
52
+ * Nothing keeps a copy. `private` forbids shared/CDN caches from storing the
53
+ * response; `no-store` prevents any cache — the browser's own, and its
54
+ * back/forward cache — from keeping one either.
55
+ *
56
+ * The default for a signed-in author, and the rule for every credential
57
+ * surface whatever the audience.
35
58
  */
36
- const DEFAULT_CACHE_CONTROL = "private, no-store";
59
+ const NO_STORE_CACHE_CONTROL = "private, no-store";
37
60
 
38
61
  /**
39
- * Middleware that defaults a missing `Cache-Control` header to
40
- * `private, no-store`.
62
+ * Default for anonymous responses. `private` still keeps shared/CDN caches
63
+ * out; `no-cache` lets the browser keep a copy it must revalidate before
64
+ * reuse, which is what makes the page eligible for the back/forward cache.
65
+ */
66
+ const ANONYMOUS_CACHE_CONTROL = "private, no-cache";
67
+
68
+ /**
69
+ * Middleware that defaults a missing `Cache-Control` header: `private,
70
+ * no-cache` for anonymous responses, `private, no-store` once signed in.
41
71
  *
42
72
  * Runs after the route handler: if the handler (or an inner middleware)
43
73
  * already set `Cache-Control`, that explicit value wins. Only responses that
44
- * declare nothing receive the safe default.
74
+ * declare nothing receive the default. Requires `attachSession()` to have run
75
+ * first; without it every response is treated as anonymous, so mount it after.
45
76
  *
46
77
  * @returns Hono middleware enforcing the default cache policy.
47
78
  *
48
79
  * @example
49
80
  * ```ts
81
+ * app.use("*", attachSession());
50
82
  * app.use("*", defaultCacheControl());
51
83
  * ```
52
84
  */
53
85
  export function defaultCacheControl(): MiddlewareHandler<Env> {
54
86
  return async (c, next) => {
55
87
  await next();
56
- if (!c.res.headers.has("Cache-Control")) {
57
- c.res.headers.set("Cache-Control", DEFAULT_CACHE_CONTROL);
58
- }
88
+ if (c.res.headers.has("Cache-Control")) return;
89
+ c.res.headers.set(
90
+ "Cache-Control",
91
+ c.var.isAuthenticated ? NO_STORE_CACHE_CONTROL : ANONYMOUS_CACHE_CONTROL,
92
+ );
93
+ };
94
+ }
95
+
96
+ /**
97
+ * Middleware for a path that must never be written to any cache.
98
+ *
99
+ * For the credential surfaces: sign-in, sign-out, setup, password reset, the
100
+ * SSO handoff, the dev login, and better-auth's own endpoints. Every one of
101
+ * them is reached without a session, so `defaultCacheControl` would hand them
102
+ * the anonymous `no-cache` — which lets the browser keep the page, and the URL
103
+ * it came from, on disk. A reset link's one-time token then outlives the visit
104
+ * on a shared machine.
105
+ *
106
+ * Unconditional, unlike the default: this is a rule about the path, not a
107
+ * fallback for a path that said nothing. Mount it after
108
+ * `defaultCacheControl()` so the header is already set by the time that one
109
+ * looks, and before the handler it protects — a terminal handler registered
110
+ * first ends the chain and this never runs.
111
+ *
112
+ * @returns Hono middleware forcing `private, no-store`.
113
+ *
114
+ * @example
115
+ * ```ts
116
+ * app.use("/reset", noStore());
117
+ * app.route("/", resetRoutes);
118
+ * ```
119
+ */
120
+ export function noStore(): MiddlewareHandler<Env> {
121
+ return async (c, next) => {
122
+ await next();
123
+ c.res.headers.set("Cache-Control", NO_STORE_CACHE_CONTROL);
59
124
  };
60
125
  }
@@ -18,7 +18,6 @@ import {
18
18
  import { buildThemeStyle, resolveBuiltinTheme } from "../lib/theme.js";
19
19
  import {
20
20
  BUILTIN_FONT_THEMES,
21
- getCjkFontCssVariables,
22
21
  getFontThemeCssVariables,
23
22
  } from "../ui/font-themes.js";
24
23
 
@@ -47,14 +46,15 @@ export function withConfig(): MiddlewareHandler<Env> {
47
46
  // Resolve active color theme
48
47
  const activeTheme = resolveBuiltinTheme(appConfig.themeId);
49
48
 
50
- // Build font theme CSS variables
49
+ // Build font theme CSS variables. The CJK fallback stacks are deliberately
50
+ // NOT resolved here: they depend on the language of the page being
51
+ // rendered — a post's own language, or the language a filtered list view is
52
+ // showing — which this middleware runs too early to know. `BaseLayout`
53
+ // emits them alongside the matching CJK stylesheet instead.
51
54
  const fontTheme = BUILTIN_FONT_THEMES.find(
52
55
  (f) => f.id === appConfig.fontThemeId,
53
56
  );
54
- const fontOverrides = {
55
- ...getCjkFontCssVariables(appConfig.siteLanguage, appConfig.cjkSerifFont),
56
- ...(fontTheme ? getFontThemeCssVariables(fontTheme) : {}),
57
- };
57
+ const fontOverrides = fontTheme ? getFontThemeCssVariables(fontTheme) : {};
58
58
 
59
59
  const themeStyle = buildThemeStyle(
60
60
  activeTheme,
@@ -13,8 +13,10 @@ import {
13
13
  ConfigurationError,
14
14
  DomainError,
15
15
  NotFoundError,
16
+ SiteUnavailableError,
16
17
  ValidationError,
17
18
  } from "../lib/errors.js";
19
+ import { renderSiteUnavailablePage } from "../lib/site-unavailable-page.js";
18
20
  import { dsToast } from "../lib/sse.js";
19
21
  import { getRuntimeConfigurationErrorPage } from "../lib/startup-config.js";
20
22
 
@@ -68,6 +70,16 @@ export const errorHandler: ErrorHandler<Env> = (err, c) => {
68
70
  return c.json({ error: "Something went wrong on our end" }, 500);
69
71
  }
70
72
 
73
+ // A suspended site: the host is real, so answer with a page that says so
74
+ // instead of the generic 404 shell. 503 keeps the site's search index alive
75
+ // while it can still be restored; `Retry-After` is deliberately omitted
76
+ // because we can't promise when (or whether) it comes back.
77
+ if (err instanceof SiteUnavailableError) {
78
+ return c.html(renderSiteUnavailablePage(c.env), 503, {
79
+ "Cache-Control": "no-store",
80
+ });
81
+ }
82
+
71
83
  // Non-API routes: map NotFoundError to Hono's built-in 404
72
84
  if (err instanceof NotFoundError) {
73
85
  return c.notFound();
@@ -1,15 +1,28 @@
1
1
  /**
2
2
  * Onboarding Middleware
3
3
  *
4
- * Redirects key page routes to /setup if onboarding hasn't been completed.
4
+ * Redirects key page routes to /setup while first-run setup is unfinished.
5
5
  * Uses an allowlist approach: only explicitly listed page routes are redirected,
6
6
  * so static assets, API endpoints, feeds, and other resources always pass through.
7
7
  * Caches the result in memory so the DB is only queried once per isolate lifetime.
8
+ *
9
+ * How much is gated depends on how far setup has got:
10
+ *
11
+ * - `pending` — nobody owns the site and it holds nothing. Readers have no
12
+ * business being anywhere but setup, so the public root is gated too.
13
+ * - `provisioned` — the site has an owner and owes setup its last answers. A
14
+ * control plane leaves a hosted site here at creation, and a self-hosted one
15
+ * sits here between its two setup screens. Either way the site is real, so
16
+ * readers are served normally; only the author's own entrances are gated,
17
+ * and only once they are signed in, because the remaining questions are
18
+ * theirs to answer. `/signin` staying open is what lets a self-hosted author
19
+ * who lost the session mid-setup get back to the screen they left.
8
20
  */
9
21
 
10
22
  import type { MiddlewareHandler } from "hono";
11
23
  import type { Bindings } from "../types.js";
12
24
  import type { AppVariables } from "../types/app-context.js";
25
+ import { ONBOARDING_STATUS } from "../lib/constants.js";
13
26
  import { getRuntimeSitePathPrefix } from "../lib/site-resolution.js";
14
27
  import { toPublicPath } from "../lib/url.js";
15
28
 
@@ -31,16 +44,23 @@ export function requireOnboarding(): MiddlewareHandler<Env> {
31
44
  return next();
32
45
  }
33
46
 
34
- if (!shouldRedirect(path)) {
47
+ if (!isGatedPath(path)) {
35
48
  return next();
36
49
  }
37
50
 
38
- const isComplete = await c.var.services.settings.isOnboardingComplete();
39
- if (isComplete) {
51
+ const status = await c.var.services.settings.getOnboardingStatus();
52
+ if (status === ONBOARDING_STATUS.COMPLETED) {
40
53
  completedOnboardingSites.add(c.var.currentSite.id);
41
54
  return next();
42
55
  }
43
56
 
57
+ if (
58
+ status === ONBOARDING_STATUS.PROVISIONED &&
59
+ !(c.var.isAuthenticated && isAuthorPath(path))
60
+ ) {
61
+ return next();
62
+ }
63
+
44
64
  const sitePathPrefix = getRuntimeSitePathPrefix({
45
65
  env: c.env,
46
66
  appConfig: c.var.appConfig,
@@ -52,15 +72,17 @@ export function requireOnboarding(): MiddlewareHandler<Env> {
52
72
  }
53
73
 
54
74
  /**
55
- * Only these page routes are redirected to /setup during onboarding.
75
+ * Only these page routes are ever redirected to /setup.
56
76
  * Everything else (assets, API, feeds, media, etc.) passes through.
57
77
  */
58
- function shouldRedirect(path: string): boolean {
78
+ function isGatedPath(path: string): boolean {
79
+ return path === "/signin" || path === "/reset" || isAuthorPath(path);
80
+ }
81
+
82
+ /** The routes an author uses to reach their own site. */
83
+ function isAuthorPath(path: string): boolean {
59
84
  return (
60
- path === "/" ||
61
- path === "/signin" ||
62
- path === "/reset" ||
63
- path.startsWith("/settings")
85
+ path === "/" || path.startsWith("/settings") || path.startsWith("/compose")
64
86
  );
65
87
  }
66
88
 
@@ -14,7 +14,7 @@ import type { MiddlewareHandler } from "hono";
14
14
  import type { Bindings } from "../types.js";
15
15
  import type { AppVariables } from "../types/app-context.js";
16
16
  import { getConfiguredStorageDriver, getEnvString } from "../lib/env.js";
17
- import { IS_VITE_DEV } from "../lib/version.js";
17
+ import { IS_VITE_DEV } from "../lib/build-env.js";
18
18
  import {
19
19
  buildCspDirectives,
20
20
  type ContentSecurityPolicyDirectives,
@@ -6,31 +6,119 @@
6
6
  *
7
7
  * This replaces ad-hoc `auth.api.getSession()` calls scattered across
8
8
  * view helpers (e.g. `lib/navigation.ts`) so each request only parses
9
- * the session cookie once. better-auth's own cookieCache (5 min) still
10
- * keeps this cheap, but centralizing the call also unlocks `Promise.all`
11
- * patterns in routes that previously serialized on a hidden session fetch.
9
+ * the session cookie once.
10
+ *
11
+ * `getSession` is not purely a read: better-auth re-issues the session cookie
12
+ * with a fresh `Max-Age` once per `updateAge`, and rebuilds the short-lived
13
+ * `session_data` cookie cache whenever it misses. Those live in the endpoint's
14
+ * response headers, which better-call only hands back under `returnHeaders`
15
+ * — without it the cookies are silently dropped, the browser copy expires a
16
+ * fixed 30 days after sign-in however active the user is, and the cookie cache
17
+ * never survives its first 5 minutes. So we take the headers and write them
18
+ * back onto the outgoing response.
12
19
  *
13
20
  * Never throws — any lookup error is treated as "not authenticated".
14
21
  */
15
22
 
16
- import type { MiddlewareHandler } from "hono";
23
+ import type { Context, MiddlewareHandler } from "hono";
17
24
  import type { Bindings } from "../types.js";
18
25
  import type { AppVariables } from "../types/app-context.js";
19
26
 
20
27
  type Env = { Bindings: Bindings; Variables: AppVariables };
21
28
 
29
+ /**
30
+ * Extracts the cookie name from a `Set-Cookie` header value.
31
+ *
32
+ * @param setCookie - A single `Set-Cookie` value, e.g. `a=b; Path=/; Max-Age=1`
33
+ * @returns The cookie name, or the whole trimmed string when there is no `=`
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * readCookieName("__Secure-better-auth.session_token=abc; Path=/"); // "__Secure-better-auth.session_token"
38
+ * ```
39
+ */
40
+ function readCookieName(setCookie: string): string {
41
+ const separator = setCookie.indexOf("=");
42
+ return (separator === -1 ? setCookie : setCookie.slice(0, separator)).trim();
43
+ }
44
+
45
+ /**
46
+ * Whether a `Cache-Control` value lets a shared cache store the response.
47
+ *
48
+ * A missing header counts as not shared-cacheable: `defaultCacheControl` fills
49
+ * those in with `private, no-store` later in the chain, and this middleware
50
+ * must stay correct no matter which of the two unwinds first.
51
+ *
52
+ * @param cacheControl - The response's `Cache-Control`, if any
53
+ * @returns `true` when a CDN or proxy is allowed to keep a copy
54
+ *
55
+ * @example
56
+ * ```ts
57
+ * isSharedCacheable("private, no-store"); // false
58
+ * isSharedCacheable("public, max-age=31536000"); // true
59
+ * isSharedCacheable(null); // false
60
+ * ```
61
+ */
62
+ function isSharedCacheable(cacheControl: string | null): boolean {
63
+ if (!cacheControl) return false;
64
+ const directives = cacheControl.toLowerCase();
65
+ return !directives.includes("no-store") && !directives.includes("private");
66
+ }
67
+
68
+ /**
69
+ * Copies better-auth's refreshed cookies onto the response the route produced.
70
+ *
71
+ * Two things are deliberately skipped:
72
+ *
73
+ * - **Shared-cacheable responses.** `/media/*` answers with
74
+ * `public, max-age=31536000, immutable`; a session cookie riding along on
75
+ * that response is a session handed to whoever the CDN serves next.
76
+ * `Cache-Control: public` is this codebase's explicit marker for
77
+ * "auth-invariant, safe to store" (see `middleware/cache-control.ts`), so it
78
+ * is exactly the signal to stay out of.
79
+ * - **Cookie names the route already set.** `/signout` clears the session
80
+ * cookie after this middleware read (and possibly refreshed) it. Appending
81
+ * ours last would land after the route's deletion and resurrect the session.
82
+ */
83
+ function applyAuthCookies(
84
+ c: Context<Env>,
85
+ authHeaders: Headers | null | undefined,
86
+ ): void {
87
+ const refreshed = authHeaders?.getSetCookie() ?? [];
88
+ if (refreshed.length === 0) return;
89
+ if (isSharedCacheable(c.res.headers.get("Cache-Control"))) return;
90
+
91
+ const alreadySet = new Set(c.res.headers.getSetCookie().map(readCookieName));
92
+ for (const cookie of refreshed) {
93
+ if (alreadySet.has(readCookieName(cookie))) continue;
94
+ c.res.headers.append("Set-Cookie", cookie);
95
+ }
96
+ }
97
+
22
98
  export function attachSession(): MiddlewareHandler<Env> {
23
99
  return async (c, next) => {
24
- try {
25
- const session = await c.var.auth.api.getSession({
100
+ const read = () =>
101
+ c.var.auth.api.getSession({
26
102
  headers: c.req.raw.headers,
103
+ returnHeaders: true,
27
104
  });
28
- c.set("session", session ?? null);
29
- c.set("isAuthenticated", !!session?.user);
105
+
106
+ let authHeaders: Headers | null = null;
107
+ try {
108
+ // One retry: a blipping database read would otherwise present to the
109
+ // user as a logout, and `requireAuth` would send them back to sign-in
110
+ // with a session that was valid the whole time.
111
+ const { headers, response } = await read().catch(() => read());
112
+ authHeaders = headers;
113
+ c.set("session", response ?? null);
114
+ c.set("isAuthenticated", !!response?.user);
30
115
  } catch {
31
116
  c.set("session", null);
32
117
  c.set("isAuthenticated", false);
33
118
  }
119
+
34
120
  await next();
121
+
122
+ applyAuthCookies(c, authHeaders);
35
123
  };
36
124
  }
@@ -1,27 +1,25 @@
1
1
  import { afterEach, describe, expect, it, vi } from "vitest";
2
2
 
3
+ const MIGRATE_ENV_KEYS = [
4
+ "DATABASE_URL",
5
+ "DATA_DIR",
6
+ "JANT_DEBUG_MIGRATE",
7
+ "JANT_ENV_FILE",
8
+ ] as const;
9
+
3
10
  describe("jant migrate", () => {
4
- const originalDatabaseUrl = process.env.DATABASE_URL;
5
- const originalDataDir = process.env.DATA_DIR;
6
- const originalDebugFlag = process.env.JANT_DEBUG_MIGRATE;
11
+ const originalEnv = Object.fromEntries(
12
+ MIGRATE_ENV_KEYS.map((key) => [key, process.env[key]]),
13
+ ) as Record<(typeof MIGRATE_ENV_KEYS)[number], string | undefined>;
7
14
 
8
15
  afterEach(() => {
9
- if (originalDatabaseUrl === undefined) {
10
- delete process.env.DATABASE_URL;
11
- } else {
12
- process.env.DATABASE_URL = originalDatabaseUrl;
13
- }
14
-
15
- if (originalDataDir === undefined) {
16
- delete process.env.DATA_DIR;
17
- } else {
18
- process.env.DATA_DIR = originalDataDir;
19
- }
20
-
21
- if (originalDebugFlag === undefined) {
22
- delete process.env.JANT_DEBUG_MIGRATE;
23
- } else {
24
- process.env.JANT_DEBUG_MIGRATE = originalDebugFlag;
16
+ for (const key of MIGRATE_ENV_KEYS) {
17
+ const value = originalEnv[key];
18
+ if (value === undefined) {
19
+ delete process.env[key];
20
+ } else {
21
+ process.env[key] = value;
22
+ }
25
23
  }
26
24
 
27
25
  vi.restoreAllMocks();
@@ -33,6 +31,10 @@ describe("jant migrate", () => {
33
31
  "postgres://shell_user:super-secret@db.example.com:5432/shell_db";
34
32
  delete process.env.DATA_DIR;
35
33
  process.env.JANT_DEBUG_MIGRATE = "1";
34
+ // This test is about the auto-located `.env.node` itself, so it opts back
35
+ // into the search the suite disables in `vitest.config.ts`. `node:fs` is
36
+ // mocked below, so the file it finds is the mock's content, not a real one.
37
+ delete process.env.JANT_ENV_FILE;
36
38
 
37
39
  const migrate = vi.fn(async () => {});
38
40
  const poolQuery = vi