@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,132 @@
1
+ /**
2
+ * Smart Collections API Routes
3
+ */
4
+
5
+ import { Hono } from "hono";
6
+ import { z } from "zod";
7
+ import type { Bindings } from "../../types.js";
8
+ import type { AppVariables } from "../../types/app-context.js";
9
+ import { requireAuthApi } from "../../middleware/auth.js";
10
+ import {
11
+ ContentLanguageSchema,
12
+ CreateSmartCollectionSchema,
13
+ SmartCollectionIdSchema,
14
+ SmartCollectionPreviewSchema,
15
+ UpdateSmartCollectionSchema,
16
+ parseValidated,
17
+ } from "../../lib/schemas.js";
18
+ import { assertFound, parseIdParam } from "../../lib/errors.js";
19
+ import { ID_PREFIX } from "../../lib/ids.js";
20
+
21
+ type Env = { Bindings: Bindings; Variables: AppVariables };
22
+
23
+ export const smartCollectionsApiRoutes = new Hono<Env>();
24
+
25
+ /**
26
+ * Address availability, in the shape posts already use.
27
+ *
28
+ * `suggest` derives one from a title; `check` answers whether a typed one is
29
+ * free. Collections answer the same question at `/api/collections/slug`.
30
+ */
31
+ const SlugQuerySchema = z.discriminatedUnion("mode", [
32
+ z.object({
33
+ mode: z.literal("suggest"),
34
+ title: z.string().trim().max(300).optional(),
35
+ smartCollectionId: SmartCollectionIdSchema.optional(),
36
+ }),
37
+ z.object({
38
+ mode: z.literal("check"),
39
+ slug: z.string().trim().toLowerCase().min(1).max(200),
40
+ smartCollectionId: SmartCollectionIdSchema.optional(),
41
+ }),
42
+ ]);
43
+
44
+ const PreviewQuerySchema = z.object({
45
+ lang: ContentLanguageSchema.optional(),
46
+ });
47
+
48
+ // --- Address ---------------------------------------------------------------
49
+ // Declared before `/:id` so the literal path wins.
50
+
51
+ smartCollectionsApiRoutes.get("/slug", requireAuthApi(), async (c) => {
52
+ const query = parseValidated(SlugQuerySchema, c.req.query());
53
+
54
+ if (query.mode === "suggest") {
55
+ const slug = await c.var.services.smartCollections.suggestSlug({
56
+ title: query.title,
57
+ excludeId: query.smartCollectionId,
58
+ });
59
+ return c.json({ slug });
60
+ }
61
+
62
+ const available = await c.var.services.smartCollections.checkSlugAvailability(
63
+ query.slug,
64
+ query.smartCollectionId,
65
+ );
66
+ return c.json({ slug: query.slug, available });
67
+ });
68
+
69
+ // --- Preview ---------------------------------------------------------------
70
+
71
+ /**
72
+ * How many threads a set of conditions would gather, against the site total.
73
+ *
74
+ * POST rather than GET: the conditions are a typed body in the same shape the
75
+ * create endpoint validates, and putting them in a URL would mean inventing a
76
+ * second spelling for them.
77
+ */
78
+ smartCollectionsApiRoutes.post("/preview", requireAuthApi(), async (c) => {
79
+ const body = parseValidated(SmartCollectionPreviewSchema, await c.req.json());
80
+ const query = parseValidated(PreviewQuerySchema, c.req.query());
81
+
82
+ const { count, baseline } = await c.var.services.smartCollections.preview(
83
+ body.selection ?? {},
84
+ { isAuthenticated: c.var.isAuthenticated, lang: query.lang },
85
+ );
86
+
87
+ c.header("Cache-Control", "no-store");
88
+ return c.json({ count, baseline });
89
+ });
90
+
91
+ // --- CRUD ------------------------------------------------------------------
92
+
93
+ smartCollectionsApiRoutes.get("/", requireAuthApi(), async (c) => {
94
+ const query = parseValidated(PreviewQuerySchema, c.req.query());
95
+ const smartCollections =
96
+ await c.var.services.smartCollections.listDirectoryEntries({
97
+ isAuthenticated: c.var.isAuthenticated,
98
+ lang: query.lang,
99
+ });
100
+ return c.json({ smartCollections });
101
+ });
102
+
103
+ smartCollectionsApiRoutes.post("/", requireAuthApi(), async (c) => {
104
+ const body = parseValidated(CreateSmartCollectionSchema, await c.req.json());
105
+ const smartCollection = await c.var.services.smartCollections.create(body);
106
+ return c.json({ smartCollection }, 201);
107
+ });
108
+
109
+ smartCollectionsApiRoutes.get("/:id", requireAuthApi(), async (c) => {
110
+ const id = parseIdParam(c.req.param("id"), ID_PREFIX.smartCollection);
111
+ const smartCollection = await c.var.services.smartCollections.getById(id);
112
+ assertFound(smartCollection, "Smart collection");
113
+ return c.json({ smartCollection });
114
+ });
115
+
116
+ smartCollectionsApiRoutes.put("/:id", requireAuthApi(), async (c) => {
117
+ const id = parseIdParam(c.req.param("id"), ID_PREFIX.smartCollection);
118
+ const body = parseValidated(UpdateSmartCollectionSchema, await c.req.json());
119
+ const smartCollection = await c.var.services.smartCollections.update(
120
+ id,
121
+ body,
122
+ );
123
+ assertFound(smartCollection, "Smart collection");
124
+ return c.json({ smartCollection });
125
+ });
126
+
127
+ smartCollectionsApiRoutes.delete("/:id", requireAuthApi(), async (c) => {
128
+ const id = parseIdParam(c.req.param("id"), ID_PREFIX.smartCollection);
129
+ const deleted = await c.var.services.smartCollections.delete(id);
130
+ assertFound(deleted || null, "Smart collection");
131
+ return c.json({ success: true });
132
+ });
@@ -25,6 +25,7 @@ import {
25
25
  getTelegramWebhookSecret,
26
26
  } from "../../lib/env.js";
27
27
  import { timingSafeEqualText } from "../../lib/crypto.js";
28
+ import { runDeferred } from "../../lib/deferred.js";
28
29
  import {
29
30
  answerCallbackQuery,
30
31
  buildDeepLink,
@@ -316,7 +317,7 @@ async function processUpdate(
316
317
  posterFileId: media.posterFileId ?? null,
317
318
  });
318
319
 
319
- runDeferred(c, async () => {
320
+ runDeferred(c, "Telegram", async () => {
320
321
  await sleep(ALBUM_BUFFER_DELAY_MS);
321
322
  const claimed = await telegram.claimAlbumGroup(botId, mediaGroupId);
322
323
  if (claimed.length === 0) return;
@@ -371,45 +372,6 @@ function sleep(ms: number): Promise<void> {
371
372
  return new Promise((resolve) => setTimeout(resolve, ms));
372
373
  }
373
374
 
374
- /**
375
- * Schedules background work that must outlive the current HTTP response so
376
- * Telegram can deliver the next webhook in this chat without waiting.
377
- *
378
- * On Cloudflare Workers `c.executionCtx.waitUntil` keeps the worker alive
379
- * until the promise settles. In Node / tests there's no such API, so we just
380
- * let the promise float — Node's event loop keeps running it. Either way the
381
- * promise has its own try/catch so unhandled rejections never bubble up.
382
- *
383
- * Background work also tracks the pending-promises array we register on the
384
- * env binding for tests so a vitest can `await` them after advancing timers.
385
- */
386
- function runDeferred(
387
- c: {
388
- env: Bindings;
389
- executionCtx?: { waitUntil: (promise: Promise<unknown>) => void };
390
- },
391
- work: () => Promise<void>,
392
- ): void {
393
- const promise = work().catch((err) => {
394
- const message = err instanceof Error ? err.message : String(err);
395
- // eslint-disable-next-line no-console -- Background failures must be visible.
396
- console.error(`[Jant] Telegram background error: ${message}`);
397
- });
398
- try {
399
- c.executionCtx?.waitUntil(promise);
400
- } catch {
401
- // executionCtx not available (e.g. Node, tests) — promise still runs.
402
- }
403
- const pending = (
404
- c.env as Bindings & {
405
- __telegramPending?: Promise<unknown>[];
406
- }
407
- ).__telegramPending;
408
- if (Array.isArray(pending)) {
409
- pending.push(promise);
410
- }
411
- }
412
-
413
375
  /**
414
376
  * Convert a message's caption + caption_entities into markdown, returning
415
377
  * `null` when there's nothing to record. Centralized so single-media and album
@@ -39,7 +39,7 @@ import {
39
39
  } from "../../lib/errors.js";
40
40
  import { getI18n } from "../../i18n/index.js";
41
41
  import { ID_PREFIX } from "../../lib/ids.js";
42
- import { parseValidated } from "../../lib/schemas.js";
42
+ import { MediaIdSchema, parseValidated } from "../../lib/schemas.js";
43
43
  import { toApiMedia } from "../../lib/api-media.js";
44
44
 
45
45
  type Env = { Bindings: Bindings; Variables: AppVariables };
@@ -49,6 +49,7 @@ export const uploadApiRoutes = new Hono<Env>();
49
49
  const ListMediaQuerySchema = z.object({
50
50
  limit: z.coerce.number().int().min(1).max(200).optional().default(50),
51
51
  mimePrefix: z.string().trim().min(1).optional(),
52
+ cursor: MediaIdSchema.optional(),
52
53
  });
53
54
 
54
55
  const UpdateMediaSchema = z.object({
@@ -421,14 +422,20 @@ uploadApiRoutes.post("/", async (c) => {
421
422
 
422
423
  // List uploaded files (JSON only)
423
424
  uploadApiRoutes.get("/", async (c) => {
424
- const { limit, mimePrefix } = parseValidated(
425
+ const { limit, mimePrefix, cursor } = parseValidated(
425
426
  ListMediaQuerySchema,
426
427
  c.req.query(),
427
428
  );
428
- const mediaList = await c.var.services.media.list({ limit, mimePrefix });
429
+ const mediaList = await c.var.services.media.list({
430
+ limit,
431
+ mimePrefix,
432
+ cursor,
433
+ });
429
434
 
430
435
  return c.json({
431
436
  media: mediaList.map((media) => toApiMedia(media, c.var.appConfig)),
437
+ nextCursor:
438
+ mediaList.length === limit ? (mediaList.at(-1)?.id ?? null) : null,
432
439
  });
433
440
  });
434
441
 
@@ -116,8 +116,7 @@ function scheduleExpiredUploadCleanup(
116
116
  .catch(() => {});
117
117
 
118
118
  let executionCtx:
119
- | { waitUntil: (promise: Promise<unknown>) => void }
120
- | undefined;
119
+ { waitUntil: (promise: Promise<unknown>) => void } | undefined;
121
120
  try {
122
121
  executionCtx = c.executionCtx;
123
122
  } catch {
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Tests for who may answer the last setup question on a provisioned site.
3
+ *
4
+ * A provisioned site already has an owner, so `POST /setup` writes the site's
5
+ * name and language on behalf of that owner. Carrying a session is not enough
6
+ * to be that owner — the session may belong to someone this site has never
7
+ * heard of.
8
+ */
9
+
10
+ import { describe, expect, it, vi } from "vitest";
11
+ import { Hono } from "hono";
12
+ import { createI18n } from "../../../i18n/index.js";
13
+ import { attachSession } from "../../../middleware/session.js";
14
+ import { ONBOARDING_STATUS } from "../../../lib/constants.js";
15
+ import { setupRoutes } from "../setup.js";
16
+ import type { Bindings } from "../../../types.js";
17
+ import type { AppVariables } from "../../../types/app-context.js";
18
+
19
+ type Env = { Bindings: Bindings; Variables: AppVariables };
20
+
21
+ function createMockAuth(signedIn: boolean) {
22
+ return {
23
+ api: {
24
+ getSession: async () => ({
25
+ headers: new Headers(),
26
+ response: signedIn
27
+ ? {
28
+ user: {
29
+ id: "usr_outsider",
30
+ email: "someone@test.com",
31
+ name: "S",
32
+ },
33
+ session: { id: "ses_outsider" },
34
+ }
35
+ : null,
36
+ }),
37
+ },
38
+ } as unknown as AppVariables["auth"];
39
+ }
40
+
41
+ function createProvisionedSetupApp(options: {
42
+ signedIn: boolean;
43
+ isMember: boolean;
44
+ }) {
45
+ const completeSiteSetup = vi.fn(async () => {});
46
+
47
+ const app = new Hono<Env>();
48
+ app.use("*", async (c, next) => {
49
+ c.env = {} as Bindings;
50
+ c.set("auth", createMockAuth(options.signedIn));
51
+ c.set("services", {
52
+ settings: {
53
+ getOnboardingStatus: async () => ONBOARDING_STATUS.PROVISIONED,
54
+ get: async () => "My Blog",
55
+ },
56
+ bootstrap: { completeSiteSetup },
57
+ siteMembers: {
58
+ get: async () =>
59
+ options.isMember
60
+ ? {
61
+ createdAt: 0,
62
+ role: "owner",
63
+ siteId: "sit_test",
64
+ updatedAt: 0,
65
+ userId: "usr_outsider",
66
+ }
67
+ : null,
68
+ },
69
+ } as unknown as AppVariables["services"]);
70
+ c.set("currentSite", {
71
+ createdAt: 0,
72
+ id: "sit_test",
73
+ key: "default",
74
+ status: "active",
75
+ updatedAt: 0,
76
+ });
77
+ c.set("appConfig", {
78
+ siteName: "Jant",
79
+ sitePathPrefix: "",
80
+ siteLanguage: "en",
81
+ } as AppVariables["appConfig"]);
82
+ c.set("lang", "en");
83
+ c.set("i18n", createI18n("en"));
84
+ await next();
85
+ });
86
+ app.use("*", attachSession());
87
+ app.route("/", setupRoutes);
88
+
89
+ return { app, completeSiteSetup };
90
+ }
91
+
92
+ function postLanguage(app: Hono<Env>) {
93
+ return app.request("/setup", {
94
+ method: "POST",
95
+ headers: { "Content-Type": "application/json" },
96
+ body: JSON.stringify({ contentLanguage: "zh-Hans" }),
97
+ });
98
+ }
99
+
100
+ describe("POST /setup on a provisioned site", () => {
101
+ it("refuses a session that is not a member of this site", async () => {
102
+ const { app, completeSiteSetup } = createProvisionedSetupApp({
103
+ signedIn: true,
104
+ isMember: false,
105
+ });
106
+
107
+ const res = await postLanguage(app);
108
+
109
+ await expect(res.text()).resolves.toContain("/signin");
110
+ expect(completeSiteSetup).not.toHaveBeenCalled();
111
+ });
112
+
113
+ it("lets the site's own owner answer", async () => {
114
+ const { app, completeSiteSetup } = createProvisionedSetupApp({
115
+ signedIn: true,
116
+ isMember: true,
117
+ });
118
+
119
+ await postLanguage(app);
120
+
121
+ expect(completeSiteSetup).toHaveBeenCalledOnce();
122
+ });
123
+ });
124
+
125
+ describe("GET /setup on a provisioned site", () => {
126
+ it("sends a non-member to the site instead of showing the form", async () => {
127
+ const { app } = createProvisionedSetupApp({
128
+ signedIn: true,
129
+ isMember: false,
130
+ });
131
+
132
+ const res = await app.request("/setup", { redirect: "manual" });
133
+
134
+ expect(res.status).toBe(302);
135
+ expect(res.headers.get("Location")).toBe("/");
136
+ });
137
+ });
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Tests for the Discover answer the setup screen collects.
3
+ *
4
+ * The point under test is that setup records an answer rather than inferring
5
+ * one — a ticked box and a cleared box both write, and a form that carried no
6
+ * field at all writes nothing, since that last case is an older client or a
7
+ * scripted setup and reading it as a refusal would opt sites out that never
8
+ * said so — and then acts on it, on the same terms as the settings page's own
9
+ * switch.
10
+ */
11
+
12
+ import { describe, expect, it, vi } from "vitest";
13
+ import { Hono } from "hono";
14
+ import { createI18n } from "../../../i18n/index.js";
15
+ import { attachSession } from "../../../middleware/session.js";
16
+ import { ONBOARDING_STATUS } from "../../../lib/constants.js";
17
+ import { setupRoutes } from "../setup.js";
18
+ import type { Bindings } from "../../../types.js";
19
+ import type { AppVariables } from "../../../types/app-context.js";
20
+
21
+ type Env = { Bindings: Bindings; Variables: AppVariables };
22
+
23
+ function createMockAuth() {
24
+ return {
25
+ api: {
26
+ getSession: async () => ({
27
+ headers: new Headers(),
28
+ response: {
29
+ user: { id: "usr_owner", email: "owner@test.com", name: "O" },
30
+ session: { id: "ses_owner" },
31
+ },
32
+ }),
33
+ },
34
+ } as unknown as AppVariables["auth"];
35
+ }
36
+
37
+ /**
38
+ * A provisioned site whose owner is answering. Both install kinds reach the
39
+ * Discover write through this one branch — a self-hosted site arrives here from
40
+ * its second screen, a hosted one from its only screen.
41
+ */
42
+ function createSetupApp(
43
+ options: { demoMode?: boolean; shouldAnnounce?: boolean } = {},
44
+ ) {
45
+ const completeSiteSetup = vi.fn(async () => {});
46
+ const updateDiscoverSetting = vi.fn(async () => ({
47
+ shouldAnnounce: options.shouldAnnounce ?? true,
48
+ }));
49
+ const announceToDiscover = vi.fn(async () => {});
50
+ // The hook `runDeferred` offers tests: every backgrounded promise lands
51
+ // here, so the announcement can be awaited instead of raced.
52
+ const pending: Promise<unknown>[] = [];
53
+
54
+ const app = new Hono<Env>();
55
+ app.use("*", async (c, next) => {
56
+ c.env = {
57
+ DISCOVER_PING_URL: "https://directory.test/api/discover/ping",
58
+ __pendingDeferred: pending,
59
+ } as unknown as Bindings;
60
+ c.set("auth", createMockAuth());
61
+ c.set("services", {
62
+ settings: {
63
+ getOnboardingStatus: async () => ONBOARDING_STATUS.PROVISIONED,
64
+ // Already named, so this is the hosted screen: language and Discover
65
+ // and nothing else.
66
+ get: async () => "My Blog",
67
+ updateDiscoverSetting,
68
+ announceToDiscover,
69
+ },
70
+ bootstrap: { completeSiteSetup },
71
+ siteMembers: {
72
+ get: async () => ({
73
+ createdAt: 0,
74
+ role: "owner",
75
+ siteId: "sit_test",
76
+ updatedAt: 0,
77
+ userId: "usr_owner",
78
+ }),
79
+ },
80
+ } as unknown as AppVariables["services"]);
81
+ c.set("currentSite", {
82
+ createdAt: 0,
83
+ id: "sit_test",
84
+ key: "default",
85
+ status: "active",
86
+ updatedAt: 0,
87
+ });
88
+ c.set("appConfig", {
89
+ siteName: "Jant",
90
+ sitePathPrefix: "",
91
+ siteLanguage: "en",
92
+ siteUrl: "https://example.com",
93
+ demoMode: options.demoMode ?? false,
94
+ noindex: false,
95
+ rssFeedsEnabled: true,
96
+ } as AppVariables["appConfig"]);
97
+ c.set("lang", "en");
98
+ c.set("i18n", createI18n("en"));
99
+ await next();
100
+ });
101
+ app.use("*", attachSession());
102
+ app.route("/", setupRoutes);
103
+
104
+ return { app, updateDiscoverSetting, announceToDiscover, pending };
105
+ }
106
+
107
+ function post(app: Hono<Env>, body: Record<string, unknown>) {
108
+ return app.request("/setup", {
109
+ method: "POST",
110
+ headers: { "Content-Type": "application/json" },
111
+ body: JSON.stringify({ contentLanguage: "zh-Hans", ...body }),
112
+ });
113
+ }
114
+
115
+ describe("POST /setup, the Discover answer", () => {
116
+ it("stores `latest` when the box was ticked", async () => {
117
+ const { app, updateDiscoverSetting } = createSetupApp();
118
+
119
+ await post(app, { discover: true });
120
+
121
+ expect(updateDiscoverSetting).toHaveBeenCalledWith("latest", {
122
+ demoMode: false,
123
+ });
124
+ });
125
+
126
+ // The half that matters on a deployment which lists by default: a cleared box
127
+ // has to be written, or `DISCOVER=latest` keeps deciding for someone who just
128
+ // said no.
129
+ it("stores `off` when the box was cleared", async () => {
130
+ const { app, updateDiscoverSetting } = createSetupApp();
131
+
132
+ await post(app, { discover: false });
133
+
134
+ expect(updateDiscoverSetting).toHaveBeenCalledWith("off", {
135
+ demoMode: false,
136
+ });
137
+ });
138
+
139
+ it("writes nothing when the form carried no answer", async () => {
140
+ const { app, updateDiscoverSetting } = createSetupApp();
141
+
142
+ await post(app, {});
143
+
144
+ expect(updateDiscoverSetting).not.toHaveBeenCalled();
145
+ });
146
+
147
+ // The answer takes effect now, not when the author next happens to open a
148
+ // settings page they have no reason to open. A directory decides eligibility
149
+ // for itself and re-reads the feed on its own schedule, so a site with
150
+ // nothing published yet loses nothing by saying hello early.
151
+ it("announces the site when the box was ticked", async () => {
152
+ const { app, announceToDiscover, pending } = createSetupApp();
153
+
154
+ await post(app, { discover: true });
155
+ await Promise.all(pending);
156
+
157
+ expect(announceToDiscover).toHaveBeenCalledWith({
158
+ endpoint: "https://directory.test/api/discover/ping",
159
+ feedUrl: "https://example.com/latest/feed",
160
+ });
161
+ });
162
+
163
+ // Not awaited: a directory takes up to twelve seconds to give up on, and the
164
+ // last screen of setup is not a place to spend them. The outcome is recorded
165
+ // for the settings page's status block to read back.
166
+ it("does not hold the response while the directory answers", async () => {
167
+ const { app, announceToDiscover, pending } = createSetupApp();
168
+ let settled = false;
169
+ announceToDiscover.mockImplementation(async () => {
170
+ await new Promise((resolve) => setTimeout(resolve, 20));
171
+ settled = true;
172
+ });
173
+
174
+ await post(app, { discover: true });
175
+
176
+ expect(settled).toBe(false);
177
+ await Promise.all(pending);
178
+ expect(settled).toBe(true);
179
+ });
180
+
181
+ // Nothing to tell a directory that was never told anything: going from unset
182
+ // to `off` leaves the site exactly as unlisted as it already was.
183
+ it("announces nothing when the setting service reports no change", async () => {
184
+ const { app, announceToDiscover, pending } = createSetupApp({
185
+ shouldAnnounce: false,
186
+ });
187
+
188
+ await post(app, { discover: false });
189
+ await Promise.all(pending);
190
+
191
+ expect(announceToDiscover).not.toHaveBeenCalled();
192
+ });
193
+
194
+ it("announces nothing from a demo site", async () => {
195
+ const { app, announceToDiscover, pending } = createSetupApp({
196
+ demoMode: true,
197
+ });
198
+
199
+ await post(app, { discover: true });
200
+ await Promise.all(pending);
201
+
202
+ expect(announceToDiscover).not.toHaveBeenCalled();
203
+ });
204
+ });