@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
@@ -0,0 +1,183 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { readAllStylesheets } from "./helpers/component-css.js";
3
+
4
+ /**
5
+ * The sanctioned width breakpoints, mirroring the ledger comment in
6
+ * `styles/tokens.css`. Each number is one job; a job never gets a second
7
+ * number.
8
+ *
9
+ * This is not bookkeeping for its own sake. The composer once switched to its
10
+ * full-screen sheet at 700px while its own controls took their touch sizing at
11
+ * 760px, so a 750px window rendered a thumb-sized Post button (46px, against
12
+ * 33px one pixel wider) inside a windowed dialog. Nothing was wrong in either
13
+ * rule on its own — they had simply drifted to two numbers for one switch, and
14
+ * no test could see it.
15
+ */
16
+ const LEDGER = [
17
+ 480, // Header hamburger; the composer's post-meta pill sheds its words.
18
+ 580, // Nav collapse tier-sm.
19
+ 640, // Media grid and lightbox chrome.
20
+ 700, // The phone switch.
21
+ 780, // Nav collapse tier-md.
22
+ 960, // Nav collapse tier-lg.
23
+ 1024, // Tufte two-column → single-column.
24
+ 1079, // Collections sidebar drops out.
25
+ 1200, // Header search collapses to an icon.
26
+ ] as const;
27
+
28
+ /**
29
+ * Height breakpoints are a separate axis with a separate job: a short viewport
30
+ * has no room for the composer's chrome regardless of how wide it is. One
31
+ * number, stated on both sides.
32
+ */
33
+ const HEIGHT_LEDGER = [760] as const;
34
+
35
+ interface Condition {
36
+ /** The stylesheet the condition appears in. */
37
+ file: string;
38
+ /** 1-indexed line of the `@media` prelude the condition sits in. */
39
+ line: number;
40
+ /** `min` or `max`. */
41
+ bound: "min" | "max";
42
+ /** `width` or `height`. */
43
+ axis: "width" | "height";
44
+ /** The pixel value as written. */
45
+ value: number;
46
+ /** The condition as written, for failure messages. */
47
+ text: string;
48
+ }
49
+
50
+ /**
51
+ * Every `(min-width: …)` / `(max-width: …)` / `(min-height: …)` /
52
+ * `(max-height: …)` condition in an `@media` prelude across the hand-written
53
+ * stylesheets.
54
+ *
55
+ * `@media` only. A `@container` query carrying the same words measures its
56
+ * container, not the viewport, so it answers to the component it wraps rather
57
+ * than to this ledger — the archive filter bar splits on its own inline size
58
+ * at 39rem and is right to.
59
+ *
60
+ * A prelude may span several lines (the composer's full-screen sheet lists
61
+ * three conditions), so the line number is counted to the condition itself
62
+ * rather than to the `@media` that opens it.
63
+ */
64
+ function collectConditions(): Condition[] {
65
+ const found: Condition[] = [];
66
+
67
+ for (const { name, css } of readAllStylesheets()) {
68
+ const preludes = /@media\b([^{;]*)/g;
69
+ let prelude: RegExpExecArray | null;
70
+
71
+ while ((prelude = preludes.exec(css)) !== null) {
72
+ const offset = prelude.index + "@media".length;
73
+ const pattern = /\((min|max)-(width|height):\s*([^)]+)\)/g;
74
+ let match: RegExpExecArray | null;
75
+
76
+ while ((match = pattern.exec(prelude[1])) !== null) {
77
+ const [text, bound, axis, raw] = match;
78
+ const px = /^(\d+(?:\.\d+)?)px$/.exec(raw.trim());
79
+
80
+ found.push({
81
+ file: name,
82
+ line: css.slice(0, offset + match.index).split("\n").length,
83
+ bound: bound as "min" | "max",
84
+ axis: axis as "width" | "height",
85
+ // A non-px unit lands as NaN and fails the ledger check by name,
86
+ // which is the point: `rem` in a media query resolves against the
87
+ // initial font-size rather than the root this document sets, so the
88
+ // number it reads as is not the number it is.
89
+ value: px ? Number(px[1]) : Number.NaN,
90
+ text,
91
+ });
92
+ }
93
+ }
94
+ }
95
+
96
+ return found;
97
+ }
98
+
99
+ /**
100
+ * The breakpoint a condition belongs to. `min-width: 700px` and
101
+ * `max-width: 699px` are the two sides of the same 700px switch, so the `max`
102
+ * side is stated one pixel below the number it divides on.
103
+ */
104
+ function breakpointOf(condition: Condition): number {
105
+ return condition.bound === "max" ? condition.value + 1 : condition.value;
106
+ }
107
+
108
+ describe("stylesheet breakpoints", () => {
109
+ it("uses only the breakpoints on the ledger", () => {
110
+ const strays = collectConditions()
111
+ .filter((condition) => {
112
+ const ledger = condition.axis === "width" ? LEDGER : HEIGHT_LEDGER;
113
+ // Both spellings of a breakpoint are accepted here — `max-width: 640px`
114
+ // and `max-width: 639px` are the same switch. Which one a file may use
115
+ // is settled by the overlap test below.
116
+ return !(
117
+ ledger.includes(condition.value as never) ||
118
+ ledger.includes(breakpointOf(condition) as never)
119
+ );
120
+ })
121
+ .map(
122
+ (condition) =>
123
+ `${condition.file}:${condition.line} ${condition.text}` +
124
+ ` — not on the ledger in styles/tokens.css`,
125
+ );
126
+
127
+ expect(strays).toEqual([]);
128
+ });
129
+
130
+ it("never lets both sides of a breakpoint match the same viewport", () => {
131
+ const conditions = collectConditions();
132
+ const overlaps: string[] = [];
133
+
134
+ for (const axis of ["width", "height"] as const) {
135
+ const onAxis = conditions.filter((condition) => condition.axis === axis);
136
+
137
+ for (const value of new Set(
138
+ onAxis.filter((c) => c.bound === "min").map((c) => c.value),
139
+ )) {
140
+ // `min-*: N` and `max-*: N` both match at exactly N. Wherever a
141
+ // breakpoint is written from both sides, the lower side has to sit one
142
+ // pixel below it — otherwise a viewport of exactly N gets the phone
143
+ // layout and the desktop spacing at once.
144
+ const clash = onAxis.filter(
145
+ (condition) => condition.bound === "max" && condition.value === value,
146
+ );
147
+
148
+ overlaps.push(
149
+ ...clash.map(
150
+ (condition) =>
151
+ `${condition.file}:${condition.line} ${condition.text}` +
152
+ ` overlaps (min-${axis}: ${value}px) at exactly ${value}px` +
153
+ ` — write it as (max-${axis}: ${value - 1}px)`,
154
+ ),
155
+ );
156
+ }
157
+ }
158
+
159
+ expect(overlaps).toEqual([]);
160
+ });
161
+
162
+ it("keeps the phone switch on one number", () => {
163
+ // The regression this file exists for. Touch sizing and the mobile type
164
+ // scale used to ride on 760px while the layout they sit inside switched at
165
+ // 700px; anything between the two was a desktop window wearing phone
166
+ // controls. Any width breakpoint in the 700s that is not the nav ladder's
167
+ // 780px is that drift coming back.
168
+ const nearMisses = collectConditions()
169
+ .filter(
170
+ (condition) =>
171
+ condition.axis === "width" &&
172
+ condition.value > 700 &&
173
+ condition.value < 780,
174
+ )
175
+ .map(
176
+ (condition) =>
177
+ `${condition.file}:${condition.line} ${condition.text}` +
178
+ ` — the phone switch is 700px; see the ledger in styles/tokens.css`,
179
+ );
180
+
181
+ expect(nearMisses).toEqual([]);
182
+ });
183
+ });
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Streaming ZIP helpers used by `site export`, `site import`,
3
+ * `site pull-media`, and `site snapshot`.
4
+ */
5
+
6
+ import { createWriteStream } from "node:fs";
7
+ import {
8
+ chmod,
9
+ mkdir,
10
+ mkdtemp,
11
+ readFile,
12
+ rm,
13
+ stat,
14
+ writeFile,
15
+ } from "node:fs/promises";
16
+ import { tmpdir } from "node:os";
17
+ import { join } from "node:path";
18
+ import { pipeline } from "node:stream/promises";
19
+ import { zipSync } from "fflate";
20
+ import yazl from "yazl";
21
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
22
+ import {
23
+ extractZipFile,
24
+ writeDirectoryToZip,
25
+ } from "../../bin/lib/zip-archive.js";
26
+
27
+ describe("zip-archive", () => {
28
+ let tempDir: string;
29
+
30
+ beforeEach(async () => {
31
+ tempDir = await mkdtemp(join(tmpdir(), "jant-zip-"));
32
+ });
33
+
34
+ afterEach(async () => {
35
+ await chmod(join(tempDir, "source", "locked.md"), 0o644).catch(() => {});
36
+ await rm(tempDir, { recursive: true, force: true });
37
+ });
38
+
39
+ it("writes a directory and reads it back byte for byte", async () => {
40
+ const source = join(tempDir, "source");
41
+ const image = new Uint8Array(4096).map((_, index) => index % 251);
42
+ await mkdir(join(source, "content", "hello"), { recursive: true });
43
+ await mkdir(join(source, "static", "media"), { recursive: true });
44
+ await writeFile(
45
+ join(source, "hugo.toml"),
46
+ 'baseURL = "https://a.example/"',
47
+ );
48
+ await writeFile(
49
+ join(source, "content", "hello", "_index.md"),
50
+ "---\n---\n你好",
51
+ );
52
+ await writeFile(join(source, "static", "media", "a.jpg"), image);
53
+
54
+ const zipPath = join(tempDir, "out", "site.zip");
55
+ expect(await writeDirectoryToZip(source, zipPath)).toEqual({ files: 3 });
56
+
57
+ const target = join(tempDir, "target");
58
+ expect(await extractZipFile(zipPath, target)).toEqual({ files: 3 });
59
+ expect(
60
+ await readFile(join(target, "content", "hello", "_index.md"), "utf8"),
61
+ ).toBe("---\n---\n你好");
62
+ expect(
63
+ new Uint8Array(await readFile(join(target, "static", "media", "a.jpg"))),
64
+ ).toEqual(image);
65
+ });
66
+
67
+ it("reads a ZIP64 archive", async () => {
68
+ const zipFile = new yazl.ZipFile();
69
+ zipFile.addBuffer(Buffer.from("big enough to need ZIP64"), "data/a.txt");
70
+ zipFile.end({ forceZip64Format: true });
71
+ const zipPath = join(tempDir, "zip64.zip");
72
+ await pipeline(zipFile.outputStream, createWriteStream(zipPath));
73
+
74
+ await extractZipFile(zipPath, join(tempDir, "target"));
75
+
76
+ expect(
77
+ await readFile(join(tempDir, "target", "data", "a.txt"), "utf8"),
78
+ ).toBe("big enough to need ZIP64");
79
+ });
80
+
81
+ it("refuses an entry that climbs out of the target directory", async () => {
82
+ const zipPath = join(tempDir, "escape.zip");
83
+ await writeFile(
84
+ zipPath,
85
+ zipSync({ "../escaped.txt": new TextEncoder().encode("x") }),
86
+ );
87
+
88
+ await expect(
89
+ extractZipFile(zipPath, join(tempDir, "target")),
90
+ ).rejects.toThrow();
91
+ await expect(stat(join(tempDir, "escaped.txt"))).rejects.toThrow();
92
+ });
93
+
94
+ it("leaves no archive behind when writing fails", async () => {
95
+ if (process.getuid?.() === 0) return; // root reads a mode-000 file anyway
96
+ const source = join(tempDir, "source");
97
+ await mkdir(source, { recursive: true });
98
+ await writeFile(join(source, "locked.md"), "secret");
99
+ await chmod(join(source, "locked.md"), 0o000);
100
+
101
+ const zipPath = join(tempDir, "site.zip");
102
+ await expect(writeDirectoryToZip(source, zipPath)).rejects.toThrow();
103
+ await expect(stat(zipPath)).rejects.toThrow();
104
+ await expect(stat(`${zipPath}.partial`)).rejects.toThrow();
105
+ });
106
+ });
package/src/app.tsx CHANGED
@@ -16,11 +16,13 @@ import { hostedDomainCheckRoutes } from "./routes/hosted/domain-check.js";
16
16
 
17
17
  // Routes - Pages
18
18
  import { homeRoutes } from "./routes/pages/home.js";
19
+ import { languageRoutes } from "./routes/pages/language.js";
19
20
  import { pageRoutes } from "./routes/pages/page.js";
20
21
  import { collectionRoutes } from "./routes/pages/collection.js";
21
22
  import { archiveRoutes } from "./routes/pages/archive.js";
22
23
  import { searchRoutes } from "./routes/pages/search.js";
23
24
  import { featuredRoutes } from "./routes/pages/featured.js";
25
+ import { subscribeRoutes } from "./routes/pages/subscribe.js";
24
26
  import { latestRoutes } from "./routes/pages/latest.js";
25
27
  import { collectionsPageRoutes } from "./routes/pages/collections.js";
26
28
  import { newPostRoutes } from "./routes/pages/new.js";
@@ -37,6 +39,7 @@ import { postsApiRoutes } from "./routes/api/posts.js";
37
39
  import { attachmentsApiRoutes } from "./routes/api/attachments.js";
38
40
  import { navItemsApiRoutes } from "./routes/api/nav-items.js";
39
41
  import { collectionsApiRoutes } from "./routes/api/collections.js";
42
+ import { smartCollectionsApiRoutes } from "./routes/api/smart-collections.js";
40
43
  import { settingsApiRoutes } from "./routes/api/settings.js";
41
44
  import { uploadApiRoutes } from "./routes/api/upload.js";
42
45
  import { uploadsApiRoutes } from "./routes/api/uploads.js";
@@ -59,6 +62,7 @@ import { internalPostBodyHtmlRoutes } from "./routes/api/internal/post-body-html
59
62
  import { internalUploadsRoutes } from "./routes/api/internal/uploads.js";
60
63
  import { publicArchiveApiRoutes } from "./routes/api/public/archive.js";
61
64
  import { publicPostsApiRoutes } from "./routes/api/public/posts.js";
65
+ import { discoverApiRoutes } from "./routes/api/discover.js";
62
66
  // Routes - Compose
63
67
  import { composeRoutes } from "./routes/compose.js";
64
68
 
@@ -71,7 +75,7 @@ import { siteSkillRoutes } from "./routes/site-skill.js";
71
75
  // Middleware
72
76
  import { requireAuth } from "./middleware/auth.js";
73
77
  import { attachSession } from "./middleware/session.js";
74
- import { defaultCacheControl } from "./middleware/cache-control.js";
78
+ import { defaultCacheControl, noStore } from "./middleware/cache-control.js";
75
79
  import { requireRssFeedsEnabled } from "./middleware/public-content-access.js";
76
80
  import { requireOnboarding } from "./middleware/onboarding.js";
77
81
  import { errorHandler } from "./middleware/error-handler.js";
@@ -89,7 +93,11 @@ import {
89
93
  } from "./lib/jant-branding.js";
90
94
  import { isAssetPath } from "./lib/asset-path.js";
91
95
  import { getHostedCanonicalRedirect } from "./lib/hosted-domain.js";
92
- import { stripSitePathPrefix, toPublicHref } from "./lib/url.js";
96
+ import { normalizePath, stripSitePathPrefix, toPublicHref } from "./lib/url.js";
97
+ import {
98
+ matchesIfNoneMatch,
99
+ withConditionalResponse,
100
+ } from "./lib/http-cache.js";
93
101
  import { withWorkerResponseCache } from "./lib/worker-response-cache.js";
94
102
  import { createRequestRuntime } from "./runtime/index.js";
95
103
  import { getInstanceReadiness } from "./runtime/readiness.js";
@@ -265,22 +273,30 @@ export function createApp(): App {
265
273
  const app = new Hono<{ Bindings: Bindings; Variables: AppVariables }>();
266
274
  const defaultFetch = app.fetch.bind(app);
267
275
 
268
- app.fetch = (request, env, executionCtx) => {
276
+ app.fetch = async (request, env, executionCtx) => {
269
277
  const bindings = env as Bindings | undefined;
270
278
  const preparedRequest = prepareRequestForRouting(
271
279
  request,
272
280
  getConfiguredSingleSitePathPrefix(bindings),
273
281
  );
274
282
  if (preparedRequest instanceof Response) {
275
- return Promise.resolve(preparedRequest);
283
+ return preparedRequest;
276
284
  }
277
- return withWorkerResponseCache({
278
- bindings,
279
- executionCtx,
285
+ // Revalidation sits outside the response cache, so a reader holding the
286
+ // current version is answered from a cache hit as cheaply as from a miss,
287
+ // and a miss still stores the full document on its way out.
288
+ return withConditionalResponse(
280
289
  request,
281
- next: () =>
282
- Promise.resolve(defaultFetch(preparedRequest, bindings, executionCtx)),
283
- });
290
+ await withWorkerResponseCache({
291
+ bindings,
292
+ executionCtx,
293
+ request,
294
+ next: () =>
295
+ Promise.resolve(
296
+ defaultFetch(preparedRequest, bindings, executionCtx),
297
+ ),
298
+ }),
299
+ );
284
300
  };
285
301
 
286
302
  // Global error handler: maps DomainError → HTTP responses
@@ -307,7 +323,11 @@ export function createApp(): App {
307
323
  if (startupConfigError) {
308
324
  return c.html(startupConfigError, 500);
309
325
  }
310
- const runtime = await createRequestRuntime(c.env, publicRequestUrl);
326
+ const runtime = await createRequestRuntime(
327
+ c.env,
328
+ publicRequestUrl,
329
+ c.req.raw,
330
+ );
311
331
  c.set("services", runtime.services);
312
332
  c.set("servicesForSite", runtime.servicesForSite);
313
333
  c.set("hostedHandoff", runtime.hostedHandoff);
@@ -324,12 +344,35 @@ export function createApp(): App {
324
344
  // downstream handlers don't each call auth.api.getSession themselves.
325
345
  app.use("*", attachSession());
326
346
 
327
- // Default every response without an explicit Cache-Control to
328
- // `private, no-store`. Jant pages are auth-variant, so a shared/CDN cache
329
- // must never store them; routes serving genuinely public resources (media,
330
- // feeds, sitemaps, favicons) set their own Cache-Control and are untouched.
347
+ // Default every response without an explicit Cache-Control: `private,
348
+ // no-cache` for an anonymous reader, `private, no-store` once signed in.
349
+ // Jant pages are auth-variant, so a shared/CDN cache must never store them
350
+ // — that is what `private` is for in both — while a reader's own browser
351
+ // keeps the page and its back/forward cache. Routes serving genuinely public
352
+ // resources (media, feeds, sitemaps, favicons) set their own Cache-Control
353
+ // and are untouched.
331
354
  app.use("*", defaultCacheControl());
332
355
 
356
+ // Credential surfaces override that default. Each of these is served to
357
+ // someone with no session yet, so the anonymous branch above would let the
358
+ // browser keep it — and its URL — on disk: a reset link's one-time token, a
359
+ // sign-in form, an SSO handoff. One list rather than a `use()` beside each
360
+ // route group, because it has to be registered up here anyway: `/api/auth/*`
361
+ // ends in a terminal handler that never yields, so a middleware registered
362
+ // after it never runs. The handlers themselves live under "Auth routes"
363
+ // below and in `app.all("/api/auth/*")`; a new one belongs in both places.
364
+ for (const path of [
365
+ "/setup",
366
+ "/signin",
367
+ "/signout",
368
+ "/reset",
369
+ "/__sso",
370
+ "/__dev/login",
371
+ "/api/auth/*",
372
+ ]) {
373
+ app.use(path, noStore());
374
+ }
375
+
333
376
  app.use("*", async (c, next) => {
334
377
  const redirectUrl = await getHostedCanonicalRedirect({
335
378
  currentSite: c.var.currentSite,
@@ -371,8 +414,10 @@ export function createApp(): App {
371
414
  const storage = c.var.storage;
372
415
  if (!storage) return c.notFound();
373
416
 
417
+ // The outer conditional layer would answer this too, but only after the
418
+ // object has been fetched and rendered; matching here skips that work.
374
419
  const etag = `"${media.updatedAt}"`;
375
- if (c.req.header("If-None-Match") === etag) {
420
+ if (matchesIfNoneMatch(c.req.header("If-None-Match"), etag)) {
376
421
  return new Response(null, { status: 304, headers: { ETag: etag } });
377
422
  }
378
423
 
@@ -488,7 +533,13 @@ export function createApp(): App {
488
533
  await next();
489
534
  });
490
535
 
491
- // Redirect middleware — only handles redirect-type custom URLs
536
+ // Redirect middleware — only handles redirect-type custom URLs.
537
+ //
538
+ // The lookup it needs is the same `path_registry` read the catch-all route
539
+ // makes to resolve a post or collection, so the record is stashed on the
540
+ // context and that route reuses it instead of asking again. Nothing between
541
+ // here and the route writes to the registry, so the record cannot go stale
542
+ // within the request.
492
543
  app.use("*", async (c, next) => {
493
544
  const path = new URL(c.req.url).pathname;
494
545
  // Skip redirect lookup for fixed machine routes and static assets.
@@ -496,17 +547,20 @@ export function createApp(): App {
496
547
  return next();
497
548
  }
498
549
 
499
- const customUrl = await c.var.services.customUrls.getByPath(path.slice(1));
500
- if (customUrl?.targetType === "redirect" && customUrl.toPath) {
550
+ const storedPath = normalizePath(path);
551
+ const record = await c.var.services.paths.resolve(storedPath);
552
+ c.set("pathLookup", { path: storedPath, record });
553
+
554
+ if (record?.kind === "redirect" && record.redirectToPath) {
501
555
  return c.redirect(
502
556
  toPublicHref(
503
- customUrl.toPath,
557
+ `/${record.redirectToPath}`,
504
558
  getRuntimeSitePathPrefix({
505
559
  env: c.env,
506
560
  currentSiteDomain: c.var.currentSiteDomain,
507
561
  }),
508
562
  ),
509
- customUrl.redirectType ?? 301,
563
+ record.redirectType ?? 301,
510
564
  );
511
565
  }
512
566
 
@@ -529,15 +583,18 @@ export function createApp(): App {
529
583
  // API Routes
530
584
  app.route("/api/public/posts", publicPostsApiRoutes);
531
585
  app.route("/api/public/archive", publicArchiveApiRoutes);
586
+ app.route("/api/discover", discoverApiRoutes);
532
587
  app.route("/api/posts", postsApiRoutes);
533
588
  app.route("/api/nav-items", navItemsApiRoutes);
534
589
  app.route("/api/collections", collectionsApiRoutes);
590
+ app.route("/api/smart-collections", smartCollectionsApiRoutes);
535
591
  app.route("/api/settings", settingsApiRoutes);
536
592
  app.route("/api/custom-urls", customUrlsApiRoutes);
537
593
  app.route("/api/export", exportApiRoutes);
538
594
  app.route("/api/github-sync", githubSyncAdminRoutes);
539
595
 
540
- // Auth routes
596
+ // Auth routes. Their paths are also listed in the `noStore()` block above,
597
+ // which is what keeps a credential page out of the browser's disk cache.
541
598
  app.route("/", setupRoutes);
542
599
  app.route("/", signinRoutes);
543
600
  app.route("/", resetRoutes);
@@ -571,6 +628,7 @@ export function createApp(): App {
571
628
  app.route("/", newPostRoutes);
572
629
  app.route("/archive", archiveRoutes);
573
630
  app.route("/featured", featuredRoutes);
631
+ app.route("/subscribe", subscribeRoutes);
574
632
  app.route("/latest", latestRoutes);
575
633
  app.route("/", partialPageRoutes);
576
634
  app.route("/_", brandRoutes);
@@ -579,6 +637,12 @@ export function createApp(): App {
579
637
  app.route("/collections", collectionRoutes);
580
638
  app.route("/", homeRoutes);
581
639
 
640
+ // Per-language views of every surface above. Mounted here on purpose: after
641
+ // the static route groups, so `/archive` is the archive rather than a
642
+ // language named "archive", and before the catch-all, so `/en` is a language
643
+ // view rather than a lookup for a post whose slug is "en".
644
+ app.route("/:lang", languageRoutes);
645
+
582
646
  // Custom page catch-all (must be last)
583
647
  app.route("/", pageRoutes);
584
648
 
package/src/auth.ts CHANGED
@@ -66,7 +66,24 @@ export function createAuth(
66
66
  },
67
67
  },
68
68
  session: {
69
- expiresIn: 3600 * 24 * 30, // 30 days
69
+ // An idle window, not a hard cap. `attachSession` writes better-auth's
70
+ // re-issued cookie back to the browser, so any visit inside the window
71
+ // pushes both the stored session and the cookie out another 90 days —
72
+ // a single author on their own machine should effectively never be
73
+ // asked to sign in again.
74
+ expiresIn: 3600 * 24 * 90, // 90 days
75
+ // How stale a session may get before a read renews it. Same as
76
+ // better-auth's default, but stated here because it sets the renewal
77
+ // cadence and that shouldn't be invisible library behaviour.
78
+ updateAge: 3600 * 24, // 1 day
79
+ // Turns off better-auth's re-authentication gate, whose default lets a
80
+ // session read `/list-sessions` only within 24 hours of sign-in — and it
81
+ // measures that from `createdAt`, which renewal never moves, so the
82
+ // Sessions page would answer "Session is not fresh" from day two onward.
83
+ // The gate also guards nothing here: revoking a session, the destructive
84
+ // half of that page, is not behind it, and the endpoints that would care
85
+ // (delete-user, unlink-account) are not part of Jant.
86
+ freshAge: 0,
70
87
  cookieCache: {
71
88
  enabled: true,
72
89
  maxAge: 60 * 5, // 5 minutes
@@ -11,6 +11,10 @@ vi.mock("../confirm.js", () => ({
11
11
  showConfirmDialog: vi.fn(),
12
12
  }));
13
13
 
14
+ vi.mock("../collection-dialog-host.js", () => ({
15
+ openCollectionDialog: vi.fn(),
16
+ }));
17
+
14
18
  function createMarkup() {
15
19
  document.body.innerHTML = `
16
20
  <div id="toast-container"></div>
@@ -29,9 +33,9 @@ function createMarkup() {
29
33
  More actions
30
34
  </button>
31
35
  <div data-collection-page-menu hidden>
32
- <a href="/collections/original-slug/edit?returnTo=%2Foriginal-slug" role="menuitem">
36
+ <button type="button" role="menuitem" data-collection-page-action="edit">
33
37
  Edit
34
- </a>
38
+ </button>
35
39
  <button
36
40
  type="button"
37
41
  role="menuitem"
@@ -132,6 +136,51 @@ describe("collection detail page actions", () => {
132
136
  expect(window.location.pathname).toBe("/collections");
133
137
  });
134
138
 
139
+ it("edits the collection in the dialog, and follows a moved address", async () => {
140
+ createMarkup();
141
+ const { openCollectionDialog } =
142
+ await import("../collection-dialog-host.js");
143
+ vi.mocked(openCollectionDialog).mockResolvedValue({
144
+ changed: true,
145
+ collection: { id: "collection-1", slug: "renamed", title: "Renamed" },
146
+ });
147
+ await import("../collection-page-actions.js");
148
+
149
+ document
150
+ .querySelector<HTMLElement>("[data-collection-page-action='toggle-menu']")
151
+ ?.click();
152
+ document
153
+ .querySelector<HTMLElement>("[data-collection-page-action='edit']")
154
+ ?.click();
155
+ await flushAsyncWork();
156
+
157
+ expect(openCollectionDialog).toHaveBeenCalledWith({
158
+ collectionId: "collection-1",
159
+ });
160
+ expect(window.location.pathname).toBe("/renamed");
161
+ });
162
+
163
+ it("leaves the page where it is when the dialog is dismissed", async () => {
164
+ createMarkup();
165
+ const { openCollectionDialog } =
166
+ await import("../collection-dialog-host.js");
167
+ vi.mocked(openCollectionDialog).mockResolvedValue({ changed: false });
168
+ const reload = vi.fn();
169
+ vi.spyOn(window.location, "reload").mockImplementation(reload);
170
+ await import("../collection-page-actions.js");
171
+
172
+ document
173
+ .querySelector<HTMLElement>("[data-collection-page-action='toggle-menu']")
174
+ ?.click();
175
+ document
176
+ .querySelector<HTMLElement>("[data-collection-page-action='edit']")
177
+ ?.click();
178
+ await flushAsyncWork();
179
+
180
+ expect(reload).not.toHaveBeenCalled();
181
+ expect(window.location.pathname).toBe("/original-slug");
182
+ });
183
+
135
184
  it("adds the collection to navigation and reveals the settings action", async () => {
136
185
  createMarkup();
137
186
  const fetchMock = vi.fn().mockResolvedValue(