@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
@@ -7,17 +7,23 @@ import { run } from "../../../bin/commands/reset-password.js";
7
7
  import { migrate } from "../runtime.js";
8
8
  import type { Bindings } from "../../types.js";
9
9
  import { createBootstrapService } from "../../services/bootstrap.js";
10
+ import { createSiteService } from "../../services/site.js";
10
11
  import { createNodeDatabase } from "../../db/index.js";
11
12
 
12
13
  describe("jant reset-password", () => {
13
14
  const tempDirs: string[] = [];
14
- const originalEnv = process.env.DATABASE_URL;
15
+ const originalEnv = {
16
+ DATABASE_URL: process.env.DATABASE_URL,
17
+ SITE_RESOLUTION_MODE: process.env.SITE_RESOLUTION_MODE,
18
+ };
15
19
 
16
20
  afterEach(async () => {
17
- if (originalEnv === undefined) {
18
- delete process.env.DATABASE_URL;
19
- } else {
20
- process.env.DATABASE_URL = originalEnv;
21
+ for (const [key, value] of Object.entries(originalEnv)) {
22
+ if (value === undefined) {
23
+ delete process.env[key];
24
+ } else {
25
+ process.env[key] = value;
26
+ }
21
27
  }
22
28
 
23
29
  await Promise.all(
@@ -37,7 +43,14 @@ describe("jant reset-password", () => {
37
43
 
38
44
  const sqlite = new Database(databasePath);
39
45
  try {
40
- const bootstrap = createBootstrapService(createNodeDatabase(sqlite));
46
+ const db = createNodeDatabase(sqlite);
47
+ // Each setup screen is its own request, bound to the site it resolved:
48
+ // none before the account step, the one it created after.
49
+ const bootstrapForRequest = async () =>
50
+ createBootstrapService(
51
+ db,
52
+ (await createSiteService(db).resolveSingleSite()).site.id,
53
+ );
41
54
  await sqlite
42
55
  .prepare(
43
56
  `
@@ -53,10 +66,17 @@ describe("jant reset-password", () => {
53
66
  new Date().toISOString(),
54
67
  new Date().toISOString(),
55
68
  );
56
- await bootstrap.completeInitialSetup({
69
+ await (
70
+ await bootstrapForRequest()
71
+ ).provisionOwnerAccount({
57
72
  ownerUserId: "usr_reset_password_test",
58
- siteName: "Reset Password Test",
59
73
  });
74
+ await (
75
+ await bootstrapForRequest()
76
+ ).completeSiteSetup(
77
+ { siteName: "Reset Password Test" },
78
+ { oldLanguage: "" },
79
+ );
60
80
  } finally {
61
81
  sqlite.close();
62
82
  }
@@ -88,4 +108,57 @@ describe("jant reset-password", () => {
88
108
  expect.stringMatching(/^Visit: \/reset\?token=[a-f0-9]{64}$/),
89
109
  );
90
110
  });
111
+
112
+ // A hosted database holds every tenant, so the token goes to the site the
113
+ // operator named — and nowhere when they named none.
114
+ it("writes the token to the site --site names on a host-based database", async () => {
115
+ const root = await mkdtemp(join(tmpdir(), "jant-reset-password-hosted-"));
116
+ tempDirs.push(root);
117
+
118
+ const databasePath = join(root, "jant.sqlite");
119
+ await migrate({ DATABASE_URL: `file:${databasePath}` } as Bindings);
120
+ process.env.DATABASE_URL = `file:${databasePath}`;
121
+ process.env.SITE_RESOLUTION_MODE = "host-based";
122
+
123
+ const sqlite = new Database(databasePath);
124
+ try {
125
+ sqlite
126
+ .prepare(
127
+ `
128
+ INSERT INTO site (id, key, status, created_at, updated_at)
129
+ VALUES
130
+ ('sit_alpha000000000000000000000', 'alpha', 'active', 1774200000, 1774200000),
131
+ ('sit_beta0000000000000000000000', 'beta', 'active', 1774200001, 1774200001)
132
+ `,
133
+ )
134
+ .run();
135
+ } finally {
136
+ sqlite.close();
137
+ }
138
+
139
+ vi.spyOn(console, "log").mockImplementation(() => {});
140
+
141
+ await expect(run([])).rejects.toThrow(
142
+ "host-based mode needs a target site. Pass --site <key|id>, --host <host>, or --url <url>.",
143
+ );
144
+ await run(["--site", "beta"]);
145
+
146
+ const sqliteReadonly = new Database(databasePath, { readonly: true });
147
+ try {
148
+ const owners = sqliteReadonly
149
+ .prepare(
150
+ `
151
+ SELECT site_id
152
+ FROM "site_setting"
153
+ WHERE "key" = 'PASSWORD_RESET_TOKEN'
154
+ `,
155
+ )
156
+ .pluck()
157
+ .all();
158
+
159
+ expect(owners).toEqual(["sit_beta0000000000000000000000"]);
160
+ } finally {
161
+ sqliteReadonly.close();
162
+ }
163
+ });
91
164
  });
@@ -2,7 +2,7 @@ import { mkdtempSync, writeFileSync } from "node:fs";
2
2
  import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
- import { loadNodeEnvFile } from "../../../bin/lib/node-env.js";
5
+ import { findNodeEnvPath, loadNodeEnvFile } from "../../../bin/lib/node-env.js";
6
6
  import {
7
7
  formatRuntimeBanner,
8
8
  resolveCliRuntime,
@@ -106,6 +106,53 @@ describe("loadNodeEnvFile", () => {
106
106
  });
107
107
  });
108
108
 
109
+ describe("findNodeEnvPath", () => {
110
+ let tmpDir: string;
111
+
112
+ beforeEach(() => {
113
+ tmpDir = mkdtempSync(join(tmpdir(), "jant-env-path-"));
114
+ });
115
+
116
+ it("finds .env.node in the given directory when JANT_ENV_FILE is unset", () => {
117
+ const envPath = join(tmpDir, ".env.node");
118
+ writeFileSync(envPath, "DATABASE_URL=file:./jant.sqlite\n");
119
+
120
+ expect(findNodeEnvPath(tmpDir, {})).toBe(envPath);
121
+ });
122
+
123
+ it("loads nothing when JANT_ENV_FILE is empty", () => {
124
+ writeFileSync(
125
+ join(tmpDir, ".env.node"),
126
+ "DATABASE_URL=file:./local.sqlite\n",
127
+ );
128
+
129
+ expect(findNodeEnvPath(tmpDir, { JANT_ENV_FILE: "" })).toBeNull();
130
+ expect(findNodeEnvPath(tmpDir, { JANT_ENV_FILE: " " })).toBeNull();
131
+ });
132
+
133
+ it("uses the file JANT_ENV_FILE names, resolved against the directory", () => {
134
+ writeFileSync(
135
+ join(tmpDir, ".env.node"),
136
+ "DATABASE_URL=file:./local.sqlite\n",
137
+ );
138
+ const overridePath = join(tmpDir, "staging.env");
139
+ writeFileSync(overridePath, "DATABASE_URL=postgres://example/db\n");
140
+
141
+ expect(findNodeEnvPath(tmpDir, { JANT_ENV_FILE: "staging.env" })).toBe(
142
+ overridePath,
143
+ );
144
+ expect(findNodeEnvPath(tmpDir, { JANT_ENV_FILE: overridePath })).toBe(
145
+ overridePath,
146
+ );
147
+ });
148
+
149
+ it("throws when JANT_ENV_FILE names a file that does not exist", () => {
150
+ expect(() =>
151
+ findNodeEnvPath(tmpDir, { JANT_ENV_FILE: "missing.env" }),
152
+ ).toThrow(/JANT_ENV_FILE points at "missing.env"/);
153
+ });
154
+ });
155
+
109
156
  describe("formatRuntimeBanner", () => {
110
157
  const originalLog = console.log;
111
158
  beforeEach(() => {
@@ -0,0 +1,163 @@
1
+ import { mkdtemp, rm } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { Readable } from "node:stream";
5
+ import Database from "better-sqlite3";
6
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
7
+ import { run } from "../../../bin/commands/setup.js";
8
+ import { verifyPassword } from "../../lib/password.js";
9
+ import type { Bindings } from "../../types.js";
10
+ import { migrate } from "../runtime.js";
11
+
12
+ const SITE_ID = "sit_01kn8jq3t4famtyg9hjd074ckr";
13
+ const ENV_KEYS = [
14
+ "DATABASE_URL",
15
+ "DATA_DIR",
16
+ "SITE_ORIGIN",
17
+ "SITE_RESOLUTION_MODE",
18
+ ] as const;
19
+
20
+ function pipe(text: string) {
21
+ return { stdin: Readable.from([text]) };
22
+ }
23
+
24
+ describe("jant setup", () => {
25
+ const originalEnv = Object.fromEntries(
26
+ ENV_KEYS.map((key) => [key, process.env[key]]),
27
+ );
28
+ let root: string;
29
+ let databasePath: string;
30
+
31
+ beforeEach(async () => {
32
+ for (const key of ENV_KEYS) delete process.env[key];
33
+ root = await mkdtemp(join(tmpdir(), "jant-setup-"));
34
+ databasePath = join(root, "jant.sqlite");
35
+ await migrate({ DATABASE_URL: `file:${databasePath}` } as Bindings);
36
+ process.env.DATABASE_URL = `file:${databasePath}`;
37
+ vi.spyOn(console, "log").mockImplementation(() => {});
38
+ });
39
+
40
+ afterEach(async () => {
41
+ for (const [key, value] of Object.entries(originalEnv)) {
42
+ if (value === undefined) delete process.env[key];
43
+ else process.env[key] = value;
44
+ }
45
+ await rm(root, { recursive: true, force: true });
46
+ vi.restoreAllMocks();
47
+ });
48
+
49
+ function readDatabase<T>(read: (sqlite: Database.Database) => T): T {
50
+ const sqlite = new Database(databasePath, { readonly: true });
51
+ try {
52
+ return read(sqlite);
53
+ } finally {
54
+ sqlite.close();
55
+ }
56
+ }
57
+
58
+ it("sets up the install with the piped password", async () => {
59
+ process.env.SITE_ORIGIN = "https://notes.example.com";
60
+
61
+ const result = await run(
62
+ [
63
+ "--email",
64
+ "Owner@Example.com",
65
+ "--password-stdin",
66
+ "--site-id",
67
+ SITE_ID,
68
+ "--site-name",
69
+ "Field Notes",
70
+ ],
71
+ // `echo` ends the password with a line break that is not part of it.
72
+ pipe("correct horse\n"),
73
+ );
74
+
75
+ expect(result).toEqual({ outcome: "created", siteId: SITE_ID });
76
+ expect(console.log).toHaveBeenCalledWith(`Set up site ${SITE_ID}.`);
77
+
78
+ const stored = readDatabase((sqlite) => ({
79
+ email: sqlite.prepare('SELECT email FROM "user"').pluck().get(),
80
+ hash: sqlite.prepare('SELECT password FROM "account"').pluck().get(),
81
+ host: sqlite.prepare('SELECT host FROM "site_domain"').pluck().get(),
82
+ status: sqlite
83
+ .prepare(
84
+ `SELECT value FROM "site_setting" WHERE "key" = 'ONBOARDING_STATUS'`,
85
+ )
86
+ .pluck()
87
+ .get(),
88
+ }));
89
+ expect(stored).toMatchObject({
90
+ email: "owner@example.com",
91
+ host: "notes.example.com",
92
+ status: "completed",
93
+ });
94
+ expect(
95
+ await verifyPassword({
96
+ hash: String(stored.hash),
97
+ password: "correct horse",
98
+ }),
99
+ ).toBe(true);
100
+ });
101
+
102
+ it("changes nothing when run again", async () => {
103
+ const args = ["--email", "owner@example.com", "--password-stdin"];
104
+ const first = await run(args, pipe("correct horse"));
105
+ const second = await run(args, pipe("a different password"));
106
+
107
+ expect(second).toEqual({
108
+ outcome: "already-set-up",
109
+ siteId: first.siteId,
110
+ });
111
+ expect(console.log).toHaveBeenLastCalledWith(
112
+ `Site ${first.siteId} was already set up. Nothing changed.`,
113
+ );
114
+ });
115
+
116
+ it("refuses a password passed any way but standard input", async () => {
117
+ await expect(
118
+ run(["--email", "owner@example.com"], pipe("correct horse")),
119
+ ).rejects.toThrow("Pass --password-stdin");
120
+ });
121
+
122
+ it("refuses to wait on a terminal for the password", async () => {
123
+ const stdin = Object.assign(Readable.from([]), { isTTY: true });
124
+
125
+ await expect(
126
+ run(["--email", "owner@example.com", "--password-stdin"], { stdin }),
127
+ ).rejects.toThrow("reads the password from a pipe");
128
+ });
129
+
130
+ it("rejects answers the setup screens would reject", async () => {
131
+ const args = ["--email", "owner@example.com", "--password-stdin"];
132
+
133
+ await expect(run(args, pipe("short"))).rejects.toThrow(
134
+ "Password must be at least 8 characters",
135
+ );
136
+ await expect(
137
+ run(
138
+ [...args, "--site-id", "pst_01kn8jq3t4famtyg9hjd074ckr"],
139
+ pipe("correct horse"),
140
+ ),
141
+ ).rejects.toThrow("Invalid ID");
142
+ await expect(
143
+ run([...args, "--time-zone", "Mars/Olympus"], pipe("correct horse")),
144
+ ).rejects.toThrow("Choose a valid time zone.");
145
+
146
+ expect(
147
+ readDatabase((sqlite) =>
148
+ sqlite.prepare('SELECT COUNT(*) FROM "user"').pluck().get(),
149
+ ),
150
+ ).toBe(0);
151
+ });
152
+
153
+ it("refuses to run without a Node database", async () => {
154
+ delete process.env.DATABASE_URL;
155
+
156
+ await expect(
157
+ run(
158
+ ["--email", "owner@example.com", "--password-stdin"],
159
+ pipe("correct horse"),
160
+ ),
161
+ ).rejects.toThrow("jant setup runs against a Node database");
162
+ });
163
+ });
@@ -1,5 +1,8 @@
1
1
  import { describe, expect, it, vi } from "vitest";
2
- import { resolveCliSite } from "../../../bin/lib/site-selection.js";
2
+ import {
3
+ parseCliSiteSelector,
4
+ resolveCliSite,
5
+ } from "../../../bin/lib/site-selection.js";
3
6
 
4
7
  function createSiteRow(id, key) {
5
8
  return {
@@ -11,6 +14,8 @@ function createSiteRow(id, key) {
11
14
  };
12
15
  }
13
16
 
17
+ const HOST_BASED_ENV = { SITE_RESOLUTION_MODE: "host-based" };
18
+
14
19
  describe("CLI site selection", () => {
15
20
  it("resolves a host-scoped site in host-based mode", async () => {
16
21
  const query = vi.fn(async (sql) => {
@@ -26,9 +31,7 @@ describe("CLI site selection", () => {
26
31
  query,
27
32
  },
28
33
  {
29
- env: {
30
- SITE_RESOLUTION_MODE: "host-based",
31
- },
34
+ env: HOST_BASED_ENV,
32
35
  host: "demo.jant.blog",
33
36
  },
34
37
  );
@@ -54,9 +57,7 @@ describe("CLI site selection", () => {
54
57
  query,
55
58
  },
56
59
  {
57
- env: {
58
- SITE_RESOLUTION_MODE: "host-based",
59
- },
60
+ env: HOST_BASED_ENV,
60
61
  url: "https://demo.jant.blog/base/",
61
62
  },
62
63
  );
@@ -64,49 +65,91 @@ describe("CLI site selection", () => {
64
65
  expect(resolved.site.key).toBe("demo");
65
66
  });
66
67
 
67
- it("falls back to the only site in host-based mode", async () => {
68
- const query = vi.fn(async (sql) => {
69
- if (sql.includes('FROM "site_domain"')) {
70
- return [];
71
- }
72
-
73
- return [createSiteRow("sit_only", "only")];
74
- });
68
+ it("resolves --site by key or by id", async () => {
69
+ const sites = [
70
+ createSiteRow("sit_one", "one"),
71
+ createSiteRow("sit_two", "two"),
72
+ ];
73
+ const query = vi.fn(async (sql) =>
74
+ sites.filter(
75
+ (site) =>
76
+ sql.includes(`"id" = '${site.id}'`) ||
77
+ sql.includes(`"key" = '${site.key}'`),
78
+ ),
79
+ );
75
80
 
76
- const resolved = await resolveCliSite(
77
- {
78
- query,
79
- },
80
- {
81
- env: {
82
- SITE_RESOLUTION_MODE: "host-based",
83
- },
84
- },
81
+ const byKey = await resolveCliSite(
82
+ { query },
83
+ { env: HOST_BASED_ENV, site: "two" },
84
+ );
85
+ const byId = await resolveCliSite(
86
+ { query },
87
+ { env: HOST_BASED_ENV, site: "sit_one" },
85
88
  );
86
89
 
87
- expect(resolved.site.id).toBe("sit_only");
90
+ expect(byKey.site.id).toBe("sit_two");
91
+ expect(byId.site.id).toBe("sit_one");
88
92
  });
89
93
 
90
- it("requires explicit selection for host-based multi-site instances", async () => {
94
+ // Unreachable through the managed API, whose keys cannot contain `_`, but a
95
+ // hand-edited row must not make `--site` pick a tenant by accident.
96
+ it("prefers the id match when a key equals another site's id", async () => {
91
97
  const query = vi.fn(async () => [
92
- createSiteRow("sit_one", "one"),
93
- createSiteRow("sit_two", "two"),
98
+ createSiteRow("sit_other", "sit_target"),
99
+ createSiteRow("sit_target", "target"),
94
100
  ]);
95
101
 
102
+ const resolved = await resolveCliSite(
103
+ { query },
104
+ { env: HOST_BASED_ENV, site: "sit_target" },
105
+ );
106
+
107
+ expect(resolved.site.key).toBe("target");
108
+ });
109
+
110
+ it("names the --site value that matched nothing", async () => {
111
+ const query = vi.fn(async () => []);
112
+
113
+ await expect(
114
+ resolveCliSite({ query }, { env: HOST_BASED_ENV, site: "missing" }),
115
+ ).rejects.toThrow("No site found for --site missing.");
116
+ });
117
+
118
+ // A hosted database holds every tenant. Falling back to "the only site"
119
+ // would work until the day a second tenant signs up, so host-based mode
120
+ // never guesses — it fails before running a single query.
121
+ it("requires a site flag in host-based mode, however many sites exist", async () => {
122
+ const query = vi.fn(async () => [createSiteRow("sit_only", "only")]);
123
+
124
+ await expect(
125
+ resolveCliSite({ query }, { env: HOST_BASED_ENV }),
126
+ ).rejects.toThrow(
127
+ "host-based mode needs a target site. Pass --site <key|id>, --host <host>, or --url <url>.",
128
+ );
129
+ expect(query).not.toHaveBeenCalled();
130
+ });
131
+
132
+ it("rejects more than one site flag instead of picking one", async () => {
133
+ const query = vi.fn(async () => []);
134
+
96
135
  await expect(
97
136
  resolveCliSite(
98
- {
99
- query,
100
- },
101
- {
102
- env: {
103
- SITE_RESOLUTION_MODE: "host-based",
104
- },
105
- },
137
+ { query },
138
+ { env: HOST_BASED_ENV, site: "one", host: "two.jant.blog" },
106
139
  ),
107
- ).rejects.toThrow(
108
- "host-based mode requires --site, --host, or --url when the database contains multiple sites.",
140
+ ).rejects.toThrow("Choose only one of --site, --host, or --url.");
141
+ expect(query).not.toHaveBeenCalled();
142
+ });
143
+
144
+ it("uses the only site in single-site mode", async () => {
145
+ const query = vi.fn(async () => [createSiteRow("sit_only", "only")]);
146
+
147
+ const resolved = await resolveCliSite(
148
+ { query },
149
+ { env: { SITE_RESOLUTION_MODE: "single-site" } },
109
150
  );
151
+
152
+ expect(resolved.site.id).toBe("sit_only");
110
153
  });
111
154
 
112
155
  it("returns an actionable message for single-site multi-site instances", async () => {
@@ -131,3 +174,50 @@ describe("CLI site selection", () => {
131
174
  );
132
175
  });
133
176
  });
177
+
178
+ describe("parseCliSiteSelector", () => {
179
+ it("returns null when no site flag was passed", () => {
180
+ expect(parseCliSiteSelector({ pathPrefix: "/blog" })).toBeNull();
181
+ });
182
+
183
+ it("reads --site as a key or id", () => {
184
+ expect(parseCliSiteSelector({ site: " demo " })).toEqual({
185
+ kind: "site",
186
+ idOrKey: "demo",
187
+ });
188
+ });
189
+
190
+ it("splits --url into its host and path prefix", () => {
191
+ expect(parseCliSiteSelector({ url: "https://demo.jant.blog/" })).toEqual({
192
+ kind: "host",
193
+ host: "demo.jant.blog",
194
+ pathPrefix: null,
195
+ });
196
+ expect(
197
+ parseCliSiteSelector({ url: "https://demo.jant.blog/blog/" }),
198
+ ).toEqual({
199
+ kind: "host",
200
+ host: "demo.jant.blog",
201
+ pathPrefix: "/blog",
202
+ });
203
+ });
204
+
205
+ it("pairs --host with --path-prefix", () => {
206
+ expect(
207
+ parseCliSiteSelector({ host: "demo.jant.blog", pathPrefix: "blog/" }),
208
+ ).toEqual({
209
+ kind: "host",
210
+ host: "demo.jant.blog",
211
+ pathPrefix: "/blog",
212
+ });
213
+ });
214
+
215
+ it("rejects --url together with --host", () => {
216
+ expect(() =>
217
+ parseCliSiteSelector({
218
+ host: "demo.jant.blog",
219
+ url: "https://demo.jant.blog/",
220
+ }),
221
+ ).toThrow("Choose only one of --site, --host, or --url.");
222
+ });
223
+ });
@@ -780,6 +780,73 @@ describe("jant site snapshot export/import", () => {
780
780
  expect(await targetStorage.get(SNAPSHOT_MEDIA_KEY)).not.toBeNull();
781
781
  });
782
782
 
783
+ // The snapshot was exported where the bytes lived in R2; this import puts
784
+ // them in local storage. Rows left saying `r2` would look for R2_PUBLIC_URL,
785
+ // miss the avatar's existing row on replacement, and never purge their trash.
786
+ it("records imported media under the storage the import wrote it to", async () => {
787
+ const root = await mkdtemp(join(tmpdir(), "jant-site-snapshot-provider-"));
788
+ tempDirs.push(root);
789
+
790
+ const targetDbPath = join(root, "target.sqlite");
791
+ const targetStoragePath = join(root, "target-media");
792
+ const snapshotPath = join(root, "snapshot");
793
+
794
+ await migrate({ DATABASE_URL: `file:${targetDbPath}` } as Bindings);
795
+ const targetSqlite = new Database(targetDbPath);
796
+ try {
797
+ targetSqlite.exec(`
798
+ INSERT INTO "site" ("id", "key", "status", "created_at", "updated_at")
799
+ VALUES ('${SNAPSHOT_SITE_ID}', '${SNAPSHOT_SITE_KEY}', 'active', 1774009000, 1774009000);
800
+ `);
801
+ } finally {
802
+ targetSqlite.close();
803
+ }
804
+
805
+ await mkdir(
806
+ join(snapshotPath, "objects", "media", SNAPSHOT_SITE_ID, "files"),
807
+ {
808
+ recursive: true,
809
+ },
810
+ );
811
+ await writeFile(join(snapshotPath, "objects", SNAPSHOT_MEDIA_KEY), "png");
812
+ await writeFile(
813
+ join(snapshotPath, "meta.json"),
814
+ JSON.stringify({
815
+ format: "jant-site-snapshot",
816
+ version: 2,
817
+ dialect: "sqlite",
818
+ site: { id: SNAPSHOT_SITE_ID, key: SNAPSHOT_SITE_KEY },
819
+ }),
820
+ );
821
+ await writeFile(
822
+ join(snapshotPath, "db.sql"),
823
+ `INSERT INTO "media" ("id", "site_id", "post_id", "filename", "original_name", "mime_type", "size", "storage_key", "provider", "position", "media_kind", "created_at", "updated_at") VALUES('${SNAPSHOT_MEDIA_ID}', '${SNAPSHOT_SITE_ID}', NULL, '${SNAPSHOT_MEDIA_ID}.png', 'sample.png', 'image/png', 3, '${SNAPSHOT_MEDIA_KEY}', 'r2', 'a0', 'image', 1774009200, 1774009200);\n`,
824
+ );
825
+
826
+ useLocalSnapshotRuntime(`file:${targetDbPath}`, targetStoragePath);
827
+ vi.spyOn(console, "log").mockImplementation(() => {});
828
+ const { run: runImport } =
829
+ await import("../../../bin/commands/site/snapshot/import.js");
830
+ await runImport(["--path", snapshotPath, "--replace"]);
831
+
832
+ const verifySqlite = new Database(targetDbPath, { readonly: true });
833
+ try {
834
+ expect(
835
+ verifySqlite
836
+ .prepare(`SELECT "provider" FROM "media" WHERE "id" = ?`)
837
+ .pluck()
838
+ .get(SNAPSHOT_MEDIA_ID),
839
+ ).toBe("local");
840
+ } finally {
841
+ verifySqlite.close();
842
+ }
843
+ expect(
844
+ await createLocalDriver({ rootPath: targetStoragePath }).get(
845
+ SNAPSHOT_MEDIA_KEY,
846
+ ),
847
+ ).not.toBeNull();
848
+ });
849
+
783
850
  it("requires --replace for snapshot import", async () => {
784
851
  const root = await mkdtemp(join(tmpdir(), "jant-site-snapshot-replace-"));
785
852
  tempDirs.push(root);
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
2
2
  import {
3
3
  assertSnapshotDialectMatches,
4
4
  assertSnapshotMeta,
5
+ buildMediaProviderSql,
5
6
  buildSnapshotMeta,
6
7
  getSnapshotDialect,
7
8
  SNAPSHOT_VERSION,
@@ -183,3 +184,22 @@ describe("assertSnapshotDialectMatches", () => {
183
184
  expect(() => assertSnapshotDialectMatches({}, "pg")).not.toThrow();
184
185
  });
185
186
  });
187
+
188
+ describe("buildMediaProviderSql", () => {
189
+ it("records the site's media under the given storage driver", () => {
190
+ expect(buildMediaProviderSql("sit_o'brien", "s3")).toBe(
191
+ `UPDATE "media" SET "provider" = 's3' WHERE "site_id" = 'sit_o''brien';`,
192
+ );
193
+ });
194
+
195
+ // The value is written into SQL and read back as where the bytes live, so
196
+ // anything but a real driver is refused rather than quoted.
197
+ it("refuses anything that is not a storage driver", () => {
198
+ expect(() => buildMediaProviderSql("sit_test", "gcs")).toThrow(
199
+ 'Snapshot import cannot record media under storage driver "gcs".',
200
+ );
201
+ expect(() => buildMediaProviderSql("sit_test", "s3' OR '1")).toThrow(
202
+ "cannot record media under storage driver",
203
+ );
204
+ });
205
+ });