@jant/core 0.6.14 → 0.6.15

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 (311) hide show
  1. package/bin/commands/import-site.js +81 -13
  2. package/bin/commands/migrate.js +55 -0
  3. package/bin/commands/posts/rebuild-html.js +184 -0
  4. package/bin/commands/site/snapshot/import.js +14 -10
  5. package/bin/lib/d1-query.js +23 -5
  6. package/bin/lib/migration-runner.js +29 -7
  7. package/bin/lib/node-database.js +32 -0
  8. package/bin/lib/site-snapshot.js +347 -46
  9. package/bin/lib/sql-export.js +1 -1
  10. package/bin/lib/thread-collection-migration.js +326 -0
  11. package/dist/app-BxCOR3Uc.js +6 -0
  12. package/dist/{app-2i7-WQHg.js → app-C-oi_t8W.js} +7408 -6343
  13. package/dist/client/.vite/manifest.json +3 -3
  14. package/dist/client/_assets/client-B_eGKN5p.css +2 -0
  15. package/dist/client/_assets/{client-BvID5ucz.js → client-DxY5BFe8.js} +58 -37
  16. package/dist/client/_assets/client-auth-CakPESv9.js +5533 -0
  17. package/dist/{export-DWn149b7.js → export-CsFx6F_M.js} +1259 -125
  18. package/dist/{github-sync-Ckk0sJxK.js → github-sync-Cq1pzzOI.js} +1 -1
  19. package/dist/{github-sync-C0zyF6XS.js → github-sync-ppWXN3jb.js} +2 -2
  20. package/dist/index.js +3 -3
  21. package/dist/node.js +5 -5
  22. package/package.json +11 -10
  23. package/src/__tests__/bin/posts-rebuild-html.test.ts +137 -0
  24. package/src/__tests__/dev-navigation-defaults.test.ts +22 -0
  25. package/src/__tests__/export-hugo-build.test.ts +63 -4
  26. package/src/__tests__/export-import-roundtrip.test.ts +6 -4
  27. package/src/__tests__/export-service.test.ts +142 -9
  28. package/src/__tests__/helpers/app.ts +3 -0
  29. package/src/__tests__/helpers/export-fixtures.ts +2 -0
  30. package/src/__tests__/import-site-command.test.ts +65 -0
  31. package/src/__tests__/vite-config.test.ts +45 -0
  32. package/src/app.tsx +16 -3
  33. package/src/client/__tests__/collection-created-notice.test.ts +36 -0
  34. package/src/client/__tests__/collection-form-bridge.test.ts +10 -1
  35. package/src/client/__tests__/collection-page-actions.test.ts +71 -2
  36. package/src/client/__tests__/compose-bridge.test.ts +64 -0
  37. package/src/client/__tests__/feed-video-player.test.ts +16 -1
  38. package/src/client/__tests__/footnote-rail.test.ts +472 -0
  39. package/src/client/__tests__/hydrate-partial.test.ts +3 -0
  40. package/src/client/__tests__/toast.test.ts +18 -1
  41. package/src/client/collection-created-notice.ts +66 -0
  42. package/src/client/collection-form-bridge.ts +17 -0
  43. package/src/client/collection-navigation.ts +44 -0
  44. package/src/client/collection-page-actions.ts +57 -1
  45. package/src/client/components/__tests__/jant-collection-directory.test.ts +155 -6
  46. package/src/client/components/__tests__/jant-compose-dialog.test.ts +370 -5
  47. package/src/client/components/__tests__/jant-compose-editor.test.ts +97 -0
  48. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +104 -0
  49. package/src/client/components/__tests__/jant-config-editor.test.ts +405 -0
  50. package/src/client/components/__tests__/jant-media-lightbox.test.ts +121 -0
  51. package/src/client/components/__tests__/jant-nav-manager.test.ts +538 -2
  52. package/src/client/components/__tests__/jant-post-form.test.ts +2 -2
  53. package/src/client/components/__tests__/jant-post-menu.test.ts +47 -5
  54. package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -2
  55. package/src/client/components/__tests__/jant-settings-general.test.ts +3 -3
  56. package/src/client/components/collection-manager-types.ts +9 -3
  57. package/src/client/components/compose-types.ts +8 -0
  58. package/src/client/components/jant-collection-directory.ts +165 -30
  59. package/src/client/components/jant-command-palette.ts +2 -1
  60. package/src/client/components/jant-compose-dialog.ts +85 -21
  61. package/src/client/components/jant-compose-editor.ts +13 -7
  62. package/src/client/components/jant-compose-fullscreen.ts +24 -4
  63. package/src/client/components/jant-config-editor.ts +722 -0
  64. package/src/client/components/jant-media-lightbox.ts +100 -18
  65. package/src/client/components/jant-nav-manager.ts +1283 -54
  66. package/src/client/components/jant-post-form.ts +3 -3
  67. package/src/client/components/jant-post-menu.ts +86 -74
  68. package/src/client/components/nav-manager-types.ts +51 -2
  69. package/src/client/components/post-form-types.ts +1 -1
  70. package/src/client/compose-bridge.ts +4 -1
  71. package/src/client/feed-video-player.ts +62 -2
  72. package/src/client/footnote-rail.ts +478 -0
  73. package/src/client/hydrate-partial.ts +2 -0
  74. package/src/client/media-lightbox-events.ts +63 -0
  75. package/src/client/site-header-fragment.ts +38 -0
  76. package/src/client/tiptap/__tests__/blockquote-editing.test.ts +201 -0
  77. package/src/client/tiptap/__tests__/clear-formatting.test.ts +168 -0
  78. package/src/client/tiptap/__tests__/list-editing.test.ts +447 -0
  79. package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +727 -4
  80. package/src/client/tiptap/__tests__/table-controls.test.ts +263 -0
  81. package/src/client/tiptap/__tests__/table-size-picker.test.ts +179 -0
  82. package/src/client/tiptap/bubble-menu.ts +88 -10
  83. package/src/client/tiptap/continuous-lists.ts +102 -0
  84. package/src/client/tiptap/create-editor.ts +3 -0
  85. package/src/client/tiptap/extensions.ts +14 -5
  86. package/src/client/tiptap/markdown-clipboard.ts +608 -11
  87. package/src/client/tiptap/slash-commands.ts +8 -1
  88. package/src/client/tiptap/structural-keymap.ts +211 -0
  89. package/src/client/tiptap/tab-indent.ts +8 -22
  90. package/src/client/tiptap/table-control-labels.ts +30 -0
  91. package/src/client/tiptap/table-controls.ts +537 -0
  92. package/src/client/tiptap/table-size-picker.ts +254 -0
  93. package/src/client/toast.ts +8 -6
  94. package/src/client-auth.ts +1 -0
  95. package/src/client.ts +1 -0
  96. package/src/db/__tests__/d1-query.test.ts +43 -1
  97. package/src/db/__tests__/migration-runner.test.ts +63 -0
  98. package/src/db/__tests__/migrations.test.ts +469 -0
  99. package/src/db/__tests__/thread-collection-migration.test.ts +364 -0
  100. package/src/db/migrations/0027_old_blue_blade.sql +39 -0
  101. package/src/db/migrations/0028_superb_post.sql +2 -0
  102. package/src/db/migrations/0029_boring_magma.sql +1 -0
  103. package/src/db/migrations/0030_typical_vivisector.sql +50 -0
  104. package/src/db/migrations/meta/0027_snapshot.json +2511 -0
  105. package/src/db/migrations/meta/0028_snapshot.json +2511 -0
  106. package/src/db/migrations/meta/0029_snapshot.json +2519 -0
  107. package/src/db/migrations/meta/0030_snapshot.json +2541 -0
  108. package/src/db/migrations/meta/_journal.json +28 -0
  109. package/src/db/migrations/pg/0025_moaning_epoch.sql +39 -0
  110. package/src/db/migrations/pg/0026_talented_killer_shrike.sql +2 -0
  111. package/src/db/migrations/pg/0027_short_cannonball.sql +1 -0
  112. package/src/db/migrations/pg/0028_perfect_sharon_carter.sql +27 -0
  113. package/src/db/migrations/pg/meta/0025_snapshot.json +3204 -0
  114. package/src/db/migrations/pg/meta/0026_snapshot.json +3204 -0
  115. package/src/db/migrations/pg/meta/0027_snapshot.json +3211 -0
  116. package/src/db/migrations/pg/meta/0028_snapshot.json +3248 -0
  117. package/src/db/migrations/pg/meta/_journal.json +28 -0
  118. package/src/db/pg/schema.ts +28 -20
  119. package/src/db/rehearsal-fixtures/demo-current.json +39 -4
  120. package/src/db/rehearsal-fixtures/demo-current.sql +121 -6
  121. package/src/db/schema.ts +28 -20
  122. package/src/i18n/__tests__/detect.test.ts +8 -2
  123. package/src/i18n/detect.ts +14 -5
  124. package/src/i18n/locales/public/en.po +133 -30
  125. package/src/i18n/locales/public/en.ts +1 -1
  126. package/src/i18n/locales/public/zh-Hans.po +128 -25
  127. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  128. package/src/i18n/locales/public/zh-Hant.po +128 -25
  129. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  130. package/src/i18n/locales/settings/en.po +513 -10
  131. package/src/i18n/locales/settings/en.ts +1 -1
  132. package/src/i18n/locales/settings/zh-Hans.po +515 -12
  133. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  134. package/src/i18n/locales/settings/zh-Hant.po +515 -12
  135. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  136. package/src/lib/__tests__/api-settings.test.ts +299 -0
  137. package/src/lib/__tests__/constants.test.ts +5 -0
  138. package/src/lib/__tests__/feed-policy.test.ts +25 -0
  139. package/src/lib/__tests__/footnotes-upgrade.test.ts +181 -0
  140. package/src/lib/__tests__/markdown-roundtrip-embed.test.ts +61 -0
  141. package/src/lib/__tests__/markdown-to-tiptap.test.ts +32 -4
  142. package/src/lib/__tests__/markdown.test.ts +8 -6
  143. package/src/lib/__tests__/navigation.test.ts +59 -2
  144. package/src/lib/__tests__/post-body-html.test.ts +176 -0
  145. package/src/lib/__tests__/post-display.test.ts +64 -0
  146. package/src/lib/__tests__/resolve-config.test.ts +113 -1
  147. package/src/lib/__tests__/summary.test.ts +82 -0
  148. package/src/lib/__tests__/timeline.test.ts +109 -70
  149. package/src/lib/__tests__/timezones.test.ts +31 -3
  150. package/src/lib/__tests__/tiptap-render.test.ts +268 -9
  151. package/src/lib/__tests__/tiptap-to-markdown.test.ts +6 -0
  152. package/src/lib/__tests__/view.test.ts +53 -0
  153. package/src/lib/__tests__/worker-response-cache.test.ts +18 -1
  154. package/src/lib/api-settings.ts +239 -12
  155. package/src/lib/collection-sort.ts +7 -5
  156. package/src/lib/constants.ts +2 -0
  157. package/src/lib/feed-path.ts +25 -0
  158. package/src/lib/feed-policy.ts +64 -0
  159. package/src/lib/footnotes.ts +264 -0
  160. package/src/lib/github-sync-site-config.ts +2 -0
  161. package/src/lib/hugo-markdown.ts +10 -3
  162. package/src/lib/jant-branding.ts +1 -0
  163. package/src/lib/markdown-manager.ts +188 -5
  164. package/src/lib/markdown.ts +10 -3
  165. package/src/lib/navigation.ts +16 -3
  166. package/src/lib/post-body-html.ts +131 -0
  167. package/src/lib/post-display.ts +7 -1
  168. package/src/lib/render.tsx +9 -2
  169. package/src/lib/resolve-config.ts +100 -16
  170. package/src/lib/schemas.ts +133 -1
  171. package/src/lib/settings-paths.ts +5 -0
  172. package/src/lib/site-skill.ts +78 -0
  173. package/src/lib/summary.ts +16 -5
  174. package/src/lib/timeline.ts +98 -122
  175. package/src/lib/timezones.ts +54 -14
  176. package/src/lib/tiptap-render.ts +355 -81
  177. package/src/lib/view.ts +30 -20
  178. package/src/lib/worker-response-cache.ts +11 -18
  179. package/src/middleware/__tests__/public-content-access.test.ts +91 -0
  180. package/src/middleware/config.ts +2 -2
  181. package/src/middleware/public-content-access.ts +87 -0
  182. package/src/node/__tests__/cli-export.test.ts +14 -0
  183. package/src/node/__tests__/cli-migrate.test.ts +6 -0
  184. package/src/node/__tests__/cli-site-snapshot.test.ts +205 -2
  185. package/src/node/__tests__/cli-snapshot-meta.test.ts +100 -0
  186. package/src/node/index.ts +2 -1
  187. package/src/preset.css +16 -2
  188. package/src/routes/__tests__/compose.test.ts +51 -0
  189. package/src/routes/__tests__/site-skill.test.ts +112 -0
  190. package/src/routes/api/__tests__/collections.test.ts +91 -19
  191. package/src/routes/api/__tests__/mcp.test.ts +65 -1
  192. package/src/routes/api/__tests__/nav-items.test.ts +91 -0
  193. package/src/routes/api/__tests__/public-access.test.ts +81 -0
  194. package/src/routes/api/__tests__/settings.test.ts +217 -0
  195. package/src/routes/api/collections.ts +22 -26
  196. package/src/routes/api/export.ts +2 -0
  197. package/src/routes/api/internal/__tests__/post-body-html.test.ts +163 -0
  198. package/src/routes/api/internal/__tests__/sites.test.ts +35 -2
  199. package/src/routes/api/internal/post-body-html.ts +43 -0
  200. package/src/routes/api/internal/sites.ts +24 -0
  201. package/src/routes/api/nav-items.ts +24 -1
  202. package/src/routes/api/posts.ts +2 -2
  203. package/src/routes/api/public/__tests__/archive.test.ts +35 -0
  204. package/src/routes/api/public/__tests__/posts.test.ts +208 -0
  205. package/src/routes/api/public/archive.ts +4 -0
  206. package/src/routes/api/public/posts.ts +41 -24
  207. package/src/routes/api/search.ts +3 -0
  208. package/src/routes/api/settings.ts +33 -0
  209. package/src/routes/auth/__tests__/setup.test.ts +7 -4
  210. package/src/routes/compose.tsx +2 -1
  211. package/src/routes/dash/settings.tsx +45 -2
  212. package/src/routes/feed/__tests__/feed.test.ts +210 -6
  213. package/src/routes/feed/feed.ts +23 -14
  214. package/src/routes/pages/__tests__/archive-params.test.ts +92 -3
  215. package/src/routes/pages/__tests__/collection-routing.test.ts +63 -5
  216. package/src/routes/pages/__tests__/collections.test.ts +9 -9
  217. package/src/routes/pages/__tests__/preview.test.ts +174 -0
  218. package/src/routes/pages/__tests__/search.test.ts +69 -0
  219. package/src/routes/pages/archive.tsx +27 -7
  220. package/src/routes/pages/collection.tsx +32 -19
  221. package/src/routes/pages/collections.tsx +6 -1
  222. package/src/routes/pages/page.tsx +83 -11
  223. package/src/routes/site-skill.ts +18 -0
  224. package/src/services/__tests__/auth.test.ts +7 -5
  225. package/src/services/__tests__/collection.test.ts +159 -44
  226. package/src/services/__tests__/navigation.test.ts +229 -21
  227. package/src/services/__tests__/post-timeline.test.ts +109 -30
  228. package/src/services/__tests__/post.test.ts +567 -5
  229. package/src/services/__tests__/search.test.ts +48 -1
  230. package/src/services/__tests__/settings.test.ts +8 -0
  231. package/src/services/__tests__/site-admin.test.ts +121 -1
  232. package/src/services/auth.ts +2 -2
  233. package/src/services/bootstrap.ts +3 -2
  234. package/src/services/collection.ts +226 -167
  235. package/src/services/export-theme/assets/client-site.css +1 -1
  236. package/src/services/export-theme/assets/client-site.js +46 -25
  237. package/src/services/export-theme/layouts/_default/list.html +18 -4
  238. package/src/services/export-theme/layouts/_default/rss.xml +8 -12
  239. package/src/services/export-theme/layouts/featured/list.html +5 -4
  240. package/src/services/export-theme/layouts/partials/featured-thread.html +56 -0
  241. package/src/services/export-theme/layouts/partials/footer.html +1 -1
  242. package/src/services/export-theme/layouts/partials/media-gallery.html +1 -0
  243. package/src/services/export-theme/styles/main.css +30 -2
  244. package/src/services/export.ts +91 -80
  245. package/src/services/mcp.ts +37 -27
  246. package/src/services/media.ts +1 -1
  247. package/src/services/navigation.ts +185 -46
  248. package/src/services/post.ts +874 -271
  249. package/src/services/search.ts +8 -2
  250. package/src/services/settings.ts +2 -10
  251. package/src/services/site-admin.ts +63 -43
  252. package/src/style-cjk-jp.css +1 -1
  253. package/src/style-cjk-kr.css +1 -1
  254. package/src/style-cjk-tc.css +1 -1
  255. package/src/style-cjk.css +1 -1
  256. package/src/styles/components.css +540 -0
  257. package/src/styles/site-media.css +111 -19
  258. package/src/styles/tokens.css +14 -9
  259. package/src/styles/ui.css +829 -29
  260. package/src/types/bindings.ts +3 -0
  261. package/src/types/config.ts +243 -12
  262. package/src/types/constants.ts +22 -1
  263. package/src/types/entities.ts +9 -8
  264. package/src/types/operations.ts +18 -10
  265. package/src/types/props.ts +8 -2
  266. package/src/types/raw-assets.d.ts +5 -0
  267. package/src/types/views.ts +16 -4
  268. package/src/ui/__tests__/font-themes.test.ts +42 -30
  269. package/src/ui/compose/ComposeDialog.tsx +92 -2
  270. package/src/ui/dash/appearance/NavigationContent.tsx +332 -4
  271. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +23 -0
  272. package/src/ui/dash/settings/ConfigEditorContent.tsx +578 -0
  273. package/src/ui/dash/settings/GeneralContent.tsx +15 -5
  274. package/src/ui/dash/settings/SettingsRootContent.tsx +21 -1
  275. package/src/ui/dash/settings/__tests__/ConfigEditorContent.test.tsx +98 -0
  276. package/src/ui/dash/settings/__tests__/SettingsRootContent.test.tsx +15 -0
  277. package/src/ui/feed/CuratedThreadPreview.tsx +16 -4
  278. package/src/ui/feed/LinkCard.tsx +2 -12
  279. package/src/ui/feed/NoteCard.tsx +10 -20
  280. package/src/ui/feed/QuoteCard.tsx +2 -12
  281. package/src/ui/feed/ThreadPreview.tsx +26 -34
  282. package/src/ui/feed/TimelineFeed.tsx +2 -2
  283. package/src/ui/feed/__tests__/thread-preview.test.ts +197 -56
  284. package/src/ui/feed/__tests__/timeline-cards.test.ts +178 -3
  285. package/src/ui/feed/thread-preview-state.ts +5 -7
  286. package/src/ui/font-themes.ts +74 -26
  287. package/src/ui/layouts/BaseLayout.tsx +22 -8
  288. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +64 -1
  289. package/src/ui/pages/CollectionPage.tsx +101 -22
  290. package/src/ui/pages/CollectionsPage.tsx +3 -1
  291. package/src/ui/pages/PostPage.tsx +39 -12
  292. package/src/ui/pages/SearchPage.tsx +6 -14
  293. package/src/ui/pages/__tests__/ArchivePage.test.tsx +7 -0
  294. package/src/ui/pages/__tests__/PostPage.test.tsx +88 -0
  295. package/src/ui/shared/CollectionDirectory.tsx +13 -9
  296. package/src/ui/shared/CollectionsManager.tsx +17 -7
  297. package/src/ui/shared/DraftPreviewBar.tsx +33 -0
  298. package/src/ui/shared/HomePageBranding.tsx +2 -2
  299. package/src/ui/shared/MediaGallery.tsx +1 -0
  300. package/src/ui/shared/PostFooter.tsx +3 -1
  301. package/src/ui/shared/__tests__/home-page-branding.test.tsx +1 -1
  302. package/src/ui/shared/__tests__/media-gallery.test.ts +1 -0
  303. package/src/ui/shared/__tests__/navigation-labels.test.ts +2 -2
  304. package/src/ui/shared/__tests__/post-footer.test.ts +15 -0
  305. package/src/ui/shared/collection-management-labels.ts +33 -2
  306. package/src/ui/shared/navigation-labels.ts +2 -4
  307. package/src/ui/shared/post-article-attributes.ts +46 -0
  308. package/dist/app-DZDlmh7C.js +0 -6
  309. package/dist/client/_assets/client-Cevfpm8H.css +0 -2
  310. package/dist/client/_assets/client-auth-v3yvhbgZ.js +0 -4736
  311. package/src/client/tiptap/wrapping-input-rules.ts +0 -102
@@ -1,5 +1,7 @@
1
1
  import type { Bindings } from "../types.js";
2
2
  import { getConfiguredSingleSitePathPrefix } from "./env.js";
3
+ import { POST_BODY_HTML_VERSION } from "./post-body-html.js";
4
+ import { isRssFeedPath } from "./feed-path.js";
3
5
  import { stripSitePathPrefix } from "./url.js";
4
6
 
5
7
  const TRACKING_QUERY_PARAMS = [
@@ -21,6 +23,7 @@ const TRACKING_QUERY_PARAMS = [
21
23
 
22
24
  const AUTH_COOKIE_PATTERN =
23
25
  /(?:^|;\s*)(?:__Host-)?(?:__Secure-)?better-auth[.-][^=]+=/;
26
+ const BODY_HTML_CACHE_VERSION_PARAM = "__jant_body_html";
24
27
 
25
28
  type ExecutionContextLike = {
26
29
  waitUntil(promise: Promise<unknown>): void;
@@ -50,13 +53,7 @@ export interface WorkerResponseCacheOptions {
50
53
  * @returns `true` when the request should be eligible for Worker response caching
51
54
  */
52
55
  export function isWorkerResponseCachePath(path: string): boolean {
53
- if (path === "/feed" || path.startsWith("/feed/")) {
54
- return true;
55
- }
56
-
57
- if (path === "/archive/feed" || path.startsWith("/archive/feed/")) {
58
- return true;
59
- }
56
+ if (isRssFeedPath(path)) return true;
60
57
 
61
58
  if (path === "/sitemap.xml" || path === "/robots.txt") {
62
59
  return true;
@@ -66,16 +63,6 @@ export function isWorkerResponseCachePath(path: string): boolean {
66
63
  return true;
67
64
  }
68
65
 
69
- if (
70
- path.endsWith("/feed") &&
71
- !path.startsWith("/api/") &&
72
- !path.startsWith("/settings") &&
73
- !path.startsWith("/compose") &&
74
- !path.startsWith("/_")
75
- ) {
76
- return true;
77
- }
78
-
79
66
  return false;
80
67
  }
81
68
 
@@ -83,7 +70,9 @@ export function isWorkerResponseCachePath(path: string): boolean {
83
70
  * Normalize a public request URL for Worker cache lookup.
84
71
  *
85
72
  * Tracking parameters are removed so equivalent feed requests converge on one
86
- * cache entry while functional filters like `format` remain intact.
73
+ * cache entry while functional filters like `format` remain intact. A private
74
+ * body-HTML contract version invalidates cached feed markup across renderer
75
+ * deployments; it is only used for Cache API lookup and never forwarded.
87
76
  *
88
77
  * @param requestUrl - Full public request URL
89
78
  * @returns Normalized cache key URL
@@ -95,6 +84,10 @@ export function normalizeWorkerCacheKeyUrl(requestUrl: string): string {
95
84
  url.searchParams.delete(param);
96
85
  }
97
86
 
87
+ url.searchParams.set(
88
+ BODY_HTML_CACHE_VERSION_PARAM,
89
+ String(POST_BODY_HTML_VERSION),
90
+ );
98
91
  url.searchParams.sort();
99
92
  return url.toString();
100
93
  }
@@ -0,0 +1,91 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { createTestApp } from "../../__tests__/helpers/app.js";
3
+ import { isRssFeedPath } from "../../lib/feed-path.js";
4
+ import {
5
+ requirePublicApiAccess,
6
+ requirePublicApiEnabled,
7
+ requireRssFeedsEnabled,
8
+ } from "../public-content-access.js";
9
+
10
+ describe("public content access policies", () => {
11
+ it.each([
12
+ "/feed",
13
+ "/feed/latest",
14
+ "/feed/atom.xml",
15
+ "/latest/feed",
16
+ "/featured/feed/atom.xml",
17
+ "/archive/feed",
18
+ "/reading/feed",
19
+ "/settings-notes/feed",
20
+ "/collections/reading+movies/feed",
21
+ ])("recognizes %s as a feed path", (path) => {
22
+ expect(isRssFeedPath(path)).toBe(true);
23
+ });
24
+
25
+ it.each([
26
+ "/",
27
+ "/archive",
28
+ "/api/example/feed",
29
+ "/settings/feed",
30
+ "/compose/feed",
31
+ "/_/theme/feed",
32
+ ])("does not treat %s as a feed path", (path) => {
33
+ expect(isRssFeedPath(path)).toBe(false);
34
+ });
35
+
36
+ it("requires authentication for JSON reads when public access is off", async () => {
37
+ const { app, services } = createTestApp({ authenticated: false });
38
+ await services.settings.set("PUBLIC_API_ENABLED", "false");
39
+ app.get("/api/data", requirePublicApiAccess(), (c) => c.json({ ok: true }));
40
+
41
+ const response = await app.request("/api/data");
42
+
43
+ expect(response.status).toBe(401);
44
+ });
45
+
46
+ it("returns 404 for the dedicated public API even with a session", async () => {
47
+ const { app, services } = createTestApp({ authenticated: true });
48
+ await services.settings.set("PUBLIC_API_ENABLED", "false");
49
+ app.get("/api/public/data", requirePublicApiEnabled(), (c) =>
50
+ c.json({ ok: true }),
51
+ );
52
+
53
+ const response = await app.request("/api/public/data");
54
+
55
+ expect(response.status).toBe(404);
56
+ });
57
+
58
+ it("preserves session access when public JSON reads are off", async () => {
59
+ const { app, services } = createTestApp({ authenticated: true });
60
+ await services.settings.set("PUBLIC_API_ENABLED", "false");
61
+ app.get("/api/data", requirePublicApiAccess(), (c) => c.json({ ok: true }));
62
+
63
+ const response = await app.request("/api/data");
64
+
65
+ expect(response.status).toBe(200);
66
+ });
67
+
68
+ it("preserves Bearer-token access when public JSON reads are off", async () => {
69
+ const { app, services } = createTestApp({ authenticated: false });
70
+ await services.settings.set("PUBLIC_API_ENABLED", "false");
71
+ const { plaintext } = await services.apiTokens.create("Test client");
72
+ app.get("/api/data", requirePublicApiAccess(), (c) => c.json({ ok: true }));
73
+
74
+ const response = await app.request("/api/data", {
75
+ headers: { Authorization: `Bearer ${plaintext}` },
76
+ });
77
+
78
+ expect(response.status).toBe(200);
79
+ });
80
+
81
+ it("returns 404 for feed paths when feed publishing is off", async () => {
82
+ const { app, services } = createTestApp();
83
+ await services.settings.set("RSS_FEEDS_ENABLED", "false");
84
+ app.use("*", requireRssFeedsEnabled());
85
+ app.get("*", (c) => c.text("page"));
86
+
87
+ expect((await app.request("/settings-notes/feed")).status).toBe(404);
88
+ expect((await app.request("/settings/feed")).status).toBe(200);
89
+ expect((await app.request("/settings-notes")).status).toBe(200);
90
+ });
91
+ });
@@ -18,7 +18,7 @@ import {
18
18
  import { buildThemeStyle, resolveBuiltinTheme } from "../lib/theme.js";
19
19
  import {
20
20
  BUILTIN_FONT_THEMES,
21
- getCjkSerifCssVariables,
21
+ getCjkFontCssVariables,
22
22
  getFontThemeCssVariables,
23
23
  } from "../ui/font-themes.js";
24
24
 
@@ -52,7 +52,7 @@ export function withConfig(): MiddlewareHandler<Env> {
52
52
  (f) => f.id === appConfig.fontThemeId,
53
53
  );
54
54
  const fontOverrides = {
55
- ...getCjkSerifCssVariables(appConfig.cjkSerifFont),
55
+ ...getCjkFontCssVariables(appConfig.siteLanguage, appConfig.cjkSerifFont),
56
56
  ...(fontTheme ? getFontThemeCssVariables(fontTheme) : {}),
57
57
  };
58
58
 
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Public machine-readable content access policies.
3
+ *
4
+ * These guards run after configuration has been resolved. They keep public
5
+ * HTML rendering independent from the optional JSON API and Atom surfaces.
6
+ */
7
+
8
+ import type { MiddlewareHandler } from "hono";
9
+ import type { Bindings } from "../types.js";
10
+ import type { AppVariables } from "../types/app-context.js";
11
+ import { isRssFeedPath } from "../lib/feed-path.js";
12
+ import { requireAuthApi } from "./auth.js";
13
+
14
+ type Env = { Bindings: Bindings; Variables: AppVariables };
15
+
16
+ /**
17
+ * Return 404 when the dedicated public JSON API is disabled.
18
+ *
19
+ * Unlike shared read endpoints used by the dashboard, `/api/public/*` has an
20
+ * authenticated alternative under `/api/posts`, so session and token auth do
21
+ * not bypass this switch.
22
+ *
23
+ * @returns Hono middleware that makes the public API unavailable when disabled
24
+ * @example
25
+ * ```ts
26
+ * app.use("/api/public/*", requirePublicApiEnabled());
27
+ * ```
28
+ */
29
+ export function requirePublicApiEnabled(): MiddlewareHandler<Env> {
30
+ return async (c, next) => {
31
+ if (
32
+ (c.req.method !== "GET" && c.req.method !== "HEAD") ||
33
+ c.var.appConfig.publicApiEnabled
34
+ ) {
35
+ return next();
36
+ }
37
+
38
+ return c.notFound();
39
+ };
40
+ }
41
+
42
+ /**
43
+ * Require normal authentication for shared JSON reads when public access is off.
44
+ *
45
+ * @returns Hono middleware that preserves session and Bearer-token access
46
+ * @example
47
+ * ```ts
48
+ * app.get("/api/search", requirePublicApiAccess(), searchHandler);
49
+ * ```
50
+ */
51
+ export function requirePublicApiAccess(): MiddlewareHandler<Env> {
52
+ const requireAuthentication = requireAuthApi();
53
+
54
+ return async (c, next) => {
55
+ if (
56
+ (c.req.method !== "GET" && c.req.method !== "HEAD") ||
57
+ c.var.appConfig.publicApiEnabled
58
+ ) {
59
+ return next();
60
+ }
61
+
62
+ return requireAuthentication(c, next);
63
+ };
64
+ }
65
+
66
+ /**
67
+ * Return 404 for Atom endpoints when feed publishing is disabled.
68
+ *
69
+ * @returns Hono middleware that leaves non-feed requests unchanged
70
+ * @example
71
+ * ```ts
72
+ * app.use("*", requireRssFeedsEnabled());
73
+ * ```
74
+ */
75
+ export function requireRssFeedsEnabled(): MiddlewareHandler<Env> {
76
+ return async (c, next) => {
77
+ if (
78
+ (c.req.method !== "GET" && c.req.method !== "HEAD") ||
79
+ c.var.appConfig.rssFeedsEnabled ||
80
+ !isRssFeedPath(new URL(c.req.url).pathname)
81
+ ) {
82
+ return next();
83
+ }
84
+
85
+ return c.notFound();
86
+ };
87
+ }
@@ -11,6 +11,8 @@ describe("jant export", () => {
11
11
  const tempDirs: string[] = [];
12
12
  const originalEnv = process.env.DATABASE_URL;
13
13
  const siteId = "sit_01jpyz1h3v4m7s2k8r5c9t0qbd";
14
+ const collectionId = "col_01jpyz1q6s4m8v2k5t9c3b7qdh";
15
+ const postId = "pst_01jpyz20kt5n9r3k8t6c4d2qhf";
14
16
 
15
17
  afterEach(async () => {
16
18
  if (originalEnv === undefined) {
@@ -44,6 +46,15 @@ describe("jant export", () => {
44
46
 
45
47
  INSERT INTO "site_setting" ("site_id", "key", "value", "updated_at")
46
48
  VALUES ('${siteId}', 'SITE_NAME', 'Test Site', 1773753605);
49
+
50
+ INSERT INTO "collection" ("id", "site_id", "title", "sort_order", "created_at", "updated_at")
51
+ VALUES ('${collectionId}', '${siteId}', 'Ideas', 'newest', 1773753606, 1773753606);
52
+
53
+ INSERT INTO "post" ("id", "site_id", "format", "status", "visibility", "thread_id", "created_at", "updated_at")
54
+ VALUES ('${postId}', '${siteId}', 'note', 'published', 'public', '${postId}', 1773753607, 1773753607);
55
+
56
+ INSERT INTO "thread_collection" ("site_id", "thread_id", "collection_id", "created_at", "position", "pinned_at")
57
+ VALUES ('${siteId}', '${postId}', '${collectionId}', 1773753608, 3, NULL);
47
58
  `);
48
59
  } finally {
49
60
  sqlite.close();
@@ -58,6 +69,9 @@ describe("jant export", () => {
58
69
  expect(output).toContain(
59
70
  `INSERT INTO "site_setting" ("site_id", "key", "value", "updated_at") VALUES('${siteId}', 'SITE_NAME', 'Test Site', 1773753605);`,
60
71
  );
72
+ expect(output).toContain(
73
+ `INSERT INTO "thread_collection" ("site_id", "thread_id", "collection_id", "created_at", "position", "pinned_at") VALUES('${siteId}', '${postId}', '${collectionId}', 1773753608, 3, NULL);`,
74
+ );
61
75
  expect(output).not.toContain("__drizzle_migrations");
62
76
  expect(logSpy).toHaveBeenCalledWith(
63
77
  `Exported Node database to ${exportPath}`,
@@ -83,6 +83,12 @@ describe("jant migrate", () => {
83
83
  vi.doMock("../../../bin/lib/load-node-runtime.js", () => ({
84
84
  loadNodeRuntime: vi.fn(async () => ({ migrate })),
85
85
  }));
86
+ vi.doMock("../../../bin/lib/thread-collection-migration.js", () => ({
87
+ preflightThreadCollectionMigration: vi.fn(async () => ({
88
+ phase: "fresh",
89
+ })),
90
+ verifyThreadCollectionMigration: vi.fn(async () => undefined),
91
+ }));
86
92
 
87
93
  const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
88
94
 
@@ -16,6 +16,8 @@ const SNAPSHOT_COLLECTION_ID = "col_01jpyy08bc4w2h8r7m3q5t9kdn";
16
16
  const SNAPSHOT_NAV_ID = "nav_01jpyy0gqv4m7r2k8s5c1t9bdh";
17
17
  const SNAPSHOT_DIRECTORY_ITEM_ID = "cdi_01jpyy0r6s3m8v1k5t9q2b4gcn";
18
18
  const SNAPSHOT_POST_ID = "pst_01jpyy18fh4w2m7r8k3c5t9qdn";
19
+ const SNAPSHOT_REPLY_ID = "pst_01jpyy1e6s4m8v2k5t9c3b7qdh";
20
+ const SNAPSHOT_SECOND_COLLECTION_ID = "col_01jpyy0c4s7m8r1k5t9b3q6dgh";
19
21
  const SNAPSHOT_PATH_ID = "pth_01jpyy1k2v6m4s8r1t5c9b3qgh";
20
22
  const SNAPSHOT_MEDIA_ID = "med_01jpyy1vxh4m7s2k8r5c9t3qbn";
21
23
  const SNAPSHOT_AVATAR_MEDIA_ID = "med_01jpyy1zs6m4v8r2k5t9c3b7qh";
@@ -145,7 +147,7 @@ describe("jant site snapshot export/import", () => {
145
147
  '${SNAPSHOT_POST_ID}', 1774009200, 1774009200, 1774009200, 1774009200
146
148
  );
147
149
 
148
- INSERT INTO "post_collection" ("site_id", "post_id", "collection_id", "created_at")
150
+ INSERT INTO "thread_collection" ("site_id", "thread_id", "collection_id", "created_at")
149
151
  VALUES ('${SNAPSHOT_SITE_ID}', '${SNAPSHOT_POST_ID}', '${SNAPSHOT_COLLECTION_ID}', 1774009200);
150
152
 
151
153
  INSERT INTO "path_registry" (
@@ -240,7 +242,7 @@ describe("jant site snapshot export/import", () => {
240
242
  );
241
243
  expect(meta).toEqual({
242
244
  format: "jant-site-snapshot",
243
- version: 1,
245
+ version: 2,
244
246
  dialect: "sqlite",
245
247
  site: { id: SNAPSHOT_SITE_ID, key: SNAPSHOT_SITE_KEY },
246
248
  });
@@ -281,6 +283,16 @@ describe("jant site snapshot export/import", () => {
281
283
  poster_key: SNAPSHOT_POSTER_KEY,
282
284
  });
283
285
 
286
+ const collectionMembership = verifySqlite
287
+ .prepare(
288
+ `SELECT "thread_id", "collection_id" FROM "thread_collection" WHERE "site_id" = '${SNAPSHOT_SITE_ID}'`,
289
+ )
290
+ .get();
291
+ expect(collectionMembership).toEqual({
292
+ thread_id: SNAPSHOT_POST_ID,
293
+ collection_id: SNAPSHOT_COLLECTION_ID,
294
+ });
295
+
284
296
  const siteName = verifySqlite
285
297
  .prepare(
286
298
  `SELECT "value" FROM "site_setting" WHERE "site_id" = '${SNAPSHOT_SITE_ID}' AND "key" = 'SITE_NAME'`,
@@ -339,6 +351,197 @@ describe("jant site snapshot export/import", () => {
339
351
  );
340
352
  });
341
353
 
354
+ it("imports v1 per-post Collection rows as a Thread union", async () => {
355
+ const root = await mkdtemp(join(tmpdir(), "jant-site-snapshot-v1-"));
356
+ tempDirs.push(root);
357
+ const targetDbPath = join(root, "target.sqlite");
358
+ const snapshotPath = join(root, "snapshot");
359
+ await migrate({ DATABASE_URL: `file:${targetDbPath}` } as Bindings);
360
+
361
+ const sqlite = new Database(targetDbPath);
362
+ try {
363
+ sqlite.exec(`
364
+ INSERT INTO "site" ("id", "key", "status", "created_at", "updated_at")
365
+ VALUES ('${SNAPSHOT_SITE_ID}', '${SNAPSHOT_SITE_KEY}', 'active', 1, 1);
366
+ `);
367
+ } finally {
368
+ sqlite.close();
369
+ }
370
+
371
+ await mkdir(snapshotPath, { recursive: true });
372
+ await writeFile(
373
+ join(snapshotPath, "meta.json"),
374
+ JSON.stringify({
375
+ format: "jant-site-snapshot",
376
+ version: 1,
377
+ dialect: "sqlite",
378
+ site: { id: SNAPSHOT_SITE_ID, key: SNAPSHOT_SITE_KEY },
379
+ }),
380
+ );
381
+ await writeFile(
382
+ join(snapshotPath, "db.sql"),
383
+ `
384
+ INSERT INTO "collection" ("id", "site_id", "title", "sort_order", "created_at", "updated_at") VALUES('${SNAPSHOT_COLLECTION_ID}', '${SNAPSHOT_SITE_ID}', 'Ideas', 'newest', 10, 10);
385
+ INSERT INTO "collection" ("id", "site_id", "title", "sort_order", "created_at", "updated_at") VALUES('${SNAPSHOT_SECOND_COLLECTION_ID}', '${SNAPSHOT_SITE_ID}', 'Walks', 'newest', 10, 10);
386
+ INSERT INTO "post" ("id", "site_id", "format", "status", "visibility", "reply_to_id", "thread_id", "created_at", "updated_at") VALUES('${SNAPSHOT_POST_ID}', '${SNAPSHOT_SITE_ID}', 'note', 'published', 'public', NULL, '${SNAPSHOT_POST_ID}', 10, 10);
387
+ INSERT INTO "post" ("id", "site_id", "format", "status", "visibility", "reply_to_id", "thread_id", "created_at", "updated_at") VALUES('${SNAPSHOT_REPLY_ID}', '${SNAPSHOT_SITE_ID}', 'note', 'published', 'public', '${SNAPSHOT_POST_ID}', '${SNAPSHOT_POST_ID}', 11, 11);
388
+ INSERT INTO "post_collection" ("site_id", "post_id", "collection_id", "created_at", "position", "pinned_at") VALUES('${SNAPSHOT_SITE_ID}', '${SNAPSHOT_POST_ID}', '${SNAPSHOT_COLLECTION_ID}', 20, 5, 30);
389
+ INSERT INTO "post_collection" ("site_id", "post_id", "collection_id", "created_at", "position", "pinned_at") VALUES('${SNAPSHOT_SITE_ID}', '${SNAPSHOT_REPLY_ID}', '${SNAPSHOT_COLLECTION_ID}', 40, 2, 35);
390
+ INSERT INTO "post_collection" ("site_id", "post_id", "collection_id", "created_at", "position", "pinned_at") VALUES('${SNAPSHOT_SITE_ID}', '${SNAPSHOT_REPLY_ID}', '${SNAPSHOT_SECOND_COLLECTION_ID}', 25, 7, NULL);
391
+ `,
392
+ );
393
+
394
+ useLocalSnapshotRuntime(`file:${targetDbPath}`, join(root, "target-media"));
395
+ vi.spyOn(console, "log").mockImplementation(() => {});
396
+ const { run: runImport } =
397
+ await import("../../../bin/commands/site/snapshot/import.js");
398
+ await runImport(["--path", snapshotPath, "--replace"]);
399
+
400
+ const verifySqlite = new Database(targetDbPath, { readonly: true });
401
+ try {
402
+ const rows = verifySqlite
403
+ .prepare(
404
+ `SELECT "thread_id", "collection_id", "created_at", "position", "pinned_at" FROM "thread_collection" ORDER BY "collection_id"`,
405
+ )
406
+ .all();
407
+ expect(rows).toEqual([
408
+ {
409
+ thread_id: SNAPSHOT_POST_ID,
410
+ collection_id: SNAPSHOT_COLLECTION_ID,
411
+ created_at: 40,
412
+ position: 2,
413
+ pinned_at: 35,
414
+ },
415
+ {
416
+ thread_id: SNAPSHOT_POST_ID,
417
+ collection_id: SNAPSHOT_SECOND_COLLECTION_ID,
418
+ created_at: 25,
419
+ position: 7,
420
+ pinned_at: null,
421
+ },
422
+ ]);
423
+ } finally {
424
+ verifySqlite.close();
425
+ }
426
+ });
427
+
428
+ it("rejects malformed v1 memberships before clearing target content", async () => {
429
+ const root = await mkdtemp(
430
+ join(tmpdir(), "jant-site-snapshot-v1-preflight-"),
431
+ );
432
+ tempDirs.push(root);
433
+ const targetDbPath = join(root, "target.sqlite");
434
+ const snapshotPath = join(root, "snapshot");
435
+ await migrate({ DATABASE_URL: `file:${targetDbPath}` } as Bindings);
436
+
437
+ const sqlite = new Database(targetDbPath);
438
+ try {
439
+ sqlite.exec(`
440
+ INSERT INTO "site" ("id", "key", "status", "created_at", "updated_at")
441
+ VALUES ('${SNAPSHOT_SITE_ID}', '${SNAPSHOT_SITE_KEY}', 'active', 1, 1);
442
+ INSERT INTO "post" ("id", "site_id", "format", "status", "visibility", "thread_id", "created_at", "updated_at")
443
+ VALUES ('${SNAPSHOT_OLD_POST_ID}', '${SNAPSHOT_SITE_ID}', 'note', 'published', 'public', '${SNAPSHOT_OLD_POST_ID}', 1, 1);
444
+ `);
445
+ } finally {
446
+ sqlite.close();
447
+ }
448
+
449
+ await mkdir(snapshotPath, { recursive: true });
450
+ await writeFile(
451
+ join(snapshotPath, "meta.json"),
452
+ JSON.stringify({
453
+ format: "jant-site-snapshot",
454
+ version: 1,
455
+ dialect: "sqlite",
456
+ site: { id: SNAPSHOT_SITE_ID, key: SNAPSHOT_SITE_KEY },
457
+ }),
458
+ );
459
+ await writeFile(
460
+ join(snapshotPath, "db.sql"),
461
+ `INSERT INTO "post_collection" ("site_id", "post_id", "collection_id", "created_at", "position", "pinned_at") VALUES('${SNAPSHOT_SITE_ID}', 'pst_missing', '${SNAPSHOT_COLLECTION_ID}', 10, 0, NULL);`,
462
+ );
463
+
464
+ useLocalSnapshotRuntime(`file:${targetDbPath}`, join(root, "target-media"));
465
+ const { run: runImport } =
466
+ await import("../../../bin/commands/site/snapshot/import.js");
467
+ await expect(
468
+ runImport(["--path", snapshotPath, "--replace"]),
469
+ ).rejects.toThrow(/references missing post pst_missing/);
470
+
471
+ const verifySqlite = new Database(targetDbPath, { readonly: true });
472
+ try {
473
+ expect(
474
+ verifySqlite
475
+ .prepare(
476
+ `SELECT COUNT(*) FROM "post" WHERE "id" = '${SNAPSHOT_OLD_POST_ID}'`,
477
+ )
478
+ .pluck()
479
+ .get(),
480
+ ).toBe(1);
481
+ } finally {
482
+ verifySqlite.close();
483
+ }
484
+ });
485
+
486
+ it("rolls back a Node snapshot replacement when SQL replay fails", async () => {
487
+ const root = await mkdtemp(
488
+ join(tmpdir(), "jant-site-snapshot-atomic-replace-"),
489
+ );
490
+ tempDirs.push(root);
491
+ const targetDbPath = join(root, "target.sqlite");
492
+ const snapshotPath = join(root, "snapshot");
493
+ await migrate({ DATABASE_URL: `file:${targetDbPath}` } as Bindings);
494
+
495
+ const sqlite = new Database(targetDbPath);
496
+ try {
497
+ sqlite.exec(`
498
+ INSERT INTO "site" ("id", "key", "status", "created_at", "updated_at")
499
+ VALUES ('${SNAPSHOT_SITE_ID}', '${SNAPSHOT_SITE_KEY}', 'active', 1, 1);
500
+ INSERT INTO "post" ("id", "site_id", "format", "status", "visibility", "thread_id", "created_at", "updated_at")
501
+ VALUES ('${SNAPSHOT_OLD_POST_ID}', '${SNAPSHOT_SITE_ID}', 'note', 'published', 'public', '${SNAPSHOT_OLD_POST_ID}', 1, 1);
502
+ `);
503
+ } finally {
504
+ sqlite.close();
505
+ }
506
+
507
+ await mkdir(snapshotPath, { recursive: true });
508
+ await writeFile(
509
+ join(snapshotPath, "meta.json"),
510
+ JSON.stringify({
511
+ format: "jant-site-snapshot",
512
+ version: 2,
513
+ dialect: "sqlite",
514
+ site: { id: SNAPSHOT_SITE_ID, key: SNAPSHOT_SITE_KEY },
515
+ }),
516
+ );
517
+ await writeFile(
518
+ join(snapshotPath, "db.sql"),
519
+ `
520
+ INSERT INTO "post" ("id", "site_id", "format", "status", "visibility", "thread_id", "created_at", "updated_at")
521
+ VALUES ('${SNAPSHOT_POST_ID}', '${SNAPSHOT_SITE_ID}', 'note', 'published', 'public', '${SNAPSHOT_POST_ID}', 2, 2);
522
+ INSERT INTO "thread_collection" ("site_id", "thread_id", "collection_id", "created_at")
523
+ VALUES ('${SNAPSHOT_SITE_ID}', '${SNAPSHOT_POST_ID}', '${SNAPSHOT_COLLECTION_ID}', 2);
524
+ `,
525
+ );
526
+
527
+ useLocalSnapshotRuntime(`file:${targetDbPath}`, join(root, "target-media"));
528
+ const { run: runImport } =
529
+ await import("../../../bin/commands/site/snapshot/import.js");
530
+ await expect(
531
+ runImport(["--path", snapshotPath, "--replace"]),
532
+ ).rejects.toThrow(/FOREIGN KEY constraint failed/);
533
+
534
+ const verifySqlite = new Database(targetDbPath, { readonly: true });
535
+ try {
536
+ const postIds = verifySqlite
537
+ .prepare(`SELECT "id" FROM "post" WHERE "site_id" = ? ORDER BY "id"`)
538
+ .all(SNAPSHOT_SITE_ID);
539
+ expect(postIds).toEqual([{ id: SNAPSHOT_OLD_POST_ID }]);
540
+ } finally {
541
+ verifySqlite.close();
542
+ }
543
+ });
544
+
342
545
  it("skips downloading storage objects when --skip-objects is passed", async () => {
343
546
  const root = await mkdtemp(
344
547
  join(tmpdir(), "jant-site-snapshot-skip-objects-"),