@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
@@ -18,6 +18,7 @@ import {
18
18
  siteDomains,
19
19
  siteMembers,
20
20
  sites,
21
+ smartCollections,
21
22
  user,
22
23
  verification,
23
24
  } from "../../db/schema.js";
@@ -172,6 +173,56 @@ describe("AuthService", () => {
172
173
  createdAt: timestamp,
173
174
  });
174
175
 
176
+ // A smart collection that filters *by* the collection above, with every row
177
+ // that hangs off it. This is the shape a factory reset used to die on: the
178
+ // condition column once carried `ON DELETE restrict`, so clearing
179
+ // `collection` threw halfway through a reset that runs without a
180
+ // transaction on SQLite and D1, leaving a half-erased site behind.
181
+ await db.insert(smartCollections).values({
182
+ id: "smc_01km9authdelete00000000000",
183
+ siteId: DEFAULT_TEST_SITE_ID,
184
+ title: "Untitled notes",
185
+ collectionId: "col_01km9authdelete00000000000",
186
+ format: "note",
187
+ hasTitle: false,
188
+ sort: "newest",
189
+ createdAt: timestamp,
190
+ updatedAt: timestamp,
191
+ });
192
+
193
+ await db.insert(pathRegistry).values({
194
+ id: "pth_01km9authsmart0000000000000",
195
+ siteId: DEFAULT_TEST_SITE_ID,
196
+ path: "untitled-notes",
197
+ kind: "slug",
198
+ smartCollectionId: "smc_01km9authdelete00000000000",
199
+ createdAt: timestamp,
200
+ updatedAt: timestamp,
201
+ });
202
+
203
+ await db.insert(collectionDirectoryItems).values({
204
+ id: "cdi_01km9authsmart0000000000000",
205
+ siteId: DEFAULT_TEST_SITE_ID,
206
+ type: "smart_collection",
207
+ smartCollectionId: "smc_01km9authdelete00000000000",
208
+ label: null,
209
+ position: "a1",
210
+ createdAt: timestamp,
211
+ updatedAt: timestamp,
212
+ });
213
+
214
+ await db.insert(navItems).values({
215
+ id: "nav_01km9authsmart0000000000000",
216
+ siteId: DEFAULT_TEST_SITE_ID,
217
+ type: "smart_collection",
218
+ smartCollectionId: "smc_01km9authdelete00000000000",
219
+ label: "",
220
+ url: "/untitled-notes",
221
+ position: "a1",
222
+ createdAt: timestamp,
223
+ updatedAt: timestamp,
224
+ });
225
+
175
226
  await db.insert(navItems).values({
176
227
  id: "nav_01km9authdelete00000000000",
177
228
  siteId: DEFAULT_TEST_SITE_ID,
@@ -241,6 +292,9 @@ describe("AuthService", () => {
241
292
  const remainingPosts = await db.select().from(posts);
242
293
  const remainingMedia = await db.select().from(media);
243
294
  const remainingCollections = await db.select().from(collections);
295
+ const remainingSmartCollections = await db
296
+ .select()
297
+ .from(smartCollections);
244
298
  const remainingPaths = await db.select().from(pathRegistry);
245
299
  const remainingDirectoryItems = await db
246
300
  .select()
@@ -262,6 +316,7 @@ describe("AuthService", () => {
262
316
  expect(remainingPosts).toHaveLength(0);
263
317
  expect(remainingMedia).toHaveLength(0);
264
318
  expect(remainingCollections).toHaveLength(0);
319
+ expect(remainingSmartCollections).toHaveLength(0);
265
320
  expect(remainingPaths).toHaveLength(0);
266
321
  expect(remainingDirectoryItems).toHaveLength(0);
267
322
  expect(remainingThreadCollections).toHaveLength(0);
@@ -287,12 +342,16 @@ describe("AuthService", () => {
287
342
  const remainingSiteMembers = await db.select().from(siteMembers);
288
343
  const remainingUsers = await db.select().from(user);
289
344
  const remainingPosts = await db.select().from(posts);
345
+ const remainingSmartCollections = await db
346
+ .select()
347
+ .from(smartCollections);
290
348
  const remainingSettings = await db.select().from(settingsTable);
291
349
  const existingSites = await db.select().from(sites);
292
350
  const existingSiteDomains = await db.select().from(siteDomains);
293
351
  expect(remainingSiteMembers).toHaveLength(0);
294
352
  expect(remainingUsers).toHaveLength(0);
295
353
  expect(remainingPosts).toHaveLength(0);
354
+ expect(remainingSmartCollections).toHaveLength(0);
296
355
  expect(remainingSettings).toHaveLength(0);
297
356
  expect(existingSites).toHaveLength(0);
298
357
  expect(existingSiteDomains).toHaveLength(0);
@@ -305,22 +364,72 @@ describe("AuthService", () => {
305
364
  db = testDb.db as unknown as Database;
306
365
  });
307
366
 
308
- it("validates delete CSRF tokens in Node runtimes", async () => {
367
+ function createTokenService(authSecret = "test-auth-secret") {
309
368
  const settingsService = createSettingsService(db, DEFAULT_TEST_SITE_ID);
310
- const authService = createAuthService(db, settingsService, {
369
+ return createAuthService(db, settingsService, {
311
370
  databaseDialect: "pg",
371
+ authSecret,
312
372
  });
373
+ }
374
+
375
+ it("validates delete CSRF tokens in Node runtimes", async () => {
376
+ const authService = createTokenService();
313
377
 
314
- const token = await authService.generateDeleteCsrfToken();
378
+ const token = await authService.generateDeleteCsrfToken("ses_1");
315
379
 
316
- await expect(authService.validateDeleteCsrfToken(token)).resolves.toBe(
317
- true,
318
- );
319
380
  await expect(
320
- authService.validateDeleteCsrfToken(`${token}nope`),
381
+ authService.validateDeleteCsrfToken(token, "ses_1"),
382
+ ).resolves.toBe(true);
383
+ await expect(
384
+ authService.validateDeleteCsrfToken(`${token}nope`, "ses_1"),
321
385
  ).resolves.toBe(false);
322
386
  });
323
387
 
388
+ it("issues the same delete token however often a session asks", async () => {
389
+ // Rendering the delete page is a GET. Issuing twice must not invalidate
390
+ // the copy already sitting in an open tab — that turned any prefetch or
391
+ // browser extension re-fetching the page into a dead delete button.
392
+ const authService = createTokenService();
393
+
394
+ const first = await authService.generateDeleteCsrfToken("ses_1");
395
+ const second = await authService.generateDeleteCsrfToken("ses_1");
396
+
397
+ expect(second).toBe(first);
398
+ await expect(
399
+ authService.validateDeleteCsrfToken(first, "ses_1"),
400
+ ).resolves.toBe(true);
401
+ });
402
+
403
+ it("refuses a delete token minted for another session", async () => {
404
+ const authService = createTokenService();
405
+
406
+ const token = await authService.generateDeleteCsrfToken("ses_1");
407
+
408
+ await expect(
409
+ authService.validateDeleteCsrfToken(token, "ses_2"),
410
+ ).resolves.toBe(false);
411
+ });
412
+
413
+ it("refuses a delete token minted under another signing key", async () => {
414
+ const token =
415
+ await createTokenService("secret-a").generateDeleteCsrfToken("ses_1");
416
+
417
+ await expect(
418
+ createTokenService("secret-b").validateDeleteCsrfToken(token, "ses_1"),
419
+ ).resolves.toBe(false);
420
+ });
421
+
422
+ it("refuses to mint a delete token without a signing key", async () => {
423
+ const settingsService = createSettingsService(db, DEFAULT_TEST_SITE_ID);
424
+ const authService = createAuthService(db, settingsService, {
425
+ databaseDialect: "pg",
426
+ });
427
+
428
+ await expect(
429
+ authService.generateDeleteCsrfToken("ses_1"),
430
+ ).rejects.toThrow(/AUTH_SECRET/);
431
+ });
432
+
324
433
  it("validates password reset tokens in Node runtimes", async () => {
325
434
  const settingsService = createSettingsService(db, DEFAULT_TEST_SITE_ID);
326
435
  const authService = createAuthService(db, settingsService, {
@@ -0,0 +1,230 @@
1
+ import { sql } from "drizzle-orm";
2
+ import { beforeEach, describe, expect, it } from "vitest";
3
+ import { createTestDatabase } from "../../__tests__/helpers/db.js";
4
+ import type { Database } from "../../db/index.js";
5
+ import {
6
+ account,
7
+ navItems,
8
+ settings,
9
+ siteDomains,
10
+ siteMembers,
11
+ sites,
12
+ user,
13
+ } from "../../db/schema.js";
14
+ import { verifyPassword } from "../../lib/password.js";
15
+ import { createBootstrapService } from "../bootstrap.js";
16
+ import { createSiteService, TRANSIENT_SINGLE_SITE_ID } from "../site.js";
17
+
18
+ const SNAPSHOT_SITE_ID = "sit_01kn8jq3t4famtyg9hjd074ckr";
19
+ const OWNER = { email: "owner@example.com", password: "correct horse" };
20
+
21
+ async function readSettings(db: Database, siteId: string) {
22
+ const rows = await db.select().from(settings);
23
+ return Object.fromEntries(
24
+ rows
25
+ .filter((row) => row.siteId === siteId)
26
+ .map((row) => [row.key, row.value]),
27
+ );
28
+ }
29
+
30
+ async function storedPasswordHash(db: Database) {
31
+ const rows = await db.select({ password: account.password }).from(account);
32
+ expect(rows).toHaveLength(1);
33
+ return rows[0]?.password ?? "";
34
+ }
35
+
36
+ describe("EnsureSingleSiteOptions.id", () => {
37
+ let db: Database;
38
+
39
+ beforeEach(async () => {
40
+ db = createTestDatabase().db as unknown as Database;
41
+ await db.run(sql`DELETE FROM "site"`);
42
+ });
43
+
44
+ it("creates the site with the given id", async () => {
45
+ const { site } = await createSiteService(db).ensureSingleSite({
46
+ id: SNAPSHOT_SITE_ID,
47
+ });
48
+
49
+ expect(site.id).toBe(SNAPSHOT_SITE_ID);
50
+ });
51
+
52
+ // Like the host and key, the id says what a new site is created with. It is
53
+ // never a request to rename a site that exists.
54
+ it("leaves an existing site's id alone", async () => {
55
+ const { site: existing } = await createSiteService(db).ensureSingleSite();
56
+ const { site } = await createSiteService(db).ensureSingleSite({
57
+ id: SNAPSHOT_SITE_ID,
58
+ });
59
+
60
+ expect(site.id).toBe(existing.id);
61
+ expect(await db.select().from(sites)).toHaveLength(1);
62
+ });
63
+ });
64
+
65
+ describe("BootstrapService.setUpInstance", () => {
66
+ let db: Database;
67
+
68
+ function bootstrap(options?: Parameters<typeof createBootstrapService>[2]) {
69
+ return createBootstrapService(db, TRANSIENT_SINGLE_SITE_ID, options);
70
+ }
71
+
72
+ beforeEach(async () => {
73
+ db = createTestDatabase().db as unknown as Database;
74
+ await db.run(sql`DELETE FROM "site"`);
75
+ });
76
+
77
+ it("leaves a fresh install set up, the way both screens would", async () => {
78
+ const result = await bootstrap().setUpInstance({
79
+ ...OWNER,
80
+ siteId: SNAPSHOT_SITE_ID,
81
+ siteName: "Field Notes",
82
+ siteLanguage: "zh-Hans",
83
+ timeZone: "Asia/Shanghai",
84
+ });
85
+
86
+ expect(result).toEqual({ outcome: "created", siteId: SNAPSHOT_SITE_ID });
87
+
88
+ const [owner] = await db.select().from(user);
89
+ expect(owner).toMatchObject({
90
+ email: OWNER.email,
91
+ name: "Field Notes",
92
+ role: "admin",
93
+ });
94
+ expect(
95
+ await verifyPassword({
96
+ hash: await storedPasswordHash(db),
97
+ password: OWNER.password,
98
+ }),
99
+ ).toBe(true);
100
+
101
+ expect(await db.select().from(siteMembers)).toEqual([
102
+ expect.objectContaining({
103
+ siteId: SNAPSHOT_SITE_ID,
104
+ userId: owner?.id,
105
+ role: "owner",
106
+ }),
107
+ ]);
108
+ expect(await db.select().from(navItems)).toHaveLength(5);
109
+ expect(await readSettings(db, SNAPSHOT_SITE_ID)).toMatchObject({
110
+ ONBOARDING_STATUS: "completed",
111
+ SITE_NAME: "Field Notes",
112
+ SITE_LANGUAGE: "zh-Hans",
113
+ TIME_ZONE: "Asia/Shanghai",
114
+ });
115
+ });
116
+
117
+ // An empty answer on the screens: the base locale and UTC, and no name, so
118
+ // the site keeps its fallback and the account keeps the address's stand-in.
119
+ it("gives unanswered questions the screens' defaults", async () => {
120
+ const result = await bootstrap().setUpInstance(OWNER);
121
+
122
+ expect(result.outcome).toBe("created");
123
+ expect(result.siteId).toMatch(/^sit_/);
124
+ const stored = await readSettings(db, result.siteId);
125
+ expect(stored).toMatchObject({
126
+ ONBOARDING_STATUS: "completed",
127
+ SITE_LANGUAGE: "en",
128
+ TIME_ZONE: "UTC",
129
+ });
130
+ expect(stored.SITE_NAME).toBeUndefined();
131
+ expect((await db.select().from(user))[0]?.name).toBe("owner");
132
+ });
133
+
134
+ it("gives the site its configured domain", async () => {
135
+ await createBootstrapService(db, TRANSIENT_SINGLE_SITE_ID, {
136
+ bootstrapSite: { host: "notes.example.com" },
137
+ }).setUpInstance(OWNER);
138
+
139
+ expect(await db.select().from(siteDomains)).toEqual([
140
+ expect.objectContaining({ host: "notes.example.com", kind: "primary" }),
141
+ ]);
142
+ });
143
+
144
+ // A deployment runs this on every start. The owner may have changed the
145
+ // password since, and a restart must not quietly put the old one back.
146
+ it("changes nothing on an install that finished setup", async () => {
147
+ const first = await bootstrap().setUpInstance({
148
+ ...OWNER,
149
+ siteName: "Field Notes",
150
+ });
151
+ const hashBefore = await storedPasswordHash(db);
152
+
153
+ const second = await bootstrap().setUpInstance({
154
+ email: "someone-else@example.com",
155
+ password: "a different password",
156
+ siteName: "Renamed",
157
+ });
158
+
159
+ expect(second).toEqual({
160
+ outcome: "already-set-up",
161
+ siteId: first.siteId,
162
+ });
163
+ expect(await storedPasswordHash(db)).toBe(hashBefore);
164
+ expect(await db.select().from(user)).toHaveLength(1);
165
+ expect((await readSettings(db, first.siteId)).SITE_NAME).toBe(
166
+ "Field Notes",
167
+ );
168
+ });
169
+
170
+ it("refuses a site id that is not the install's", async () => {
171
+ await bootstrap().setUpInstance(OWNER);
172
+
173
+ await expect(
174
+ bootstrap().setUpInstance({ ...OWNER, siteId: SNAPSHOT_SITE_ID }),
175
+ ).rejects.toThrow(`not ${SNAPSHOT_SITE_ID}`);
176
+ });
177
+
178
+ describe("after a run that stopped once the account existed", () => {
179
+ beforeEach(async () => {
180
+ await bootstrap().setUpInstance(OWNER);
181
+ // Undo everything past the account, which is where an interrupted run
182
+ // leaves an install.
183
+ await db.run(sql`DELETE FROM "site"`);
184
+ });
185
+
186
+ it("finishes with the same address and password", async () => {
187
+ const result = await bootstrap().setUpInstance({
188
+ ...OWNER,
189
+ siteId: SNAPSHOT_SITE_ID,
190
+ siteName: "Field Notes",
191
+ });
192
+
193
+ expect(result).toEqual({ outcome: "resumed", siteId: SNAPSHOT_SITE_ID });
194
+ expect(await db.select().from(user)).toHaveLength(1);
195
+ expect((await db.select().from(user))[0]?.name).toBe("Field Notes");
196
+ expect((await readSettings(db, SNAPSHOT_SITE_ID)).ONBOARDING_STATUS).toBe(
197
+ "completed",
198
+ );
199
+ });
200
+
201
+ it("refuses the wrong password", async () => {
202
+ await expect(
203
+ bootstrap().setUpInstance({ ...OWNER, password: "not the password" }),
204
+ ).rejects.toThrow("does not match");
205
+ expect(await db.select().from(sites)).toHaveLength(0);
206
+ });
207
+
208
+ it("refuses another address", async () => {
209
+ await expect(
210
+ bootstrap().setUpInstance({
211
+ email: "someone-else@example.com",
212
+ password: OWNER.password,
213
+ }),
214
+ ).rejects.toThrow("already has an account under another address");
215
+ expect(await db.select().from(user)).toHaveLength(1);
216
+ expect(await db.select().from(sites)).toHaveLength(0);
217
+ });
218
+ });
219
+
220
+ // A hosted site's owner arrives through the control plane's handoff, so
221
+ // this refuses before creating an account in a database every tenant shares.
222
+ it("refuses a host-based install before writing anything", async () => {
223
+ await expect(
224
+ bootstrap({ siteResolutionMode: "host-based" }).setUpInstance(OWNER),
225
+ ).rejects.toThrow("setUpInstance runs on self-hosted installs only");
226
+
227
+ expect(await db.select().from(user)).toHaveLength(0);
228
+ expect(await db.select().from(sites)).toHaveLength(0);
229
+ });
230
+ });
@@ -267,6 +267,102 @@ describe("CollectionService", () => {
267
267
  });
268
268
 
269
269
  describe("listDirectoryData", () => {
270
+ it("counts only what this reader can see", async () => {
271
+ const reading = await collectionService.create({
272
+ slug: "reading",
273
+ title: "Reading",
274
+ });
275
+
276
+ const published = await postService.create({
277
+ format: "note",
278
+ bodyMarkdown: "Published",
279
+ });
280
+ const draft = await postService.create({
281
+ format: "note",
282
+ bodyMarkdown: "Draft",
283
+ status: "draft",
284
+ });
285
+ const secret = await postService.create({
286
+ format: "note",
287
+ bodyMarkdown: "Private",
288
+ visibility: "private",
289
+ });
290
+ for (const post of [published, draft, secret]) {
291
+ await collectionService.addThread(reading.id, post.id);
292
+ }
293
+
294
+ // A stranger who could subtract the directory's number from the
295
+ // collection page's would learn how much unpublished work is in here.
296
+ const anonymous = await collectionService.listDirectoryData({
297
+ isAuthenticated: false,
298
+ });
299
+ expect(anonymous.collections[0]?.threadCount).toBe(1);
300
+ expect(anonymous.collections[0]?.recentActivityAt).toBe(
301
+ published.lastActivityAt,
302
+ );
303
+
304
+ const author = await collectionService.listDirectoryData({
305
+ isAuthenticated: true,
306
+ });
307
+ expect(author.collections[0]?.threadCount).toBe(2);
308
+ });
309
+
310
+ it("counts a Thread once, whichever of its posts matched", async () => {
311
+ const reading = await collectionService.create({
312
+ slug: "reading",
313
+ title: "Reading",
314
+ });
315
+ const root = await postService.create({
316
+ format: "note",
317
+ bodyMarkdown: "Root",
318
+ });
319
+ await postService.create({
320
+ format: "note",
321
+ bodyMarkdown: "Reply",
322
+ replyToId: root.id,
323
+ });
324
+ await collectionService.addThread(reading.id, root.id);
325
+
326
+ const directory = await collectionService.listDirectoryData({
327
+ isAuthenticated: false,
328
+ });
329
+ expect(directory.collections[0]?.threadCount).toBe(1);
330
+ });
331
+
332
+ it("narrows counts to the view's language", async () => {
333
+ const reading = await collectionService.create({
334
+ slug: "reading",
335
+ title: "Reading",
336
+ });
337
+ const english = await postService.create({
338
+ format: "note",
339
+ bodyMarkdown: "English",
340
+ language: "en",
341
+ });
342
+ const japanese = await postService.create({
343
+ format: "note",
344
+ bodyMarkdown: "Japanese",
345
+ language: "ja",
346
+ });
347
+ for (const post of [english, japanese]) {
348
+ await collectionService.addThread(reading.id, post.id);
349
+ }
350
+
351
+ const englishView = await collectionService.listDirectoryData({
352
+ isAuthenticated: false,
353
+ lang: "en",
354
+ });
355
+ expect(englishView.collections[0]?.threadCount).toBe(1);
356
+
357
+ // The row itself stays listed in every view — only its number narrows.
358
+ const japaneseView = await collectionService.listDirectoryData({
359
+ isAuthenticated: false,
360
+ lang: "ja",
361
+ });
362
+ expect(japaneseView.collections).toHaveLength(1);
363
+ expect(japaneseView.collections[0]?.threadCount).toBe(1);
364
+ });
365
+
270
366
  it("returns collections with recent activity and labeled dividers", async () => {
271
367
  const reading = await collectionService.create({
272
368
  slug: "reading",
@@ -283,7 +379,9 @@ describe("CollectionService", () => {
283
379
  });
284
380
  await collectionService.addThread(reading.id, post.id);
285
381
 
286
- const directory = await collectionService.listDirectoryData();
382
+ const directory = await collectionService.listDirectoryData({
383
+ isAuthenticated: false,
384
+ });
287
385
 
288
386
  expect(directory.collections).toHaveLength(1);
289
387
  expect(directory.collections[0]?.recentActivityAt).toBe(
@@ -329,7 +427,9 @@ describe("CollectionService", () => {
329
427
  vi.setSystemTime(new Date("2024-01-01T00:01:00Z"));
330
428
  await postService.update(root.id, { bodyMarkdown: "after" });
331
429
 
332
- const directory = await collectionService.listDirectoryData();
430
+ const directory = await collectionService.listDirectoryData({
431
+ isAuthenticated: false,
432
+ });
333
433
  expect(directory.collections[0]?.recentActivityAt).toBe(1704067200);
334
434
  } finally {
335
435
  vi.useRealTimers();
@@ -357,7 +457,9 @@ describe("CollectionService", () => {
357
457
  replyToId: root.id,
358
458
  });
359
459
 
360
- const directory = await collectionService.listDirectoryData();
460
+ const directory = await collectionService.listDirectoryData({
461
+ isAuthenticated: false,
462
+ });
361
463
  expect(directory.collections[0]?.recentActivityAt).toBe(1704067260);
362
464
  } finally {
363
465
  vi.useRealTimers();
@@ -371,7 +473,9 @@ describe("CollectionService", () => {
371
473
  url: "/archive?format=quote",
372
474
  });
373
475
 
374
- const directory = await collectionService.listDirectoryData();
476
+ const directory = await collectionService.listDirectoryData({
477
+ isAuthenticated: false,
478
+ });
375
479
 
376
480
  expect(directory.directoryItems).toContainEqual(
377
481
  expect.objectContaining({
@@ -1,4 +1,5 @@
1
1
  import { describe, it, expect, beforeEach } from "vitest";
2
+ import { sql } from "drizzle-orm";
2
3
  import {
3
4
  createTestDatabase,
4
5
  DEFAULT_TEST_SITE_ID,
@@ -96,6 +97,39 @@ describe("CustomUrlService", () => {
96
97
  });
97
98
  });
98
99
 
100
+ // Postgres gives no order among rows that tie on the sort key, so paging
101
+ // by created_at alone repeated some custom URLs and never showed others
102
+ // (482 rows listed, 478 distinct, on a real site).
103
+ describe("list order", () => {
104
+ it("orders custom URLs created in the same second by ID", async () => {
105
+ const created = [];
106
+ for (const path of ["a-page", "b-page", "c-page"]) {
107
+ created.push(
108
+ await customUrlService.create({
109
+ path,
110
+ targetType: "redirect",
111
+ toPath: "/target",
112
+ redirectType: 301,
113
+ }),
114
+ );
115
+ }
116
+ await db.run(sql`UPDATE path_registry SET created_at = 1000`);
117
+
118
+ const listed = await customUrlService.list();
119
+ const expected = created
120
+ .map((url) => url.id)
121
+ .sort()
122
+ .reverse();
123
+ expect(listed.map((url) => url.id)).toEqual(expected);
124
+
125
+ const pages = [
126
+ ...(await customUrlService.list({ limit: 2, offset: 0 })),
127
+ ...(await customUrlService.list({ limit: 2, offset: 2 })),
128
+ ];
129
+ expect(pages.map((url) => url.id)).toEqual(expected);
130
+ });
131
+ });
132
+
99
133
  describe("getByPath", () => {
100
134
  it("returns custom URL by path", async () => {
101
135
  await customUrlService.create({