@jant/core 0.6.16 → 0.7.1

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 (592) hide show
  1. package/README.md +11 -11
  2. package/bin/commands/assets/upload.js +192 -15
  3. package/bin/commands/export.js +3 -1
  4. package/bin/commands/import-site.js +749 -220
  5. package/bin/commands/reset-password.js +21 -8
  6. package/bin/commands/setup.js +136 -0
  7. package/bin/commands/site/export.js +71 -34
  8. package/bin/commands/site/pull-media.js +2 -6
  9. package/bin/commands/site/snapshot/export.js +19 -61
  10. package/bin/commands/site/snapshot/import.js +23 -24
  11. package/bin/lib/d1-query.js +92 -18
  12. package/bin/lib/hugo-markdown.js +4 -0
  13. package/bin/lib/node-env.js +40 -4
  14. package/bin/lib/site-pull-media.js +21 -28
  15. package/bin/lib/site-selection.js +94 -29
  16. package/bin/lib/site-snapshot.js +338 -3
  17. package/bin/lib/sql-export.js +68 -5
  18. package/bin/lib/wrangler-cli.js +37 -2
  19. package/bin/lib/wrangler-json.js +35 -0
  20. package/bin/lib/zip-archive.js +187 -0
  21. package/dist/{app-C7_4oab9.js → app-ZcaI1kPN.js} +53190 -13529
  22. package/dist/client/.vite/manifest.json +255 -90
  23. package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
  24. package/dist/client/_assets/chunks/create-editor-B-m7X7S5.js +261 -0
  25. package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
  26. package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
  27. package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
  28. package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
  29. package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
  30. package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
  31. package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
  32. package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
  33. package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
  34. package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
  35. package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
  36. package/dist/client/_assets/chunks/sortable-list-BJyd-LXE.js +2 -0
  37. package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
  38. package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
  39. package/dist/client/_assets/chunks/unsafe-svg-BkscJx69.js +2 -0
  40. package/dist/client/_assets/client-DAhoqPdr.css +2 -0
  41. package/dist/client/_assets/client-auth-CAMfTrKW.js +828 -0
  42. package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
  43. package/dist/client/_assets/client-compose-D6rExKVK.js +2004 -0
  44. package/dist/client/_assets/client-knSEyUJO.js +295 -0
  45. package/dist/client/_assets/client-manage-sVOkqVbH.js +2170 -0
  46. package/dist/client/_assets/client-settings-CGR_nZ78.js +1112 -0
  47. package/dist/github-sync-Gw4orAHk.js +8398 -0
  48. package/dist/index.js +2 -5
  49. package/dist/node.js +11 -9
  50. package/package.json +15 -5
  51. package/src/__tests__/auth-session-freshness.test.ts +57 -0
  52. package/src/__tests__/basecoat-variants.test.ts +121 -0
  53. package/src/__tests__/bin/assets-upload.test.ts +68 -0
  54. package/src/__tests__/dev-scripts.test.ts +203 -0
  55. package/src/__tests__/export-collection-order.test.ts +276 -0
  56. package/src/__tests__/export-feed-ids.test.ts +184 -0
  57. package/src/__tests__/export-feed-order.test.ts +294 -0
  58. package/src/__tests__/export-hugo-build.test.ts +390 -0
  59. package/src/__tests__/export-import-roundtrip.test.ts +94 -0
  60. package/src/__tests__/export-service.test.ts +842 -28
  61. package/src/__tests__/export-smart-collection.test.ts +329 -0
  62. package/src/__tests__/helpers/component-css.ts +91 -0
  63. package/src/__tests__/helpers/db.ts +33 -0
  64. package/src/__tests__/helpers/export-fixtures.ts +3 -0
  65. package/src/__tests__/helpers/hugo-site.ts +146 -0
  66. package/src/__tests__/import-site-command.test.ts +487 -1
  67. package/src/__tests__/mise-config.test.ts +75 -4
  68. package/src/__tests__/node-dev-tasks.test.ts +264 -0
  69. package/src/__tests__/site-export-canonical-import.test.ts +149 -0
  70. package/src/__tests__/snapshot-canonical-replay.test.ts +179 -0
  71. package/src/__tests__/snapshot-settings.test.ts +97 -0
  72. package/src/__tests__/snapshot-tables.test.ts +219 -0
  73. package/src/__tests__/sql-export.test.ts +173 -0
  74. package/src/__tests__/stylesheet-audience.test.ts +159 -0
  75. package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
  76. package/src/__tests__/zip-archive.test.ts +106 -0
  77. package/src/app.tsx +86 -22
  78. package/src/auth.ts +18 -1
  79. package/src/client/__tests__/collection-page-actions.test.ts +51 -2
  80. package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
  81. package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
  82. package/src/client/__tests__/compose-bridge.test.ts +272 -0
  83. package/src/client/__tests__/compose-launch.test.ts +62 -1
  84. package/src/client/__tests__/compose-shortcuts.test.ts +38 -9
  85. package/src/client/__tests__/compose-triggers.test.ts +90 -0
  86. package/src/client/__tests__/copy-field.test.ts +162 -0
  87. package/src/client/__tests__/custom-url-menu.test.ts +127 -6
  88. package/src/client/__tests__/image-processor.test.ts +324 -2
  89. package/src/client/__tests__/lazy-entries.test.ts +92 -0
  90. package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
  91. package/src/client/__tests__/runtime-paths.test.ts +84 -0
  92. package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
  93. package/src/client/__tests__/toast.test.ts +23 -0
  94. package/src/client/__tests__/video-processor-probe.test.ts +175 -0
  95. package/src/client/__tests__/video-processor.test.ts +181 -0
  96. package/src/client/audio-processor.ts +24 -15
  97. package/src/client/audio-waveform.ts +49 -0
  98. package/src/client/collection-dialog-host.ts +72 -0
  99. package/src/client/collection-navigation.ts +51 -16
  100. package/src/client/collection-page-actions.ts +26 -0
  101. package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
  102. package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
  103. package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
  104. package/src/client/components/__tests__/jant-command-palette.test.ts +28 -0
  105. package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
  106. package/src/client/components/__tests__/jant-compose-dialog.test.ts +1250 -53
  107. package/src/client/components/__tests__/jant-compose-editor.test.ts +544 -40
  108. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +0 -1
  109. package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
  110. package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
  111. package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
  112. package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
  113. package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
  114. package/src/client/components/__tests__/jant-settings-general.test.ts +561 -172
  115. package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
  116. package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
  117. package/src/client/components/collection-dialog-types.ts +49 -0
  118. package/src/client/components/collection-manager-types.ts +32 -1
  119. package/src/client/components/collection-types.ts +1 -15
  120. package/src/client/components/compose-types.ts +75 -1
  121. package/src/client/components/jant-collection-dialog.ts +722 -0
  122. package/src/client/components/jant-collection-directory.ts +567 -182
  123. package/src/client/components/jant-collection-form.ts +53 -232
  124. package/src/client/components/jant-command-palette.ts +100 -49
  125. package/src/client/components/jant-compose-dialog.ts +1486 -707
  126. package/src/client/components/jant-compose-editor.ts +673 -336
  127. package/src/client/components/jant-compose-fullscreen.ts +51 -43
  128. package/src/client/components/jant-config-editor.ts +65 -53
  129. package/src/client/components/jant-confirm-dialog.ts +13 -11
  130. package/src/client/components/jant-locale-picker.ts +418 -0
  131. package/src/client/components/jant-media-lightbox.ts +146 -136
  132. package/src/client/components/jant-nav-manager.ts +690 -366
  133. package/src/client/components/jant-post-menu.ts +1086 -282
  134. package/src/client/components/jant-post-picker.ts +324 -0
  135. package/src/client/components/jant-repo-picker-types.ts +6 -1
  136. package/src/client/components/jant-repo-picker.ts +137 -108
  137. package/src/client/components/jant-settings-avatar.ts +31 -27
  138. package/src/client/components/jant-settings-general.ts +622 -543
  139. package/src/client/components/jant-settings-language.ts +1087 -0
  140. package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
  141. package/src/client/components/jant-text-preview.ts +94 -86
  142. package/src/client/components/nav-manager-types.ts +62 -1
  143. package/src/client/components/settings-types.ts +14 -29
  144. package/src/client/components/smart-collection-conditions.ts +285 -0
  145. package/src/client/components/smart-collection-dialog-types.ts +68 -0
  146. package/src/client/compose-bridge.ts +214 -141
  147. package/src/client/compose-launch.ts +76 -2
  148. package/src/client/compose-shortcuts.ts +6 -8
  149. package/src/client/compose-triggers.ts +50 -0
  150. package/src/client/copy-field.ts +108 -0
  151. package/src/client/custom-url-menu.ts +60 -4
  152. package/src/client/icons.ts +14 -0
  153. package/src/client/image-plan.ts +165 -0
  154. package/src/client/image-processor.ts +195 -117
  155. package/src/client/image-worker-client.ts +45 -0
  156. package/src/client/image-worker.ts +135 -0
  157. package/src/client/lazy-entries.ts +95 -0
  158. package/src/client/lazy-slugify.ts +2 -2
  159. package/src/client/media-scroll-hint.ts +43 -2
  160. package/src/client/mediabunny.ts +29 -0
  161. package/src/client/post-picker.ts +41 -0
  162. package/src/client/runtime-paths.ts +74 -1
  163. package/src/client/settings-bridge.ts +4 -10
  164. package/src/client/site-header-nav.js +18 -6
  165. package/src/client/smart-collection-dialog-host.ts +116 -0
  166. package/src/client/smart-collection-page-actions.ts +208 -0
  167. package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
  168. package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
  169. package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
  170. package/src/client/tiptap/__tests__/list-editing.test.ts +224 -86
  171. package/src/client/tiptap/__tests__/mark-exit.test.ts +1 -2
  172. package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +26 -0
  173. package/src/client/tiptap/bubble-menu.ts +47 -16
  174. package/src/client/tiptap/extensions.ts +0 -3
  175. package/src/client/tiptap/floating-position.ts +216 -2
  176. package/src/client/tiptap/inline-image-upload.ts +7 -3
  177. package/src/client/tiptap/link-toolbar.ts +22 -9
  178. package/src/client/tiptap/structural-keymap.ts +158 -47
  179. package/src/client/toast.ts +13 -0
  180. package/src/client/types/vite-worker.d.ts +12 -0
  181. package/src/client/upload-with-metadata.ts +3 -13
  182. package/src/client/video-processor.ts +351 -107
  183. package/src/client-auth.ts +14 -31
  184. package/src/client-compose.ts +19 -0
  185. package/src/client-manage.ts +13 -0
  186. package/src/client-settings.ts +14 -0
  187. package/src/client-site.ts +10 -3
  188. package/src/client.ts +4 -0
  189. package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
  190. package/src/db/__tests__/d1-query.test.ts +56 -1
  191. package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
  192. package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
  193. package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
  194. package/src/db/migrations/0032_married_tomas.sql +3 -0
  195. package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
  196. package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
  197. package/src/db/migrations/meta/0032_snapshot.json +2582 -0
  198. package/src/db/migrations/meta/0033_snapshot.json +2816 -0
  199. package/src/db/migrations/meta/0034_snapshot.json +2816 -0
  200. package/src/db/migrations/meta/_journal.json +21 -0
  201. package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
  202. package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
  203. package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
  204. package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
  205. package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
  206. package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
  207. package/src/db/migrations/pg/meta/_journal.json +21 -0
  208. package/src/db/pg/schema.ts +195 -27
  209. package/src/db/post-visibility.ts +117 -0
  210. package/src/db/schema.ts +222 -27
  211. package/src/i18n/__tests__/detect.test.ts +26 -81
  212. package/src/i18n/__tests__/supported-locales.test.ts +83 -0
  213. package/src/i18n/context.tsx +12 -2
  214. package/src/i18n/detect.ts +7 -66
  215. package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
  216. package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
  217. package/src/i18n/locales/public/en.po +490 -158
  218. package/src/i18n/locales/public/en.ts +1 -1
  219. package/src/i18n/locales/public/zh-Hans.po +487 -155
  220. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  221. package/src/i18n/locales/public/zh-Hant.po +487 -155
  222. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  223. package/src/i18n/locales/settings/en.po +555 -73
  224. package/src/i18n/locales/settings/en.ts +1 -1
  225. package/src/i18n/locales/settings/zh-Hans.po +601 -119
  226. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  227. package/src/i18n/locales/settings/zh-Hant.po +614 -132
  228. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  229. package/src/i18n/locales.ts +94 -0
  230. package/src/i18n/middleware.ts +12 -2
  231. package/src/i18n/supported-locales.ts +80 -0
  232. package/src/lib/__tests__/api-settings.test.ts +11 -2
  233. package/src/lib/__tests__/asset-path.test.ts +92 -0
  234. package/src/lib/__tests__/deferred.test.ts +90 -0
  235. package/src/lib/__tests__/discover-ping.test.ts +89 -0
  236. package/src/lib/__tests__/discover.test.ts +410 -0
  237. package/src/lib/__tests__/feed-policy.test.ts +26 -0
  238. package/src/lib/__tests__/feed.test.ts +1526 -15
  239. package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
  240. package/src/lib/__tests__/github-sync-repo-name.test.ts +40 -0
  241. package/src/lib/__tests__/hosted-signin.test.ts +1 -1
  242. package/src/lib/__tests__/http-cache.test.ts +162 -0
  243. package/src/lib/__tests__/image.test.ts +27 -1
  244. package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
  245. package/src/lib/__tests__/jant-docs.test.ts +23 -0
  246. package/src/lib/__tests__/lang-detect.test.ts +231 -0
  247. package/src/lib/__tests__/markdown-to-tiptap.test.ts +105 -0
  248. package/src/lib/__tests__/markdown.test.ts +10 -0
  249. package/src/lib/__tests__/media-helpers.test.ts +81 -0
  250. package/src/lib/__tests__/navigation.test.ts +135 -1
  251. package/src/lib/__tests__/resolve-config.test.ts +131 -3
  252. package/src/lib/__tests__/schemas.test.ts +69 -5
  253. package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
  254. package/src/lib/__tests__/slug.test.ts +70 -0
  255. package/src/lib/__tests__/slugify.test.ts +85 -0
  256. package/src/lib/__tests__/storage.test.ts +32 -0
  257. package/src/lib/__tests__/thread-fold.test.ts +113 -0
  258. package/src/lib/__tests__/timeline.test.ts +87 -0
  259. package/src/lib/__tests__/tiptap-to-markdown.test.ts +172 -4
  260. package/src/lib/__tests__/upload-sideload.test.ts +14 -0
  261. package/src/lib/__tests__/url-fetch.test.ts +1 -2
  262. package/src/lib/__tests__/url.test.ts +78 -72
  263. package/src/lib/__tests__/view-language.test.ts +413 -0
  264. package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
  265. package/src/lib/address.ts +57 -0
  266. package/src/lib/api-posts.ts +12 -0
  267. package/src/lib/asset-path.ts +98 -1
  268. package/src/lib/build-env.ts +16 -0
  269. package/src/lib/collection-groups.ts +5 -1
  270. package/src/lib/collection-paths.ts +3 -10
  271. package/src/lib/constants.ts +28 -3
  272. package/src/lib/copy-field.ts +36 -0
  273. package/src/lib/crypto.ts +40 -0
  274. package/src/lib/csp-builder.ts +2 -1
  275. package/src/lib/deferred.ts +79 -0
  276. package/src/lib/discover-ping.ts +141 -0
  277. package/src/lib/discover.ts +439 -0
  278. package/src/lib/env.ts +134 -0
  279. package/src/lib/errors.ts +33 -0
  280. package/src/lib/feed-policy.ts +236 -2
  281. package/src/lib/feed.ts +926 -73
  282. package/src/lib/filter-dimensions.ts +1349 -0
  283. package/src/lib/github-sync-repo-name.ts +45 -0
  284. package/src/lib/github-sync-site-config.ts +3 -0
  285. package/src/lib/hosted-signin.ts +1 -1
  286. package/src/lib/http-cache.ts +148 -0
  287. package/src/lib/hugo-markdown.ts +62 -0
  288. package/src/lib/icons.ts +9 -2
  289. package/src/lib/ids.ts +3 -0
  290. package/src/lib/image.ts +17 -4
  291. package/src/lib/jant-docs.ts +42 -0
  292. package/src/lib/lang-detect.ts +308 -0
  293. package/src/lib/markdown-manager.ts +392 -2
  294. package/src/lib/media-helpers.ts +13 -1
  295. package/src/lib/navigation.ts +78 -11
  296. package/src/lib/pagination.ts +28 -24
  297. package/src/lib/per-language-surfaces.ts +67 -0
  298. package/src/lib/post-body-html.ts +11 -4
  299. package/src/lib/post-meta.ts +19 -0
  300. package/src/lib/render.tsx +55 -7
  301. package/src/lib/resolve-config.ts +111 -6
  302. package/src/lib/schemas.ts +251 -15
  303. package/src/lib/site-header-fragment.tsx +2 -1
  304. package/src/lib/site-unavailable-page.ts +47 -0
  305. package/src/lib/slug.ts +51 -7
  306. package/src/lib/slugify.ts +113 -0
  307. package/src/lib/startup-config.ts +4 -6
  308. package/src/lib/storage.ts +7 -0
  309. package/src/lib/summary.ts +50 -0
  310. package/src/lib/thread-fold.ts +161 -0
  311. package/src/lib/time.ts +32 -0
  312. package/src/lib/timeline.ts +43 -2
  313. package/src/lib/tiptap-render.ts +1 -2
  314. package/src/lib/tiptap-to-markdown.ts +13 -7
  315. package/src/lib/upload.ts +16 -13
  316. package/src/lib/url.ts +159 -86
  317. package/src/lib/version.ts +10 -8
  318. package/src/lib/view-language.ts +436 -0
  319. package/src/lib/view.ts +53 -25
  320. package/src/lib/viewer-context.ts +62 -0
  321. package/src/middleware/__tests__/auth.test.ts +70 -2
  322. package/src/middleware/__tests__/cache-control.test.ts +71 -3
  323. package/src/middleware/__tests__/onboarding.test.ts +76 -5
  324. package/src/middleware/__tests__/secure-headers.test.ts +1 -1
  325. package/src/middleware/__tests__/session.test.ts +181 -33
  326. package/src/middleware/auth.ts +50 -31
  327. package/src/middleware/cache-control.ts +75 -10
  328. package/src/middleware/config.ts +6 -6
  329. package/src/middleware/error-handler.ts +12 -0
  330. package/src/middleware/onboarding.ts +32 -10
  331. package/src/middleware/secure-headers.ts +1 -1
  332. package/src/middleware/session.ts +96 -8
  333. package/src/node/__tests__/cli-migrate.test.ts +21 -19
  334. package/src/node/__tests__/cli-reset-password.test.ts +81 -8
  335. package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
  336. package/src/node/__tests__/cli-setup.test.ts +163 -0
  337. package/src/node/__tests__/cli-site-selection.test.ts +128 -38
  338. package/src/node/__tests__/cli-site-snapshot.test.ts +67 -0
  339. package/src/node/__tests__/cli-snapshot-meta.test.ts +20 -0
  340. package/src/node/__tests__/runtime.test.ts +75 -0
  341. package/src/node/index.ts +3 -0
  342. package/src/node/request-handler.ts +3 -1
  343. package/src/preset.css +76 -22
  344. package/src/routes/__tests__/feed-validator.test.ts +156 -0
  345. package/src/routes/api/__tests__/collections.test.ts +114 -0
  346. package/src/routes/api/__tests__/discover.test.ts +222 -0
  347. package/src/routes/api/__tests__/nav-items.test.ts +212 -2
  348. package/src/routes/api/__tests__/post-translations.test.ts +829 -0
  349. package/src/routes/api/__tests__/posts.test.ts +24 -0
  350. package/src/routes/api/__tests__/settings.test.ts +24 -3
  351. package/src/routes/api/__tests__/telegram.test.ts +6 -5
  352. package/src/routes/api/__tests__/upload.test.ts +34 -0
  353. package/src/routes/api/collections.ts +49 -6
  354. package/src/routes/api/custom-urls.ts +0 -1
  355. package/src/routes/api/discover.ts +95 -0
  356. package/src/routes/api/export.ts +6 -3
  357. package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
  358. package/src/routes/api/internal/sites.ts +0 -1
  359. package/src/routes/api/nav-items.ts +55 -8
  360. package/src/routes/api/posts.ts +161 -0
  361. package/src/routes/api/public/__tests__/archive.test.ts +111 -0
  362. package/src/routes/api/public/archive.ts +77 -81
  363. package/src/routes/api/public/posts.ts +44 -7
  364. package/src/routes/api/smart-collections.ts +132 -0
  365. package/src/routes/api/telegram.ts +2 -40
  366. package/src/routes/api/upload.ts +10 -3
  367. package/src/routes/api/uploads.ts +1 -2
  368. package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
  369. package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
  370. package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
  371. package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
  372. package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
  373. package/src/routes/auth/__tests__/setup.test.ts +278 -26
  374. package/src/routes/auth/__tests__/signin.test.ts +135 -0
  375. package/src/routes/auth/setup.tsx +934 -186
  376. package/src/routes/auth/signin.tsx +10 -8
  377. package/src/routes/compose.tsx +9 -0
  378. package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
  379. package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
  380. package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
  381. package/src/routes/dash/__tests__/github-sync-app.test.ts +365 -0
  382. package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
  383. package/src/routes/dash/custom-urls.tsx +1 -37
  384. package/src/routes/dash/settings.tsx +714 -111
  385. package/src/routes/discover-announce.ts +96 -0
  386. package/src/routes/feed/__tests__/feed.test.ts +165 -2
  387. package/src/routes/feed/feed.ts +69 -40
  388. package/src/routes/feed/sitemap.ts +95 -11
  389. package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
  390. package/src/routes/pages/__tests__/archive-params.test.ts +370 -6
  391. package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
  392. package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
  393. package/src/routes/pages/__tests__/collections.test.ts +9 -3
  394. package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
  395. package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
  396. package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
  397. package/src/routes/pages/__tests__/post-page-round-trips.test.ts +211 -0
  398. package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
  399. package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
  400. package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
  401. package/src/routes/pages/__tests__/thread-order.test.ts +179 -0
  402. package/src/routes/pages/archive.tsx +487 -379
  403. package/src/routes/pages/collection.tsx +94 -96
  404. package/src/routes/pages/collections.tsx +38 -50
  405. package/src/routes/pages/featured.tsx +43 -18
  406. package/src/routes/pages/home.tsx +25 -6
  407. package/src/routes/pages/language.tsx +113 -0
  408. package/src/routes/pages/latest.tsx +26 -16
  409. package/src/routes/pages/new.tsx +3 -0
  410. package/src/routes/pages/page.tsx +228 -45
  411. package/src/routes/pages/partials.tsx +41 -0
  412. package/src/routes/pages/search.tsx +22 -5
  413. package/src/routes/pages/smart-collection.tsx +340 -0
  414. package/src/routes/pages/subscribe.tsx +182 -0
  415. package/src/runtime/__tests__/index.test.ts +19 -0
  416. package/src/runtime/__tests__/node.test.ts +129 -4
  417. package/src/runtime/__tests__/readiness.test.ts +23 -0
  418. package/src/runtime/cloudflare.ts +21 -2
  419. package/src/runtime/index.ts +27 -1
  420. package/src/runtime/node.ts +67 -1
  421. package/src/runtime/readiness.ts +15 -0
  422. package/src/runtime/site.ts +95 -24
  423. package/src/services/__tests__/auth.test.ts +116 -7
  424. package/src/services/__tests__/bootstrap-setup-instance.test.ts +230 -0
  425. package/src/services/__tests__/collection.test.ts +108 -4
  426. package/src/services/__tests__/custom-url.test.ts +34 -0
  427. package/src/services/__tests__/github-app-installations.test.ts +153 -0
  428. package/src/services/__tests__/github-sync-push.test.ts +46 -0
  429. package/src/services/__tests__/language.test.ts +495 -0
  430. package/src/services/__tests__/media.test.ts +31 -0
  431. package/src/services/__tests__/navigation.test.ts +213 -10
  432. package/src/services/__tests__/path.test.ts +236 -0
  433. package/src/services/__tests__/post-language.test.ts +830 -0
  434. package/src/services/__tests__/post-timeline.test.ts +127 -0
  435. package/src/services/__tests__/post.test.ts +120 -9
  436. package/src/services/__tests__/settings.test.ts +228 -17
  437. package/src/services/__tests__/site-admin.test.ts +5 -5
  438. package/src/services/__tests__/smart-collection.test.ts +651 -0
  439. package/src/services/about-page.ts +22 -1
  440. package/src/services/auth.ts +51 -49
  441. package/src/services/bootstrap.ts +408 -39
  442. package/src/services/collection-directory-position.ts +76 -0
  443. package/src/services/collection.ts +331 -46
  444. package/src/services/custom-url.ts +29 -24
  445. package/src/services/export-theme/assets/client-site.css +1 -1
  446. package/src/services/export-theme/assets/client-site.js +123 -123
  447. package/src/services/export-theme/layouts/_default/alias.html +27 -1
  448. package/src/services/export-theme/layouts/_default/list.html +2 -63
  449. package/src/services/export-theme/layouts/_default/rss.xml +103 -38
  450. package/src/services/export-theme/layouts/collections/list.html +3 -3
  451. package/src/services/export-theme/layouts/featured/list.html +1 -4
  452. package/src/services/export-theme/layouts/index.html +40 -26
  453. package/src/services/export-theme/layouts/partials/collection-members.html +100 -0
  454. package/src/services/export-theme/layouts/partials/collection-threads.html +33 -0
  455. package/src/services/export-theme/layouts/partials/featured-members.html +35 -0
  456. package/src/services/export-theme/layouts/partials/featured-thread.html +1 -1
  457. package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
  458. package/src/services/export-theme/layouts/partials/footer.html +1 -1
  459. package/src/services/export-theme/layouts/partials/head.html +1 -1
  460. package/src/services/export-theme/layouts/partials/header.html +5 -3
  461. package/src/services/export-theme/layouts/partials/jant-data.html +23 -0
  462. package/src/services/export-theme/layouts/partials/latest-members.html +48 -0
  463. package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
  464. package/src/services/export-theme/layouts/partials/pagination.html +14 -13
  465. package/src/services/export-theme/layouts/partials/post-card.html +8 -4
  466. package/src/services/export-theme/layouts/partials/reply.html +2 -1
  467. package/src/services/export-theme/layouts/partials/smart-collection-members.html +129 -0
  468. package/src/services/export-theme/layouts/partials/thread-preview.html +41 -31
  469. package/src/services/export-theme/layouts/post/list.html +1 -1
  470. package/src/services/export-theme/layouts/smart_collection/list.html +24 -0
  471. package/src/services/export-theme/styles/main.css +13 -2
  472. package/src/services/export-theme/theme.toml +1 -1
  473. package/src/services/export.ts +880 -72
  474. package/src/services/github-app-installations.ts +171 -4
  475. package/src/services/github-sync.ts +69 -15
  476. package/src/services/index.ts +34 -3
  477. package/src/services/language.ts +412 -0
  478. package/src/services/mcp.ts +22 -2
  479. package/src/services/media.ts +22 -7
  480. package/src/services/navigation.ts +429 -67
  481. package/src/services/path.ts +254 -6
  482. package/src/services/post.ts +1234 -190
  483. package/src/services/search.ts +39 -2
  484. package/src/services/settings.ts +238 -43
  485. package/src/services/site-admin.ts +27 -14
  486. package/src/services/site.ts +160 -32
  487. package/src/services/smart-collection.ts +636 -0
  488. package/src/services/telegram.ts +20 -22
  489. package/src/services/upload-session.ts +1 -3
  490. package/src/style-author.css +27 -0
  491. package/src/styles/components-author.css +1622 -0
  492. package/src/styles/components.css +24 -1571
  493. package/src/styles/site-media.css +16 -2
  494. package/src/styles/tokens.css +82 -12
  495. package/src/styles/ui-author.css +7602 -0
  496. package/src/styles/ui.css +1090 -7216
  497. package/src/types/app-context.ts +23 -0
  498. package/src/types/bindings.ts +8 -0
  499. package/src/types/config.ts +94 -15
  500. package/src/types/constants.ts +188 -16
  501. package/src/types/entities.ts +87 -1
  502. package/src/types/operations.ts +74 -2
  503. package/src/types/props.ts +157 -31
  504. package/src/types/views.ts +55 -1
  505. package/src/ui/__tests__/font-themes.test.ts +52 -12
  506. package/src/ui/compose/ComposeDialog.tsx +201 -7
  507. package/src/ui/compose/ComposePrompt.tsx +1 -5
  508. package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
  509. package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
  510. package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
  511. package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
  512. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
  513. package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
  514. package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
  515. package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
  516. package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
  517. package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
  518. package/src/ui/dash/settings/GeneralContent.tsx +277 -113
  519. package/src/ui/dash/settings/LanguageContent.tsx +409 -0
  520. package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
  521. package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
  522. package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
  523. package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
  524. package/src/ui/dash/settings/discover-copy.ts +116 -0
  525. package/src/ui/feed/LinkCard.tsx +55 -45
  526. package/src/ui/feed/NoteCard.tsx +9 -4
  527. package/src/ui/feed/PostStatusBadges.tsx +90 -43
  528. package/src/ui/feed/QuoteCard.tsx +55 -30
  529. package/src/ui/feed/ThreadPreview.tsx +6 -2
  530. package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
  531. package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
  532. package/src/ui/feed/thread-preview-state.ts +20 -6
  533. package/src/ui/font-themes.ts +39 -29
  534. package/src/ui/layouts/BaseLayout.tsx +155 -10
  535. package/src/ui/layouts/SiteLayout.tsx +192 -34
  536. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
  537. package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
  538. package/src/ui/pages/ArchivePage.tsx +282 -289
  539. package/src/ui/pages/CollectionPage.tsx +86 -76
  540. package/src/ui/pages/CollectionsPage.tsx +5 -0
  541. package/src/ui/pages/ComposePage.tsx +8 -0
  542. package/src/ui/pages/PostPage.tsx +47 -0
  543. package/src/ui/pages/SearchPage.tsx +3 -3
  544. package/src/ui/pages/SmartCollectionPage.tsx +434 -0
  545. package/src/ui/pages/SubscribePage.tsx +236 -0
  546. package/src/ui/pages/__tests__/ArchivePage.test.tsx +65 -9
  547. package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
  548. package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
  549. package/src/ui/shared/CollectionDirectory.tsx +178 -3
  550. package/src/ui/shared/CollectionsManager.tsx +70 -17
  551. package/src/ui/shared/CopyField.tsx +127 -0
  552. package/src/ui/shared/EmptyPostContent.tsx +23 -0
  553. package/src/ui/shared/MediaGallery.tsx +125 -46
  554. package/src/ui/shared/Pagination.tsx +20 -5
  555. package/src/ui/shared/PostFooter.tsx +20 -11
  556. package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
  557. package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
  558. package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
  559. package/src/ui/shared/__tests__/pagination.test.ts +38 -24
  560. package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
  561. package/src/ui/shared/archive-labels.ts +98 -0
  562. package/src/ui/shared/collection-management-labels.ts +94 -16
  563. package/src/ui/shared/navigation-labels.ts +45 -71
  564. package/src/ui/shared/post-article-attributes.ts +2 -0
  565. package/src/ui/shared/smart-collection-labels.ts +343 -0
  566. package/dist/app-C75Zc8dD.js +0 -6
  567. package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
  568. package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
  569. package/dist/client/_assets/chunks/url-CU9pEbk5.js +0 -1
  570. package/dist/client/_assets/client-Bq0vre8Y.js +0 -296
  571. package/dist/client/_assets/client-D3VZyG4L.css +0 -2
  572. package/dist/client/_assets/client-auth-F2rb9WfF.js +0 -5571
  573. package/dist/env-BPYViDJJ.js +0 -281
  574. package/dist/export-Dl5KCiEs.js +0 -5562
  575. package/dist/github-api-BNF35Lkx.js +0 -176
  576. package/dist/github-app-DdiD-bC4.js +0 -275
  577. package/dist/github-sync-Bqg62IvV.js +0 -4
  578. package/dist/github-sync-HdK2EgvJ.js +0 -431
  579. package/dist/url-CbLAtlZe.js +0 -900
  580. package/src/client/__tests__/collection-created-notice.test.ts +0 -36
  581. package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
  582. package/src/client/__tests__/post-form-bridge.test.ts +0 -82
  583. package/src/client/collection-created-notice.ts +0 -66
  584. package/src/client/collection-form-bridge.ts +0 -191
  585. package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
  586. package/src/client/components/jant-post-form.ts +0 -422
  587. package/src/client/components/post-form-template.ts +0 -352
  588. package/src/client/components/post-form-types.ts +0 -96
  589. package/src/client/media-metadata.ts +0 -247
  590. package/src/client/post-form-bridge.ts +0 -193
  591. package/src/client/tiptap/exitable-marks.ts +0 -73
  592. package/src/ui/pages/CollectionEditorPage.tsx +0 -175
@@ -36,13 +36,18 @@ function createTestHonoApp() {
36
36
  function createMockAuth(authenticated: boolean) {
37
37
  return {
38
38
  api: {
39
- getSession: async () =>
40
- authenticated
39
+ // `attachSession` reads with `returnHeaders: true` so it can write
40
+ // better-auth's refreshed cookies back onto the response, which puts the
41
+ // result in better-call's `{ headers, response }` shape.
42
+ getSession: async () => ({
43
+ headers: new Headers(),
44
+ response: authenticated
41
45
  ? {
42
46
  user: { id: "user-1", email: "test@test.com", name: "Test" },
43
47
  session: { id: "session-1" },
44
48
  }
45
49
  : null,
50
+ }),
46
51
  },
47
52
  } as AppVariables["auth"];
48
53
  }
@@ -200,9 +205,72 @@ describe("requireAuth", () => {
200
205
  expect(res.status).toBe(302);
201
206
  expect(res.headers.get("Location")).toBe("/login");
202
207
  });
208
+
209
+ it("surfaces a route handler's error instead of bouncing to signin", async () => {
210
+ const app = createTestHonoApp();
211
+ app.use("*", async (c, next) => {
212
+ c.set("auth", createMockAuth(true));
213
+ c.set("services", {
214
+ siteMembers: createMockSiteMembers(),
215
+ } as AppVariables["services"]);
216
+ await next();
217
+ });
218
+ app.use("*", attachSession());
219
+ app.get("/settings", requireAuth(), () => {
220
+ throw new Error("saving the post blew up");
221
+ });
222
+
223
+ // Rewriting this into a redirect reads to the author as being randomly
224
+ // signed out, and throws away whatever they were editing.
225
+ const res = await app.request("/settings", { redirect: "manual" });
226
+ expect(res.status).toBe(500);
227
+ });
228
+
229
+ it("surfaces a failed membership lookup instead of bouncing to signin", async () => {
230
+ const app = createTestHonoApp();
231
+ app.use("*", async (c, next) => {
232
+ c.set("auth", createMockAuth(true));
233
+ c.set("services", {
234
+ siteMembers: {
235
+ ...createMockSiteMembers(),
236
+ get: vi.fn(async () => {
237
+ throw new Error("database unavailable");
238
+ }),
239
+ },
240
+ } as unknown as AppVariables["services"]);
241
+ await next();
242
+ });
243
+ app.use("*", attachSession());
244
+ app.get("/settings", requireAuth(), (c) => c.text("Settings"));
245
+
246
+ // `siteMembers.get` resolves to `null` for "not a member", so a throw is
247
+ // always infrastructure — never a reason to claim the user is signed out.
248
+ const res = await app.request("/settings", { redirect: "manual" });
249
+ expect(res.status).toBe(500);
250
+ });
203
251
  });
204
252
 
205
253
  describe("requireAuthApi", () => {
254
+ it("surfaces a route handler's error instead of answering 401", async () => {
255
+ const app = createTestHonoApp();
256
+ app.use("*", async (c, next) => {
257
+ c.set("auth", createMockAuth(true));
258
+ c.set("services", {
259
+ siteMembers: createMockSiteMembers(),
260
+ } as AppVariables["services"]);
261
+ await next();
262
+ });
263
+ app.use("*", attachSession());
264
+ app.get("/api/data", requireAuthApi(), () => {
265
+ throw new Error("upstream failed");
266
+ });
267
+
268
+ // A 401 here would tell the client the login expired and send the author
269
+ // back to sign-in over what is really a server-side failure.
270
+ const res = await app.request("/api/data");
271
+ expect(res.status).toBe(500);
272
+ });
273
+
206
274
  it("allows authenticated requests via session", async () => {
207
275
  const app = createTestHonoApp();
208
276
  app.onError(errorHandler);
@@ -2,12 +2,18 @@ import { describe, expect, it } from "vitest";
2
2
  import { Hono } from "hono";
3
3
  import type { Bindings } from "../../types.js";
4
4
  import type { AppVariables } from "../../types/app-context.js";
5
- import { defaultCacheControl } from "../cache-control.js";
5
+ import { defaultCacheControl, noStore } from "../cache-control.js";
6
6
 
7
7
  type Env = { Bindings: Bindings; Variables: AppVariables };
8
8
 
9
- function buildApp(): Hono<Env> {
9
+ function buildApp(isAuthenticated = false): Hono<Env> {
10
10
  const app = new Hono<Env>();
11
+ // Stands in for `attachSession()`, which runs before this middleware and is
12
+ // what puts `isAuthenticated` on the context.
13
+ app.use("*", async (c, next) => {
14
+ c.set("isAuthenticated", isAuthenticated);
15
+ await next();
16
+ });
11
17
  app.use("*", defaultCacheControl());
12
18
 
13
19
  // Un-annotated dynamic page — the common case.
@@ -27,11 +33,31 @@ function buildApp(): Hono<Env> {
27
33
  }
28
34
 
29
35
  describe("defaultCacheControl", () => {
30
- it("defaults un-annotated responses to private, no-store", async () => {
36
+ it("lets an anonymous page stay in the browser's back/forward cache", async () => {
37
+ // `no-store` would disable it, making the back button re-request and
38
+ // re-render a page that is the same for everyone anyway. `private` is
39
+ // what keeps shared caches out, and it is still here.
31
40
  const response = await buildApp().request("/");
41
+ expect(response.headers.get("Cache-Control")).toBe("private, no-cache");
42
+ });
43
+
44
+ it("keeps a signed-in page out of every cache", async () => {
45
+ // The back/forward cache restores a snapshot without asking the server,
46
+ // so an authenticated view would survive signing out.
47
+ const response = await buildApp(true).request("/");
32
48
  expect(response.headers.get("Cache-Control")).toBe("private, no-store");
33
49
  });
34
50
 
51
+ it("never allows a shared cache to store a dynamic page", async () => {
52
+ for (const authed of [false, true]) {
53
+ const value =
54
+ (await buildApp(authed).request("/")).headers.get("Cache-Control") ??
55
+ "";
56
+ expect(value).toContain("private");
57
+ expect(value).not.toContain("public");
58
+ }
59
+ });
60
+
35
61
  it("leaves an explicit public cache policy untouched", async () => {
36
62
  const response = await buildApp().request("/feed");
37
63
  expect(response.headers.get("Cache-Control")).toBe("public, max-age=180");
@@ -45,6 +71,48 @@ describe("defaultCacheControl", () => {
45
71
  it("defaults not-found responses too", async () => {
46
72
  const response = await buildApp().request("/missing");
47
73
  expect(response.status).toBe(404);
74
+ expect(response.headers.get("Cache-Control")).toBe("private, no-cache");
75
+ });
76
+ });
77
+
78
+ describe("noStore", () => {
79
+ function buildCredentialApp(): Hono<Env> {
80
+ const app = new Hono<Env>();
81
+ app.use("*", async (c, next) => {
82
+ c.set("isAuthenticated", false);
83
+ await next();
84
+ });
85
+ app.use("*", defaultCacheControl());
86
+ // Mounted exactly as `app.tsx` does: after the default, before the route.
87
+ app.use("/reset", noStore());
88
+ app.use("/api/auth/*", noStore());
89
+
90
+ app.get("/reset", (c) => c.html("<form/>"));
91
+ // A terminal handler that never calls next(), like better-auth's.
92
+ app.all("/api/auth/*", (c) => c.json({ ok: true }));
93
+ app.get("/", (c) => c.html("<h1>home</h1>"));
94
+
95
+ return app;
96
+ }
97
+
98
+ it("keeps an anonymous credential page out of the browser's disk cache", async () => {
99
+ // The reset URL carries a one-time token, so the anonymous `no-cache`
100
+ // default — which lets the browser keep the page — is the wrong answer.
101
+ const response = await buildCredentialApp().request("/reset");
102
+ expect(response.headers.get("Cache-Control")).toBe("private, no-store");
103
+ });
104
+
105
+ it("still reaches a terminal handler that never calls next()", async () => {
106
+ // better-auth's `/api/auth/*` handler returns without yielding, so the
107
+ // middleware only runs if it was registered first.
108
+ const response = await buildCredentialApp().request(
109
+ "/api/auth/get-session",
110
+ );
48
111
  expect(response.headers.get("Cache-Control")).toBe("private, no-store");
49
112
  });
113
+
114
+ it("leaves every other anonymous page on the default", async () => {
115
+ const response = await buildCredentialApp().request("/");
116
+ expect(response.headers.get("Cache-Control")).toBe("private, no-cache");
117
+ });
50
118
  });
@@ -4,28 +4,42 @@ import { requireOnboarding, resetOnboardingCache } from "../onboarding.js";
4
4
  import type { Bindings } from "../../types.js";
5
5
  import type { AppVariables } from "../../types/app-context.js";
6
6
  import { DEFAULT_TEST_SITE_ID } from "../../__tests__/helpers/db.js";
7
+ import {
8
+ ONBOARDING_STATUS,
9
+ type OnboardingStatus,
10
+ } from "../../lib/constants.js";
7
11
 
8
12
  type Env = { Bindings: Bindings; Variables: AppVariables };
9
13
 
10
- function createMockServices(complete: boolean) {
14
+ function createMockServices(status: OnboardingStatus) {
11
15
  let callCount = 0;
12
16
  const services = {
13
17
  settings: {
14
- isOnboardingComplete: async () => {
18
+ getOnboardingStatus: async () => {
15
19
  callCount++;
16
- return complete;
20
+ return status;
17
21
  },
18
22
  },
19
23
  } as AppVariables["services"];
20
24
  return { services, getCallCount: () => callCount };
21
25
  }
22
26
 
23
- function createApp(complete: boolean) {
24
- const mock = createMockServices(complete);
27
+ function createApp(
28
+ status: OnboardingStatus | boolean,
29
+ options?: { authenticated?: boolean },
30
+ ) {
31
+ const resolved: OnboardingStatus =
32
+ typeof status === "boolean"
33
+ ? status
34
+ ? ONBOARDING_STATUS.COMPLETED
35
+ : ONBOARDING_STATUS.PENDING
36
+ : status;
37
+ const mock = createMockServices(resolved);
25
38
  const app = new Hono<Env>();
26
39
 
27
40
  app.use("*", async (c, next) => {
28
41
  c.set("services", mock.services);
42
+ c.set("isAuthenticated", options?.authenticated ?? false);
29
43
  c.set("currentSite", {
30
44
  id: DEFAULT_TEST_SITE_ID,
31
45
  key: "default",
@@ -55,6 +69,7 @@ function createApp(complete: boolean) {
55
69
  app.get("/robots.txt", (c) => c.text("robots"));
56
70
  app.get("/sitemap.xml", (c) => c.text("sitemap"));
57
71
  app.get("/media/abc.jpg", (c) => c.text("image"));
72
+ app.get("/compose", (c) => c.text("Compose"));
58
73
 
59
74
  return { app, getCallCount: mock.getCallCount };
60
75
  }
@@ -130,6 +145,62 @@ describe("requireOnboarding", () => {
130
145
  expect(getCallCount()).toBe(2); // queried again
131
146
  });
132
147
 
148
+ // A control plane already built the site and its owner, so readers must be
149
+ // served normally; only the author still owes setup an answer.
150
+ describe("provisioned sites", () => {
151
+ const provisioned = ONBOARDING_STATUS.PROVISIONED;
152
+
153
+ it("serves the public root to a signed-out visitor", async () => {
154
+ const { app } = createApp(provisioned);
155
+ const res = await app.request("/", { redirect: "manual" });
156
+ expect(res.status).toBe(200);
157
+ expect(await res.text()).toBe("Home");
158
+ });
159
+
160
+ it("serves /signin to a signed-out visitor", async () => {
161
+ const { app } = createApp(provisioned);
162
+ const res = await app.request("/signin", { redirect: "manual" });
163
+ expect(res.status).toBe(200);
164
+ });
165
+
166
+ it("redirects the signed-in author from / to /setup", async () => {
167
+ const { app } = createApp(provisioned, { authenticated: true });
168
+ const res = await app.request("/", { redirect: "manual" });
169
+ expect(res.status).toBe(302);
170
+ expect(res.headers.get("Location")).toBe("/setup");
171
+ });
172
+
173
+ it("redirects the signed-in author from /settings to /setup", async () => {
174
+ const { app } = createApp(provisioned, { authenticated: true });
175
+ const res = await app.request("/settings", { redirect: "manual" });
176
+ expect(res.status).toBe(302);
177
+ expect(res.headers.get("Location")).toBe("/setup");
178
+ });
179
+
180
+ it("redirects the signed-in author from /compose to /setup", async () => {
181
+ const { app } = createApp(provisioned, { authenticated: true });
182
+ const res = await app.request("/compose", { redirect: "manual" });
183
+ expect(res.status).toBe(302);
184
+ expect(res.headers.get("Location")).toBe("/setup");
185
+ });
186
+
187
+ it("leaves an archive page alone even for the author", async () => {
188
+ const { app } = createApp(provisioned, { authenticated: true });
189
+ const res = await app.request("/archive", { redirect: "manual" });
190
+ expect(res.status).toBe(200);
191
+ });
192
+
193
+ it("is not cached as complete", async () => {
194
+ const { app, getCallCount } = createApp(provisioned, {
195
+ authenticated: true,
196
+ });
197
+
198
+ await app.request("/", { redirect: "manual" });
199
+ await app.request("/settings", { redirect: "manual" });
200
+ expect(getCallCount()).toBe(2);
201
+ });
202
+ });
203
+
133
204
  describe("non-redirected paths (pass through without DB check)", () => {
134
205
  it("allows /setup", async () => {
135
206
  const { app, getCallCount } = createApp(false);
@@ -3,7 +3,7 @@ import { Hono } from "hono";
3
3
  import type { Bindings } from "../../types.js";
4
4
  import type { AppVariables } from "../../types/app-context.js";
5
5
 
6
- vi.mock("../../lib/version.js", () => ({
6
+ vi.mock("../../lib/build-env.js", () => ({
7
7
  IS_VITE_DEV: false,
8
8
  }));
9
9
 
@@ -1,16 +1,16 @@
1
1
  import { describe, it, expect } from "vitest";
2
+ import { eq } from "drizzle-orm";
2
3
  import { Hono } from "hono";
3
4
  import { attachSession } from "../session.js";
5
+ import { createAuth } from "../../auth.js";
6
+ import { createTestDatabase } from "../../__tests__/helpers/db.js";
7
+ import { session as sessionTable } from "../../db/schema.js";
4
8
  import type { Bindings } from "../../types.js";
5
- import type { AppVariables } from "../../types/app-context.js";
9
+ import type { AppSession, AppVariables } from "../../types/app-context.js";
6
10
 
7
11
  type Env = { Bindings: Bindings; Variables: AppVariables };
8
12
 
9
- function createAppWithAuth(mockAuth: AppVariables["auth"]): Hono<Env> & {
10
- // ensures tests see session/isAuthenticated via c.var
11
- _lastSession?: AppVariables["session"];
12
- _lastIsAuthenticated?: boolean;
13
- } {
13
+ function createAppWithAuth(mockAuth: AppVariables["auth"]): Hono<Env> {
14
14
  const app = new Hono<Env>();
15
15
  app.use("*", async (c, next) => {
16
16
  c.set("auth", mockAuth);
@@ -20,26 +20,40 @@ function createAppWithAuth(mockAuth: AppVariables["auth"]): Hono<Env> & {
20
20
  return app;
21
21
  }
22
22
 
23
+ /**
24
+ * `attachSession` calls `getSession` with `returnHeaders: true`, so the mock
25
+ * has to answer in better-call's `{ headers, response }` shape.
26
+ */
23
27
  function buildSessionMock(
24
- impl: () => Promise<AppVariables["session"]>,
28
+ impl: () => Promise<{ headers: Headers; response: AppSession }>,
25
29
  ): AppVariables["auth"] {
26
30
  return {
27
- api: {
28
- getSession: impl,
29
- },
31
+ api: { getSession: impl },
30
32
  } as unknown as AppVariables["auth"];
31
33
  }
32
34
 
35
+ const VALID_SESSION = {
36
+ user: { id: "user-1", email: "x@y.z", name: "X" },
37
+ session: { id: "sess-1" },
38
+ } as unknown as AppSession;
39
+
40
+ function respondWith(
41
+ response: AppSession,
42
+ setCookies: string[] = [],
43
+ ): () => Promise<{ headers: Headers; response: AppSession }> {
44
+ return async () => {
45
+ const headers = new Headers();
46
+ for (const cookie of setCookies) headers.append("Set-Cookie", cookie);
47
+ return { headers, response };
48
+ };
49
+ }
50
+
51
+ const REFRESHED_SESSION_COOKIE =
52
+ "__Secure-better-auth.session_token=fresh; Path=/; Max-Age=2592000; HttpOnly; Secure; SameSite=Lax";
53
+
33
54
  describe("attachSession", () => {
34
55
  it("populates c.var.session and isAuthenticated on a valid session", async () => {
35
- const mockAuth = buildSessionMock(
36
- async () =>
37
- ({
38
- user: { id: "user-1", email: "x@y.z", name: "X" },
39
- session: { id: "sess-1" },
40
- }) as unknown as AppVariables["session"],
41
- );
42
- const app = createAppWithAuth(mockAuth);
56
+ const app = createAppWithAuth(buildSessionMock(respondWith(VALID_SESSION)));
43
57
  app.get("/", (c) =>
44
58
  c.json({
45
59
  authed: c.var.isAuthenticated,
@@ -53,33 +67,167 @@ describe("attachSession", () => {
53
67
  });
54
68
 
55
69
  it("sets isAuthenticated=false and session=null when no session is present", async () => {
56
- const mockAuth = buildSessionMock(async () => null);
57
- const app = createAppWithAuth(mockAuth);
70
+ const app = createAppWithAuth(buildSessionMock(respondWith(null)));
58
71
  app.get("/", (c) =>
59
- c.json({
60
- authed: c.var.isAuthenticated,
61
- session: c.var.session,
62
- }),
72
+ c.json({ authed: c.var.isAuthenticated, session: c.var.session }),
63
73
  );
64
74
 
65
75
  const res = await app.request("/");
66
76
  expect(await res.json()).toEqual({ authed: false, session: null });
67
77
  });
68
78
 
69
- it("swallows errors from getSession and treats the request as unauthenticated", async () => {
70
- const mockAuth = buildSessionMock(async () => {
71
- throw new Error("session lookup failed");
72
- });
73
- const app = createAppWithAuth(mockAuth);
74
- app.get("/", (c) =>
75
- c.json({
76
- authed: c.var.isAuthenticated,
77
- session: c.var.session,
79
+ it("retries once so a blipping lookup does not present as a logout", async () => {
80
+ let calls = 0;
81
+ const app = createAppWithAuth(
82
+ buildSessionMock(async () => {
83
+ calls += 1;
84
+ if (calls === 1) throw new Error("transient database error");
85
+ return { headers: new Headers(), response: VALID_SESSION };
86
+ }),
87
+ );
88
+ app.get("/", (c) => c.json({ authed: c.var.isAuthenticated }));
89
+
90
+ const res = await app.request("/");
91
+ expect(calls).toBe(2);
92
+ expect(await res.json()).toEqual({ authed: true });
93
+ });
94
+
95
+ it("swallows a persistent lookup failure and treats the request as unauthenticated", async () => {
96
+ let calls = 0;
97
+ const app = createAppWithAuth(
98
+ buildSessionMock(async () => {
99
+ calls += 1;
100
+ throw new Error("session lookup failed");
78
101
  }),
79
102
  );
103
+ app.get("/", (c) =>
104
+ c.json({ authed: c.var.isAuthenticated, session: c.var.session }),
105
+ );
80
106
 
81
107
  const res = await app.request("/");
82
108
  expect(res.status).toBe(200);
109
+ expect(calls).toBe(2);
83
110
  expect(await res.json()).toEqual({ authed: false, session: null });
84
111
  });
112
+
113
+ it("writes better-auth's refreshed cookies onto the response", async () => {
114
+ const app = createAppWithAuth(
115
+ buildSessionMock(respondWith(VALID_SESSION, [REFRESHED_SESSION_COOKIE])),
116
+ );
117
+ app.get("/", (c) => c.json({ ok: true }));
118
+
119
+ const res = await app.request("/");
120
+ expect(res.headers.getSetCookie()).toEqual([REFRESHED_SESSION_COOKIE]);
121
+ });
122
+
123
+ it("never attaches cookies to a shared-cacheable response", async () => {
124
+ const app = createAppWithAuth(
125
+ buildSessionMock(respondWith(VALID_SESSION, [REFRESHED_SESSION_COOKIE])),
126
+ );
127
+ // Mirrors the media routes, which answer with a year-long public cache
128
+ // directive. A session cookie stored alongside that response is handed to
129
+ // whoever the CDN serves next.
130
+ app.get("/media/x.png", (c) =>
131
+ c.body("bytes", 200, {
132
+ "Cache-Control": "public, max-age=31536000, immutable",
133
+ }),
134
+ );
135
+
136
+ const res = await app.request("/media/x.png");
137
+ expect(res.headers.getSetCookie()).toEqual([]);
138
+ });
139
+
140
+ it("attaches cookies to an explicitly private response", async () => {
141
+ const app = createAppWithAuth(
142
+ buildSessionMock(respondWith(VALID_SESSION, [REFRESHED_SESSION_COOKIE])),
143
+ );
144
+ app.get("/", (c) =>
145
+ c.json({ ok: true }, 200, { "Cache-Control": "private, no-store" }),
146
+ );
147
+
148
+ const res = await app.request("/");
149
+ expect(res.headers.getSetCookie()).toEqual([REFRESHED_SESSION_COOKIE]);
150
+ });
151
+
152
+ it("lets a route's own cookie win, so sign-out is not undone by the refresh", async () => {
153
+ const cleared =
154
+ "__Secure-better-auth.session_token=; Path=/; Max-Age=0; HttpOnly; Secure; SameSite=Lax";
155
+ const app = createAppWithAuth(
156
+ buildSessionMock(respondWith(VALID_SESSION, [REFRESHED_SESSION_COOKIE])),
157
+ );
158
+ app.post("/signout", (c) => c.body(null, 302, { "Set-Cookie": cleared }));
159
+
160
+ const res = await app.request("/signout", { method: "POST" });
161
+ expect(res.headers.getSetCookie()).toEqual([cleared]);
162
+ });
163
+ });
164
+
165
+ describe("attachSession with a real better-auth instance", () => {
166
+ it("re-issues the session cookie so its browser lifetime rolls forward", async () => {
167
+ const { db } = createTestDatabase();
168
+ const auth = createAuth(db, {
169
+ secret: "test-secret-with-enough-entropy-for-session-cookies",
170
+ baseURL: "https://example.com",
171
+ useSecureCookies: true,
172
+ });
173
+
174
+ const signUp = await auth.api.signUpEmail({
175
+ body: {
176
+ name: "Owner",
177
+ email: "owner@example.com",
178
+ password: "correct-horse-battery",
179
+ },
180
+ returnHeaders: true,
181
+ });
182
+ const signUpCookies = signUp.headers.getSetCookie();
183
+ const sessionCookie = signUpCookies.find((cookie) =>
184
+ cookie.includes(".session_token="),
185
+ );
186
+ expect(sessionCookie).toBeDefined();
187
+ // Send only the session token, not the `session_data` cache cookie: that
188
+ // one is written with `Max-Age=300`, so any request more than five minutes
189
+ // after the last one arrives without it and takes the database path. A
190
+ // cache hit deliberately short-circuits before any refresh.
191
+ const cookieHeader = sessionCookie!.split(";")[0]!;
192
+
193
+ // better-auth only re-issues the cookie once per `updateAge` (1 day). Age
194
+ // the stored session past that threshold so this request is the one that
195
+ // refreshes: with `expiresIn` at 90 days, any expiry inside the next 89
196
+ // days is due.
197
+ const rows = await db.select().from(sessionTable);
198
+ expect(rows).toHaveLength(1);
199
+ const due = new Date(Date.now() + 28 * 24 * 60 * 60 * 1000);
200
+ await db
201
+ .update(sessionTable)
202
+ .set({ expiresAt: due })
203
+ .where(eq(sessionTable.id, rows[0]!.id));
204
+
205
+ const app = new Hono<Env>();
206
+ app.use("*", async (c, next) => {
207
+ c.set("auth", auth);
208
+ await next();
209
+ });
210
+ app.use("*", attachSession());
211
+ app.get("/", (c) => c.json({ authed: c.var.isAuthenticated }));
212
+
213
+ const res = await app.request("/", { headers: { cookie: cookieHeader } });
214
+ expect(await res.json()).toEqual({ authed: true });
215
+
216
+ // The regression this guards: without `returnHeaders` the refreshed cookie
217
+ // is dropped, so the browser copy keeps the expiry it was born with and
218
+ // dies a fixed `expiresIn` after sign-in however active the user is.
219
+ // Asserted against the aged-down 28 days rather than the configured window,
220
+ // so this stays a test of "it rolls forward" and not of the current policy.
221
+ const refreshed = res.headers
222
+ .getSetCookie()
223
+ .find((cookie) => cookie.includes(".session_token="));
224
+ expect(refreshed).toBeDefined();
225
+ expect(refreshed).toMatch(/Max-Age=\d+/);
226
+ const maxAge = Number(/Max-Age=(\d+)/.exec(refreshed!)?.[1]);
227
+ expect(maxAge).toBeGreaterThan(29 * 24 * 60 * 60);
228
+
229
+ // And the database row rolled forward with it.
230
+ const [after] = await db.select().from(sessionTable);
231
+ expect(after!.expiresAt.getTime()).toBeGreaterThan(due.getTime());
232
+ });
85
233
  });